authorgravatar for ashvin.nihalani@gmail.comAshvin Nihalani <ashvin.nihalani@gmail.com> 2026-04-13 13:34:02-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-13 13:34:02-07:00
logd18e43b6747887a5c4bdf55a04f0b3f2f71ead83
tree2524d9def3c37d9d2958f7e9516f50bc273be4bc
parent6f699346e4a4f43059c6d2e459532abb80df1338
signature Signed by PGP key B5690EEEBB952194

fix: lost provider session recovery (#1938)


2 files changed, 57 insertions(+), 2 deletions(-)

apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+55
...@@ -1241,6 +1241,61 @@ describe("ProviderCommandReactor", () => {...@@ -1241,6 +1241,61 @@ describe("ProviderCommandReactor", () => {
1241 });1241 });
1242 });1242 });
12431243
1244 it("starts a fresh session when only projected session state exists", async () => {
1245 const harness = await createHarness();
1246 const now = new Date().toISOString();
1247
1248 await Effect.runPromise(
1249 harness.engine.dispatch({
1250 type: "thread.session.set",
1251 commandId: CommandId.make("cmd-session-set-stale"),
1252 threadId: ThreadId.make("thread-1"),
1253 session: {
1254 threadId: ThreadId.make("thread-1"),
1255 status: "ready",
1256 providerName: "codex",
1257 runtimeMode: "approval-required",
1258 activeTurnId: null,
1259 lastError: null,
1260 updatedAt: now,
1261 },
1262 createdAt: now,
1263 }),
1264 );
1265
1266 await Effect.runPromise(
1267 harness.engine.dispatch({
1268 type: "thread.turn.start",
1269 commandId: CommandId.make("cmd-turn-start-stale"),
1270 threadId: ThreadId.make("thread-1"),
1271 message: {
1272 messageId: asMessageId("user-message-stale"),
1273 role: "user",
1274 text: "resume codex",
1275 attachments: [],
1276 },
1277 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
1278 runtimeMode: "approval-required",
1279 createdAt: now,
1280 }),
1281 );
1282
1283 await waitFor(() => harness.startSession.mock.calls.length === 1);
1284 await waitFor(() => harness.sendTurn.mock.calls.length === 1);
1285
1286 expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({
1287 threadId: ThreadId.make("thread-1"),
1288 modelSelection: {
1289 provider: "codex",
1290 model: "gpt-5-codex",
1291 },
1292 runtimeMode: "approval-required",
1293 });
1294 expect(harness.sendTurn.mock.calls[0]?.[0]).toMatchObject({
1295 threadId: ThreadId.make("thread-1"),
1296 });
1297 });
1298
1244 it("reacts to thread.approval.respond by forwarding provider approval response", async () => {1299 it("reacts to thread.approval.respond by forwarding provider approval response", async () => {
1245 const harness = await createHarness();1300 const harness = await createHarness();
1246 const now = new Date().toISOString();1301 const now = new Date().toISOString();
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+2-2
...@@ -285,14 +285,14 @@ const make = Effect.gen(function* () {...@@ -285,14 +285,14 @@ const make = Effect.gen(function* () {
285 createdAt,285 createdAt,
286 });286 });
287287
288 const activeSession = yield* resolveActiveSession(threadId);
288 const existingSessionThreadId =289 const existingSessionThreadId =
289 thread.session && thread.session.status !== "stopped" ? thread.id : null;290 thread.session && thread.session.status !== "stopped" && activeSession ? thread.id : null;
290 if (existingSessionThreadId) {291 if (existingSessionThreadId) {
291 const runtimeModeChanged = thread.runtimeMode !== thread.session?.runtimeMode;292 const runtimeModeChanged = thread.runtimeMode !== thread.session?.runtimeMode;
292 const providerChanged =293 const providerChanged =
293 requestedModelSelection !== undefined &&294 requestedModelSelection !== undefined &&
294 requestedModelSelection.provider !== currentProvider;295 requestedModelSelection.provider !== currentProvider;
295 const activeSession = yield* resolveActiveSession(existingSessionThreadId);
296 const sessionModelSwitch =296 const sessionModelSwitch =
297 currentProvider === undefined297 currentProvider === undefined
298 ? "in-session"298 ? "in-session"