| author | |
| committer | |
| log | 0a4d789128100ed7ccd6d42f454946aea8edfca8 |
| tree | 856d2617f0e61e467eb1094cfb05772381ba73cd |
| parent | 5722496b79079ac317a984f3baca8c88f41af070 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>6 files changed, 57 insertions(+), 4 deletions(-)
apps/server/src/orchestration-v2/Orchestrator.ts+3-1| ... | ... | @@ -2831,6 +2831,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio |
| 2831 | 2831 | ...thread, |
| 2832 | 2832 | snoozedUntil: null, |
| 2833 | 2833 | snoozedAt: null, |
| 2834 | lastSnoozeWakeAt: alreadyAwake ? thread.lastSnoozeWakeAt : now, | |
| 2834 | 2835 | updatedAt: alreadyAwake ? thread.updatedAt : now, |
| 2835 | 2836 | }; |
| 2836 | 2837 | } |
| ... | ... | @@ -2862,6 +2863,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio |
| 2862 | 2863 | settledAt: thread.settledOverride === "settled" ? null : thread.settledAt, |
| 2863 | 2864 | snoozedUntil: null, |
| 2864 | 2865 | snoozedAt: null, |
| 2866 | lastSnoozeWakeAt: thread.snoozedUntil == null ? thread.lastSnoozeWakeAt : now, | |
| 2865 | 2867 | updatedAt: alreadyPinned && !promotes ? thread.updatedAt : now, |
| 2866 | 2868 | }; |
| 2867 | 2869 | } |
| ... | ... | @@ -2933,7 +2935,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio |
| 2933 | 2935 | thread.snoozedUntil != null && |
| 2934 | 2936 | DateTime.toEpochMillis(thread.snoozedUntil) === |
| 2935 | 2937 | Date.parse(thread.limitRecovery.resetAt) |
| 2936 | ? { snoozedUntil: null, snoozedAt: null } | |
| 2938 | ? { snoozedUntil: null, snoozedAt: null, lastSnoozeWakeAt: now } | |
| 2937 | 2939 | : {}), |
| 2938 | 2940 | ...(command.branch === undefined ? {} : { branch: command.branch }), |
| 2939 | 2941 | ...(command.worktreePath === undefined ? {} : { worktreePath: command.worktreePath }), |
apps/server/src/orchestration-v2/ProjectionStore.ts+5-1| ... | ... | @@ -204,7 +204,10 @@ export type ProjectionSettlementCandidate = Pick< |
| 204 | 204 | | "activityRunStatus" |
| 205 | 205 | | "pendingRuntimeRequest" |
| 206 | 206 | | "pendingBackgroundTasks" |
| 207 | > & { readonly latestUserAuthoredMessageAt: DateTime.Utc | null }; | |
| 207 | > & | |
| 208 | Pick<OrchestrationV2AppThread, "lastSnoozeWakeAt"> & { | |
| 209 | readonly latestUserAuthoredMessageAt: DateTime.Utc | null; | |
| 210 | }; | |
| 208 | 211 | |
| 209 | 212 | const ProjectionCheckpointContext = Schema.Struct({ |
| 210 | 213 | runs: Schema.Array( |
| ... | ... | @@ -6035,6 +6038,7 @@ export const layerMemory: Layer.Layer<ProjectionStoreV2> = Layer.effect( |
| 6035 | 6038 | const shell = threadShellFromProjection(projection); |
| 6036 | 6039 | return { |
| 6037 | 6040 | ...shell, |
| 6041 | lastSnoozeWakeAt: projection.thread.lastSnoozeWakeAt ?? null, | |
| 6038 | 6042 | latestUserAuthoredMessageAt: shell.latestUserAuthoredMessageAt ?? null, |
| 6039 | 6043 | }; |
| 6040 | 6044 | }) |
apps/server/src/orchestration-v2/ThreadSettlementService.test.ts+20| ... | ... | @@ -276,6 +276,26 @@ describe("resolveAutoSettlementAt", () => { |
| 276 | 276 | ).toBeNull(); |
| 277 | 277 | }); |
| 278 | 278 | |
| 279 | it.each(["manual", "timer"])("restarts inactivity on a %s snooze wake", (wake) => { | |
| 280 | const thread = { | |
| 281 | ...shell({ latestRunCompletedAt: at(-4 * DAY_MS) }), | |
| 282 | ...(wake === "manual" | |
| 283 | ? { lastSnoozeWakeAt: at(-1_000) } | |
| 284 | : { snoozedAt: at(-3 * DAY_MS), snoozedUntil: at(-1_000) }), | |
| 285 | }; | |
| 286 | const input = { | |
| 287 | thread, | |
| 288 | pullRequest: null, | |
| 289 | nowMs: NOW_MS, | |
| 290 | autoSettleAfterDays: 1, | |
| 291 | autoSettleOnMerge: false, | |
| 292 | }; | |
| 293 | expect(ThreadSettlementService.resolveAutoSettlementAt(input)).toBeNull(); | |
| 294 | expect( | |
| 295 | ThreadSettlementService.resolveAutoSettlementAt({ ...input, nowMs: NOW_MS + DAY_MS }), | |
| 296 | ).toEqual(at(-1_000)); | |
| 297 | }); | |
| 298 | ||
| 279 | 299 | it("settles on merge only after the user's last action and preserves the activity time", () => { |
| 280 | 300 | const thread = shell({ |
| 281 | 301 | latestUserMessageAt: at(-2 * 60 * 60 * 1_000), |
apps/server/src/orchestration-v2/ThreadSettlementService.ts+8-2| ... | ... | @@ -237,8 +237,14 @@ export function resolveAutoSettlementAt(input: { |
| 237 | 237 | return activityAtMs === null ? thread.createdAt : DateTime.makeUnsafe(activityAtMs); |
| 238 | 238 | } |
| 239 | 239 | if (input.autoSettleAfterDays === null || activityAtMs === null) return null; |
| 240 | return activityAtMs < input.nowMs - input.autoSettleAfterDays * DAY_MS | |
| 241 | ? DateTime.makeUnsafe(activityAtMs) | |
| 240 | // A passed wake timer restarts inactivity. An earlier wake can only delay settlement. | |
| 241 | const snoozedUntilMs = toMillis(thread.snoozedUntil); | |
| 242 | const timerWakeAtMs = | |
| 243 | snoozedUntilMs !== null && snoozedUntilMs <= input.nowMs ? snoozedUntilMs : null; | |
| 244 | const inactivityAtMs = | |
| 245 | latestMillis([activityAtMs, toMillis(thread.lastSnoozeWakeAt), timerWakeAtMs]) ?? activityAtMs; | |
| 246 | return inactivityAtMs < input.nowMs - input.autoSettleAfterDays * DAY_MS | |
| 247 | ? DateTime.makeUnsafe(inactivityAtMs) | |
| 242 | 248 | : null; |
| 243 | 249 | } |
| 244 | 250 |
apps/server/src/orchestration-v2/runtimeLayer.test.ts+18| ... | ... | @@ -4548,6 +4548,24 @@ it.layer(layerSharedApplicationDataPlaneTest)("snooze projection", (it) => { |
| 4548 | 4548 | assert.deepEqual(thread.snoozedAt, firstSnoozedAt); |
| 4549 | 4549 | assert.deepEqual(thread.updatedAt, firstUpdatedAt); |
| 4550 | 4550 | |
| 4551 | yield* orchestrator.dispatch({ | |
| 4552 | type: "thread.unsnooze", | |
| 4553 | commandId: CommandId.make("runtime-layer-snoozed-thread-wake"), | |
| 4554 | threadId, | |
| 4555 | reason: "user", | |
| 4556 | }); | |
| 4557 | const projections = yield* ProjectionStore.ProjectionStoreV2.pipe( | |
| 4558 | Effect.provide(ProjectionStore.layer), | |
| 4559 | ); | |
| 4560 | const [candidate] = yield* projections.getSettlementCandidates(threadId); | |
| 4561 | assert.deepEqual(candidate?.lastSnoozeWakeAt, yield* DateTime.now); | |
| 4562 | yield* orchestrator.dispatch({ | |
| 4563 | type: "thread.snooze", | |
| 4564 | commandId: CommandId.make("runtime-layer-snoozed-thread-snooze-after-wake"), | |
| 4565 | threadId, | |
| 4566 | snoozedUntil, | |
| 4567 | }); | |
| 4568 | ||
| 4551 | 4569 | yield* orchestrator.dispatch({ |
| 4552 | 4570 | type: "message.dispatch", |
| 4553 | 4571 | createdBy: "user", |
packages/contracts/src/orchestrationV2.ts+3| ... | ... | @@ -404,6 +404,8 @@ export const OrchestrationV2AppThread = Schema.Struct({ |
| 404 | 404 | unsettledAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtc)), |
| 405 | 405 | snoozedUntil: Schema.optional(Schema.NullOr(Schema.DateTimeUtc)), |
| 406 | 406 | snoozedAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtc)), |
| 407 | /** Manual wakes restart inactivity without changing the sidebar's sort position. */ | |
| 408 | lastSnoozeWakeAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtc)), | |
| 407 | 409 | limitRecovery: Schema.optional(Schema.NullOr(OrchestrationV2LimitRecovery)), |
| 408 | 410 | pinnedAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtc)), |
| 409 | 411 | autoSettleDisabledAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtc)), |
| ... | ... | @@ -1999,6 +2001,7 @@ export const OrchestrationV2AppThreadJson = OrchestrationV2AppThread.mapFields(( |
| 1999 | 2001 | unsettledAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtcFromString)), |
| 2000 | 2002 | snoozedUntil: Schema.optional(Schema.NullOr(Schema.DateTimeUtcFromString)), |
| 2001 | 2003 | snoozedAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtcFromString)), |
| 2004 | lastSnoozeWakeAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtcFromString)), | |
| 2002 | 2005 | pinnedAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtcFromString)), |
| 2003 | 2006 | autoSettleDisabledAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtcFromString)), |
| 2004 | 2007 | lastVisitedAt: Schema.NullOr(Schema.DateTimeUtcFromString).pipe( |