| 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,6 +2831,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio |
| 2831 | ...thread, | 2831 | ...thread, |
| 2832 | snoozedUntil: null, | 2832 | snoozedUntil: null, |
| 2833 | snoozedAt: null, | 2833 | snoozedAt: null, |
| 2834 | lastSnoozeWakeAt: alreadyAwake ? thread.lastSnoozeWakeAt : now, | ||
| 2834 | updatedAt: alreadyAwake ? thread.updatedAt : now, | 2835 | updatedAt: alreadyAwake ? thread.updatedAt : now, |
| 2835 | }; | 2836 | }; |
| 2836 | } | 2837 | } |
| ... | @@ -2862,6 +2863,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio | ... | @@ -2862,6 +2863,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio |
| 2862 | settledAt: thread.settledOverride === "settled" ? null : thread.settledAt, | 2863 | settledAt: thread.settledOverride === "settled" ? null : thread.settledAt, |
| 2863 | snoozedUntil: null, | 2864 | snoozedUntil: null, |
| 2864 | snoozedAt: null, | 2865 | snoozedAt: null, |
| 2866 | lastSnoozeWakeAt: thread.snoozedUntil == null ? thread.lastSnoozeWakeAt : now, | ||
| 2865 | updatedAt: alreadyPinned && !promotes ? thread.updatedAt : now, | 2867 | updatedAt: alreadyPinned && !promotes ? thread.updatedAt : now, |
| 2866 | }; | 2868 | }; |
| 2867 | } | 2869 | } |
| ... | @@ -2933,7 +2935,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio | ... | @@ -2933,7 +2935,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio |
| 2933 | thread.snoozedUntil != null && | 2935 | thread.snoozedUntil != null && |
| 2934 | DateTime.toEpochMillis(thread.snoozedUntil) === | 2936 | DateTime.toEpochMillis(thread.snoozedUntil) === |
| 2935 | Date.parse(thread.limitRecovery.resetAt) | 2937 | Date.parse(thread.limitRecovery.resetAt) |
| 2936 | ? { snoozedUntil: null, snoozedAt: null } | 2938 | ? { snoozedUntil: null, snoozedAt: null, lastSnoozeWakeAt: now } |
| 2937 | : {}), | 2939 | : {}), |
| 2938 | ...(command.branch === undefined ? {} : { branch: command.branch }), | 2940 | ...(command.branch === undefined ? {} : { branch: command.branch }), |
| 2939 | ...(command.worktreePath === undefined ? {} : { worktreePath: command.worktreePath }), | 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,7 +204,10 @@ export type ProjectionSettlementCandidate = Pick< |
| 204 | | "activityRunStatus" | 204 | | "activityRunStatus" |
| 205 | | "pendingRuntimeRequest" | 205 | | "pendingRuntimeRequest" |
| 206 | | "pendingBackgroundTasks" | 206 | | "pendingBackgroundTasks" |
| 207 | > & { readonly latestUserAuthoredMessageAt: DateTime.Utc | null }; | 207 | > & |
| 208 | Pick<OrchestrationV2AppThread, "lastSnoozeWakeAt"> & { | ||
| 209 | readonly latestUserAuthoredMessageAt: DateTime.Utc | null; | ||
| 210 | }; | ||
| 208 | 211 | ||
| 209 | const ProjectionCheckpointContext = Schema.Struct({ | 212 | const ProjectionCheckpointContext = Schema.Struct({ |
| 210 | runs: Schema.Array( | 213 | runs: Schema.Array( |
| ... | @@ -6035,6 +6038,7 @@ export const layerMemory: Layer.Layer<ProjectionStoreV2> = Layer.effect( | ... | @@ -6035,6 +6038,7 @@ export const layerMemory: Layer.Layer<ProjectionStoreV2> = Layer.effect( |
| 6035 | const shell = threadShellFromProjection(projection); | 6038 | const shell = threadShellFromProjection(projection); |
| 6036 | return { | 6039 | return { |
| 6037 | ...shell, | 6040 | ...shell, |
| 6041 | lastSnoozeWakeAt: projection.thread.lastSnoozeWakeAt ?? null, | ||
| 6038 | latestUserAuthoredMessageAt: shell.latestUserAuthoredMessageAt ?? null, | 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,6 +276,26 @@ describe("resolveAutoSettlementAt", () => { |
| 276 | ).toBeNull(); | 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 | it("settles on merge only after the user's last action and preserves the activity time", () => { | 299 | it("settles on merge only after the user's last action and preserves the activity time", () => { |
| 280 | const thread = shell({ | 300 | const thread = shell({ |
| 281 | latestUserMessageAt: at(-2 * 60 * 60 * 1_000), | 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,8 +237,14 @@ export function resolveAutoSettlementAt(input: { |
| 237 | return activityAtMs === null ? thread.createdAt : DateTime.makeUnsafe(activityAtMs); | 237 | return activityAtMs === null ? thread.createdAt : DateTime.makeUnsafe(activityAtMs); |
| 238 | } | 238 | } |
| 239 | if (input.autoSettleAfterDays === null || activityAtMs === null) return null; | 239 | if (input.autoSettleAfterDays === null || activityAtMs === null) return null; |
| 240 | return activityAtMs < input.nowMs - input.autoSettleAfterDays * DAY_MS | 240 | // A passed wake timer restarts inactivity. An earlier wake can only delay settlement. |
| 241 | ? DateTime.makeUnsafe(activityAtMs) | 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 | : null; | 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,6 +4548,24 @@ it.layer(layerSharedApplicationDataPlaneTest)("snooze projection", (it) => { |
| 4548 | assert.deepEqual(thread.snoozedAt, firstSnoozedAt); | 4548 | assert.deepEqual(thread.snoozedAt, firstSnoozedAt); |
| 4549 | assert.deepEqual(thread.updatedAt, firstUpdatedAt); | 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 | yield* orchestrator.dispatch({ | 4569 | yield* orchestrator.dispatch({ |
| 4552 | type: "message.dispatch", | 4570 | type: "message.dispatch", |
| 4553 | createdBy: "user", | 4571 | createdBy: "user", |
packages/contracts/src/orchestrationV2.ts+3| ... | @@ -404,6 +404,8 @@ export const OrchestrationV2AppThread = Schema.Struct({ | ... | @@ -404,6 +404,8 @@ export const OrchestrationV2AppThread = Schema.Struct({ |
| 404 | unsettledAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtc)), | 404 | unsettledAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtc)), |
| 405 | snoozedUntil: Schema.optional(Schema.NullOr(Schema.DateTimeUtc)), | 405 | snoozedUntil: Schema.optional(Schema.NullOr(Schema.DateTimeUtc)), |
| 406 | snoozedAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtc)), | 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 | limitRecovery: Schema.optional(Schema.NullOr(OrchestrationV2LimitRecovery)), | 409 | limitRecovery: Schema.optional(Schema.NullOr(OrchestrationV2LimitRecovery)), |
| 408 | pinnedAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtc)), | 410 | pinnedAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtc)), |
| 409 | autoSettleDisabledAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtc)), | 411 | autoSettleDisabledAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtc)), |
| ... | @@ -1999,6 +2001,7 @@ export const OrchestrationV2AppThreadJson = OrchestrationV2AppThread.mapFields(( | ... | @@ -1999,6 +2001,7 @@ export const OrchestrationV2AppThreadJson = OrchestrationV2AppThread.mapFields(( |
| 1999 | unsettledAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtcFromString)), | 2001 | unsettledAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtcFromString)), |
| 2000 | snoozedUntil: Schema.optional(Schema.NullOr(Schema.DateTimeUtcFromString)), | 2002 | snoozedUntil: Schema.optional(Schema.NullOr(Schema.DateTimeUtcFromString)), |
| 2001 | snoozedAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtcFromString)), | 2003 | snoozedAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtcFromString)), |
| 2004 | lastSnoozeWakeAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtcFromString)), | ||
| 2002 | pinnedAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtcFromString)), | 2005 | pinnedAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtcFromString)), |
| 2003 | autoSettleDisabledAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtcFromString)), | 2006 | autoSettleDisabledAt: Schema.optional(Schema.NullOr(Schema.DateTimeUtcFromString)), |
| 2004 | lastVisitedAt: Schema.NullOr(Schema.DateTimeUtcFromString).pipe( | 2007 | lastVisitedAt: Schema.NullOr(Schema.DateTimeUtcFromString).pipe( |