Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/server/src/environment/ServerEnvironment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
projectWorktreeCleanup: true,
worktreesDirectory: true,
threadAutoSettlementScope: true,
threadAutoSettleSnoozedOnMerge: true,
});
}
}),
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/environment/ServerEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ export const make = Effect.gen(function* () {
settledWorktreeCleanup: true,
worktreesDirectory: true,
threadAutoSettlementScope: true,
threadAutoSettleSnoozedOnMerge: true,
threadRestartContinuation: true,
projectSettingsOverrides: true,
projectProviderInstanceOverrides: true,
Expand Down
11 changes: 11 additions & 0 deletions apps/server/src/orchestration-v2/ThreadSettlementService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ describe("isAutoSettlementCandidate", () => {
snoozedAt: at(-60 * 60 * 1_000),
});
expect(ThreadSettlementService.isAutoSettlementCandidate(snoozed, NOW_MS)).toBe(false);
expect(ThreadSettlementService.isAutoSettlementCandidate(snoozed, NOW_MS, true)).toBe(true);
expect(
ThreadSettlementService.isAutoSettlementCandidate(
shell({ ...snoozed, status: "failed", latestRunCompletedAt: at(-30 * 60 * 1_000) }),
Expand Down Expand Up @@ -1450,5 +1451,15 @@ describe("isSnoozed", () => {
autoSettleOnMerge: true,
}),
).toBeNull();
expect(
ThreadSettlementService.resolveAutoSettlementAt({
thread,
pullRequest: { state: "merged", mergedAt: DateTime.formatIso(at(-1)) },
nowMs: NOW_MS,
autoSettleAfterDays: null,
autoSettleOnMerge: true,
autoSettleSnoozedOnMerge: true,
}),
).toEqual(at(-DAY_MS));
});
});
31 changes: 26 additions & 5 deletions apps/server/src/orchestration-v2/ThreadSettlementService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ function pullRequestSettles(
export function isAutoSettlementCandidate(
thread: Omit<ProjectionStore.ProjectionSettlementCandidate, "latestUserAuthoredMessageAt">,
nowMs: number,
settleSnoozedOnMerge = false,
): boolean {
if (thread.archivedAt !== null || thread.settledOverride !== null) return false;
if (thread.pinnedAt != null || thread.autoSettleDisabledAt != null) return false;
Expand All @@ -155,7 +156,7 @@ export function isAutoSettlementCandidate(
if (backgroundWorkHoldsCompletion(thread.pendingBackgroundTasks ?? [])) return false;
if (threadHasQueuedTurnStart(thread, nowMs)) return false;
// Interrupted runs must keep the same snooze protection the sidebar shows.
return !isSnoozed(thread, nowMs);
return settleSnoozedOnMerge || !isSnoozed(thread, nowMs);
}

/** Match client effectiveSnoozed so agents and sidebar agree. */
Expand Down Expand Up @@ -191,6 +192,7 @@ export function resolveAutoSettlementAt(input: {
readonly autoSettleAfterDays: number | null;
readonly autoSettleOnMerge: boolean;
readonly autoSettleKeepPullRequests?: boolean;
readonly autoSettleSnoozedOnMerge?: boolean;
}): DateTime.Utc | null {
const { thread } = input;
let pullRequest = input.pullRequest;
Expand All @@ -214,17 +216,24 @@ export function resolveAutoSettlementAt(input: {
closedAt: latest.snapshot.closedAt ?? null,
};
}
if (!isAutoSettlementCandidate(thread, input.nowMs)) return null;
const settleSnoozedOnMerge = input.autoSettleOnMerge && input.autoSettleSnoozedOnMerge === true;
if (!isAutoSettlementCandidate(thread, input.nowMs, settleSnoozedOnMerge)) return null;
const snoozed = isSnoozed(thread, input.nowMs);
const activityAtMs =
latestMillis([
toMillis(thread.latestUserMessageAt),
toMillis(thread.latestRunRequestedAt),
toMillis(thread.latestRunStartedAt),
toMillis(thread.latestRunCompletedAt),
]) ?? DateTime.toEpochMillis(thread.createdAt);
if (pullRequest !== null && pullRequestSettles(thread, pullRequest, input.autoSettleOnMerge)) {
if (
pullRequest !== null &&
(!snoozed || pullRequest.state === "merged") &&
pullRequestSettles(thread, pullRequest, input.autoSettleOnMerge)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
) {
return DateTime.makeUnsafe(activityAtMs);
}
if (snoozed) return null;
const hasPullRequest =
hasOpenLink ||
pullRequest !== null ||
Expand Down Expand Up @@ -269,12 +278,14 @@ export function autoSettlementSettingsKey(
settings.sidebarAutoSettleOnMerge,
settings.sidebarAutoSettleAfterDays,
settings.sidebarAutoSettleKeepPullRequests,
settings.sidebarAutoSettleSnoozedOnMerge,
Object.entries(settings.projectSettingsOverrides)
.filter(
([, entry]) =>
entry.sidebarAutoSettleOnMerge !== undefined ||
entry.sidebarAutoSettleAfterDays !== undefined ||
entry.sidebarAutoSettleKeepPullRequests !== undefined,
entry.sidebarAutoSettleKeepPullRequests !== undefined ||
entry.sidebarAutoSettleSnoozedOnMerge !== undefined,
)
.sort(([left], [right]) => left.localeCompare(right))
.map(([projectId, entry]) => [
Expand All @@ -284,6 +295,7 @@ export function autoSettlementSettingsKey(
? "inherit"
: entry.sidebarAutoSettleAfterDays,
entry.sidebarAutoSettleKeepPullRequests ?? "inherit",
entry.sidebarAutoSettleSnoozedOnMerge ?? "inherit",
]),
]);
}
Expand Down Expand Up @@ -319,7 +331,14 @@ export const make = Effect.gen(function* () {
const nowMs = DateTime.toEpochMillis(yield* DateTime.now);
const projects = new Map(projectShells.map((project) => [project.id, project]));
// Merge events resweep all candidates because unlinked threads settle through branch lookup.
const candidates = threads.filter((thread) => isAutoSettlementCandidate(thread, nowMs));
const candidates = threads.filter((thread) => {
const { settings: projectSettings } = resolveProjectSettings(settings, thread.projectId);
return isAutoSettlementCandidate(
thread,
nowMs,
projectSettings.sidebarAutoSettleOnMerge && projectSettings.sidebarAutoSettleSnoozedOnMerge,
);
});

// Runs per candidate thread, thousands per sweep on busy servers; the sweep owns the span.
const settleThread = Effect.fnUntraced(
Expand All @@ -337,6 +356,7 @@ export const make = Effect.gen(function* () {
autoSettleAfterDays: currentSettings.sidebarAutoSettleAfterDays,
autoSettleOnMerge: currentSettings.sidebarAutoSettleOnMerge,
autoSettleKeepPullRequests: currentSettings.sidebarAutoSettleKeepPullRequests,
autoSettleSnoozedOnMerge: currentSettings.sidebarAutoSettleSnoozedOnMerge,
});
if (settledAt === null) return thread;
const uuid = yield* crypto.randomUUIDv4();
Expand Down Expand Up @@ -433,6 +453,7 @@ export const make = Effect.gen(function* () {
autoSettleAfterDays: settings.sidebarAutoSettleAfterDays,
autoSettleOnMerge: settings.sidebarAutoSettleOnMerge,
autoSettleKeepPullRequests: settings.sidebarAutoSettleKeepPullRequests,
autoSettleSnoozedOnMerge: settings.sidebarAutoSettleSnoozedOnMerge,
}) !== null
);
});
Expand Down
27 changes: 27 additions & 0 deletions apps/web/src/components/settings/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,10 @@ export function useSettingsRestore(onRestored?: () => void) {
...(settings.sidebarAutoSettleOnMerge !== DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleOnMerge
? ["Auto-settle merged threads"]
: []),
...(settings.sidebarAutoSettleSnoozedOnMerge !==
DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleSnoozedOnMerge
? ["Settle snoozed threads on merge"]
: []),
...(settings.autoResumeLimitedThreads !== DEFAULT_UNIFIED_SETTINGS.autoResumeLimitedThreads
? ["Resume after usage limits"]
: []),
Expand Down Expand Up @@ -634,6 +638,7 @@ export function useSettingsRestore(onRestored?: () => void) {
sidebarAutoSettleAfterDays: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleAfterDays,
sidebarAutoSettleKeepPullRequests: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleKeepPullRequests,
sidebarAutoSettleOnMerge: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleOnMerge,
sidebarAutoSettleSnoozedOnMerge: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleSnoozedOnMerge,
autoResumeLimitedThreads: DEFAULT_UNIFIED_SETTINGS.autoResumeLimitedThreads,
snoozeLimitedThreads: DEFAULT_UNIFIED_SETTINGS.snoozeLimitedThreads,
snoozePresets: DEFAULT_UNIFIED_SETTINGS.snoozePresets,
Expand Down Expand Up @@ -1492,6 +1497,10 @@ export function ThreadsSettingsPanel() {
const supportsAutoSettlementScope = connectedEnvironments.every(
(target) => target.serverConfig?.environment.capabilities.threadAutoSettlementScope === true,
);
const supportsAutoSettleSnoozedOnMerge = connectedEnvironments.every(
(target) =>
target.serverConfig?.environment.capabilities.threadAutoSettleSnoozedOnMerge === true,
);

const textGenerationProviders = serverProviders.filter(
(provider) => provider.supportsTextGeneration !== false,
Expand Down Expand Up @@ -1642,6 +1651,24 @@ export function ThreadsSettingsPanel() {
/>
}
/>
{settings.sidebarAutoSettleOnMerge && supportsAutoSettleSnoozedOnMerge ? (
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
<SettingsRow
serverScoped
settingKeys={["sidebarAutoSettleSnoozedOnMerge"]}
title={searchableSetting("settle-snoozed-threads-on-merge").title}
description="A merge also settles snoozed threads. Off keeps them snoozed until they wake."
control={
<ScopedSwitch
settingKeys={["sidebarAutoSettleSnoozedOnMerge"]}
checked={settings.sidebarAutoSettleSnoozedOnMerge}
onCheckedChange={(checked) =>
updateSettings({ sidebarAutoSettleSnoozedOnMerge: Boolean(checked) })
}
aria-label="Settle snoozed threads on merge"
/>
}
/>
) : null}

<SettingsRow
serverScoped
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/settings/settingsSearch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ describe("auto-settlement search availability", () => {
"auto-settle-inactive-threads",
"auto-settle-merged-threads",
"days-before-auto-settle",
"settle-snoozed-threads-on-merge",
"keep-threads-with-pull-requests",
]);
});
Expand Down
9 changes: 9 additions & 0 deletions apps/web/src/components/settings/settingsSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,15 @@ export const SETTINGS_SEARCH_ITEMS = [
requiresThreadAutoSettlement: true,
scope: "project-defaults",
},
{
id: "settle-snoozed-threads-on-merge",
title: "Settle snoozed threads on merge",
to: "/settings/threads",
targetId: "auto-settle-merged-threads",
searchTerms: ["snooze pull request merge auto-settle sidebar wake"],
requiresThreadAutoSettlement: true,
scope: "project-defaults",
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
{
id: "keep-threads-with-pull-requests",
title: "Keep threads with a PR",
Expand Down
2 changes: 2 additions & 0 deletions packages/contracts/src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ export const ExecutionEnvironmentCapabilities = Schema.Struct({
settledWorktreeCleanup: Schema.optionalKey(Schema.Boolean),
worktreesDirectory: Schema.optionalKey(Schema.Boolean),
threadAutoSettlementScope: Schema.optionalKey(Schema.Boolean),
/** Hide the switch when absent; older servers keep snoozed threads through a merge. */
threadAutoSettleSnoozedOnMerge: Schema.optionalKey(Schema.Boolean),
threadRestartContinuation: Schema.optionalKey(Schema.Boolean),
projectSettingsOverrides: Schema.optionalKey(Schema.Boolean),
/** Hide the switch when absent; older servers silently drop per-project provider patches. */
Expand Down
6 changes: 6 additions & 0 deletions packages/contracts/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1092,6 +1092,7 @@ export const PROJECT_SCOPED_SERVER_SETTING_KEYS = [
"sidebarAutoSettleOnMerge",
"sidebarAutoSettleAfterDays",
"sidebarAutoSettleKeepPullRequests",
"sidebarAutoSettleSnoozedOnMerge",
"continueThreadsAfterServerUpdate",
"responseStreamingMode",
] as const;
Expand Down Expand Up @@ -1129,6 +1130,7 @@ export const ProjectSettingsOverrides = Schema.Struct({
sidebarAutoSettleOnMerge: Schema.optionalKey(Schema.Boolean),
sidebarAutoSettleAfterDays: Schema.optionalKey(Schema.NullOr(SidebarAutoSettleAfterDays)),
sidebarAutoSettleKeepPullRequests: Schema.optionalKey(Schema.Boolean),
sidebarAutoSettleSnoozedOnMerge: Schema.optionalKey(Schema.Boolean),
continueThreadsAfterServerUpdate: Schema.optionalKey(Schema.Boolean),
responseStreamingMode: Schema.optionalKey(ResponseStreamingMode),
providerInstances: Schema.optionalKey(
Expand Down Expand Up @@ -1312,6 +1314,9 @@ export const ServerSettings = Schema.Struct({
Schema.withDecodingDefault(Effect.succeed(false)),
),
sidebarAutoSettleOnMerge: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
sidebarAutoSettleSnoozedOnMerge: Schema.Boolean.pipe(
Schema.withDecodingDefault(Effect.succeed(false)),
),
backgroundActivity: BackgroundActivitySettings,

automaticGitFetchInterval: Schema.DurationFromMillis.pipe(
Expand Down Expand Up @@ -1658,6 +1663,7 @@ export const ServerSettingsPatch = Schema.Struct({
idleCompaction: Schema.optionalKey(IdleCompactionMode),
sidebarAutoSettleKeepPullRequests: Schema.optionalKey(Schema.Boolean),
sidebarAutoSettleOnMerge: Schema.optionalKey(Schema.Boolean),
sidebarAutoSettleSnoozedOnMerge: Schema.optionalKey(Schema.Boolean),
autoResumeLimitedThreads: Schema.optionalKey(Schema.Boolean),
snoozeLimitedThreads: Schema.optionalKey(Schema.Boolean),
snoozePresets: Schema.optionalKey(SnoozePresetSettings),
Expand Down
Loading