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", () => {
12411241 });
12421242 });
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
12441299 it("reacts to thread.approval.respond by forwarding provider approval response", async () => {
12451300 const harness = await createHarness();
12461301 const now = new Date().toISOString();
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+2-2
......@@ -285,14 +285,14 @@ const make = Effect.gen(function* () {
285285 createdAt,
286286 });
287287
288 const activeSession = yield* resolveActiveSession(threadId);
288289 const existingSessionThreadId =
289 thread.session && thread.session.status !== "stopped" ? thread.id : null;
290 thread.session && thread.session.status !== "stopped" && activeSession ? thread.id : null;
290291 if (existingSessionThreadId) {
291292 const runtimeModeChanged = thread.runtimeMode !== thread.session?.runtimeMode;
292293 const providerChanged =
293294 requestedModelSelection !== undefined &&
294295 requestedModelSelection.provider !== currentProvider;
295 const activeSession = yield* resolveActiveSession(existingSessionThreadId);
296296 const sessionModelSwitch =
297297 currentProvider === undefined
298298 ? "in-session"