From 9c66c3de51a4e27402ccf6026f91361af591115c Mon Sep 17 00:00:00 2001 From: Ashvin Nihalani Date: Thu, 26 Mar 2026 00:30:50 -0700 Subject: [PATCH] Bind session reuse to active provider session - Require an active provider session before treating thread.session as reusable - Add regression coverage for starting a fresh session when only projected state exists --- .../Layers/ProviderCommandReactor.test.ts | 55 +++++++++++++++++++ .../Layers/ProviderCommandReactor.ts | 4 +- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts index ded64fb9e6..fb4d13f071 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts @@ -1241,6 +1241,61 @@ describe("ProviderCommandReactor", () => { }); }); + it("starts a fresh session when only projected session state exists", async () => { + const harness = await createHarness(); + const now = new Date().toISOString(); + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.session.set", + commandId: CommandId.make("cmd-session-set-stale"), + threadId: ThreadId.make("thread-1"), + session: { + threadId: ThreadId.make("thread-1"), + status: "ready", + providerName: "codex", + runtimeMode: "approval-required", + activeTurnId: null, + lastError: null, + updatedAt: now, + }, + createdAt: now, + }), + ); + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.make("cmd-turn-start-stale"), + threadId: ThreadId.make("thread-1"), + message: { + messageId: asMessageId("user-message-stale"), + role: "user", + text: "resume codex", + attachments: [], + }, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt: now, + }), + ); + + await waitFor(() => harness.startSession.mock.calls.length === 1); + await waitFor(() => harness.sendTurn.mock.calls.length === 1); + + expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({ + threadId: ThreadId.make("thread-1"), + modelSelection: { + provider: "codex", + model: "gpt-5-codex", + }, + runtimeMode: "approval-required", + }); + expect(harness.sendTurn.mock.calls[0]?.[0]).toMatchObject({ + threadId: ThreadId.make("thread-1"), + }); + }); + it("reacts to thread.approval.respond by forwarding provider approval response", async () => { const harness = await createHarness(); const now = new Date().toISOString(); diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts index a1a69f0efa..a728129fa8 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts @@ -290,14 +290,14 @@ const make = Effect.gen(function* () { createdAt, }); + const activeSession = yield* resolveActiveSession(threadId); const existingSessionThreadId = - thread.session && thread.session.status !== "stopped" ? thread.id : null; + thread.session && thread.session.status !== "stopped" && activeSession ? thread.id : null; if (existingSessionThreadId) { const runtimeModeChanged = thread.runtimeMode !== thread.session?.runtimeMode; const providerChanged = requestedModelSelection !== undefined && requestedModelSelection.provider !== currentProvider; - const activeSession = yield* resolveActiveSession(existingSessionThreadId); const sessionModelSwitch = currentProvider === undefined ? "in-session"