diff --git a/packages/core/src/session/execution/restart.ts b/packages/core/src/session/execution/restart.ts index 72f606416c3..7038349350c 100644 --- a/packages/core/src/session/execution/restart.ts +++ b/packages/core/src/session/execution/restart.ts @@ -100,7 +100,6 @@ export const layer = (options?: Options) => const recoverShell = Effect.fnUntraced(function* ( background: Job.Background, recovery: Extract, - suspended: ReadonlySet, ) { const state = background.status === "running" ? "cancelled" : background.status const text = @@ -124,7 +123,9 @@ export const layer = (options?: Options) => state, text, }), - ...(suspended.has(recovery.sessionID) ? { resume: false } : {}), + // Restart notices must not revive idle owners of long-lived shells. + // Interrupted executions resume separately after their notices are admitted. + resume: false, }) .pipe( Effect.catchTag("Session.NotFoundError", () => Effect.void), @@ -208,7 +209,7 @@ export const layer = (options?: Options) => if ((yield* jobs.get(background.id))?.status === "running") return const recovery = background.recovery yield* recovery.kind === "shell" - ? recoverShell(background, recovery, suspended) + ? recoverShell(background, recovery) : recoverSubagent(background, recovery, suspended) }), { discard: true }, diff --git a/packages/core/test/session-execution.test.ts b/packages/core/test/session-execution.test.ts index 8cd7324f9e5..9f838e66ba3 100644 --- a/packages/core/test/session-execution.test.ts +++ b/packages/core/test/session-execution.test.ts @@ -379,7 +379,7 @@ describe("SessionExecution lifecycle", () => { }) describe("SessionRestart background recovery", () => { - it.effect("wakes idle shell owners and delivers recovered notices exactly once", () => + it.effect("keeps shell owners idle until a user prompt delivers recovered notices exactly once", () => Effect.gen(function* () { const database = yield* Database.Service const store = yield* SessionStore.Service @@ -416,6 +416,20 @@ describe("SessionRestart background recovery", () => { yield* restart.resumeSuspendedSessions yield* Effect.forEach([parent, child], execution.awaitIdle, { discard: true }) + expect(drained).toEqual([]) + expect(yield* SessionInbox.list(database.db, parent)).toHaveLength(1) + expect(yield* SessionInbox.list(database.db, child)).toHaveLength(1) + expect(yield* restarted.pendingBackground).toEqual([]) + yield* restart.resumeSuspendedSessions + expect(drained).toEqual([]) + expect(yield* SessionInbox.list(database.db, parent)).toHaveLength(1) + expect(yield* SessionInbox.list(database.db, child)).toHaveLength(1) + + yield* seedInbox(database, parent, ["steer"]) + yield* seedInbox(database, child, ["steer"]) + yield* execution.wake(parent) + yield* execution.wake(child) + yield* Effect.forEach([parent, child], execution.awaitIdle, { discard: true }) expect(drained.toSorted()).toEqual([parent, child].toSorted()) expect((yield* store.context(parent)).filter((message) => message.type === "synthetic")).toMatchObject([ { @@ -509,7 +523,7 @@ describe("SessionRestart background recovery", () => { yield* Context.get(context, SessionRestart.Service).resumeSuspendedSessions yield* Context.get(context, SessionExecution.Service).awaitIdle(sessionID) - expect(drained).toEqual([sessionID]) + expect(drained).toEqual([]) const inbox = yield* SessionInbox.list(database.db, sessionID) expect(inbox).toMatchObject([ { @@ -561,7 +575,6 @@ describe("SessionRestart background recovery", () => { expect(yield* restarted.pendingBackground).toEqual([]) expect(yield* SessionInbox.list(database.db, sessionID)).toHaveLength(delivered ? 0 : 1) yield* SessionInbox.promote(database.db, bus, sessionID, "steer") - // Recovery ends a busy period, so an idle marker follows the notification. const messages = (yield* sessions.messages({ sessionID })).filter((message) => message.type !== "idle") expect(messages).toMatchObject([ {