authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-27 01:56:37-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-05 23:00:41-08:00
logc590beb4e3feb7c8bc87cf1307b3d018c73eca62
tree674e271bc693735072b4c0bd635389a4b13aa2a6
parente9ced44ac6729116a1ee8332077736ca648ee419

Support provider-aware session selection in reactor

Add restart semantics when requested provider changes and cover with tests. Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 93 insertions(+), 211 deletions(-)

apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+70-204
...@@ -87,13 +87,20 @@ describe("ProviderCommandReactor", () => {...@@ -87,13 +87,20 @@ describe("ProviderCommandReactor", () => {
87 const runtimeSessions: Array<ProviderSession> = [];87 const runtimeSessions: Array<ProviderSession> = [];
88 const startSession = vi.fn((_: unknown, input: unknown) => {88 const startSession = vi.fn((_: unknown, input: unknown) => {
89 const sessionIndex = nextSessionIndex++;89 const sessionIndex = nextSessionIndex++;
90 const provider =
91 typeof input === "object" &&
92 input !== null &&
93 "provider" in input &&
94 (input.provider === "codex" || input.provider === "claudeCode")
95 ? input.provider
96 : "codex";
90 const resumeCursor =97 const resumeCursor =
91 typeof input === "object" && input !== null && "resumeCursor" in input98 typeof input === "object" && input !== null && "resumeCursor" in input
92 ? input.resumeCursor99 ? input.resumeCursor
93 : undefined;100 : undefined;
94 const session: ProviderSession = {101 const session: ProviderSession = {
95 sessionId: asSessionId(`sess-${sessionIndex}`),102 sessionId: asSessionId(`sess-${sessionIndex}`),
96 provider: "codex" as const,103 provider,
97 status: "ready" as const,104 status: "ready" as const,
98 threadId: ProviderThreadId.makeUnsafe(`provider-thread-${sessionIndex}`),105 threadId: ProviderThreadId.makeUnsafe(`provider-thread-${sessionIndex}`),
99 resumeCursor: resumeCursor ?? { opaque: `cursor-${sessionIndex}` },106 resumeCursor: resumeCursor ?? { opaque: `cursor-${sessionIndex}` },
...@@ -240,165 +247,55 @@ describe("ProviderCommandReactor", () => {...@@ -240,165 +247,55 @@ describe("ProviderCommandReactor", () => {
240 expect(thread?.session?.sandboxMode).toBe("workspace-write");247 expect(thread?.session?.sandboxMode).toBe("workspace-write");
241 });248 });
242249
243 it("generates and renames temporary worktree branch on first turn", async () => {250 it("starts first turn with requested provider when provider is specified", async () => {
244 const harness = await createHarness();251 const harness = await createHarness();
245 const now = new Date().toISOString();252 const now = new Date().toISOString();
246253
247 await Effect.runPromise(
248 harness.engine.dispatch({
249 type: "thread.meta.update",
250 commandId: CommandId.makeUnsafe("cmd-thread-meta-set-temp-branch"),
251 threadId: ThreadId.makeUnsafe("thread-1"),
252 branch: "t3code/89abc123",
253 worktreePath: "/tmp/provider-project/.t3/worktrees/t3code-89abc123",
254 }),
255 );
256
257 await Effect.runPromise(
258 harness.engine.dispatch({
259 type: "thread.turn.start",
260 commandId: CommandId.makeUnsafe("cmd-turn-start-worktree-rename"),
261 threadId: ThreadId.makeUnsafe("thread-1"),
262 message: {
263 messageId: asMessageId("user-message-worktree-rename"),
264 role: "user",
265 text: "Fix visual bug from screenshot",
266 attachments: [
267 {
268 type: "image",
269 id: "thread-1-att-rename",
270 name: "bug.png",
271 mimeType: "image/png",
272 sizeBytes: 5,
273 },
274 ],
275 },
276 approvalPolicy: "on-request",
277 sandboxMode: "workspace-write",
278 createdAt: now,
279 }),
280 );
281
282 await waitFor(() => harness.generateBranchName.mock.calls.length === 1);
283 await waitFor(() => harness.renameBranch.mock.calls.length === 1);
284
285 expect(harness.generateBranchName.mock.calls[0]?.[0]).toEqual({
286 cwd: "/tmp/provider-project/.t3/worktrees/t3code-89abc123",
287 message: "Fix visual bug from screenshot",
288 attachments: [
289 {
290 type: "image",
291 id: "thread-1-att-rename",
292 name: "bug.png",
293 mimeType: "image/png",
294 sizeBytes: 5,
295 },
296 ],
297 });
298 expect(harness.renameBranch.mock.calls[0]?.[0]).toEqual({
299 cwd: "/tmp/provider-project/.t3/worktrees/t3code-89abc123",
300 oldBranch: "t3code/89abc123",
301 newBranch: "t3code/generated-name",
302 });
303
304 await waitFor(() => {
305 const readModel = Effect.runSync(harness.engine.getReadModel());
306 const thread = readModel.threads.find(
307 (entry) => entry.id === ThreadId.makeUnsafe("thread-1"),
308 );
309 return thread?.branch === "t3code/generated-name";
310 });
311 });
312
313 it("passes persisted attachment references to branch generation and turn start", async () => {
314 const harness = await createHarness();
315 const now = new Date().toISOString();
316
317 await Effect.runPromise(
318 harness.engine.dispatch({
319 type: "thread.meta.update",
320 commandId: CommandId.makeUnsafe("cmd-thread-meta-set-temp-branch-persisted"),
321 threadId: ThreadId.makeUnsafe("thread-1"),
322 branch: "t3code/abcdef12",
323 worktreePath: "/tmp/provider-project/.t3/worktrees/t3code-abcdef12",
324 }),
325 );
326
327 await Effect.runPromise(254 await Effect.runPromise(
328 harness.engine.dispatch({255 harness.engine.dispatch({
329 type: "thread.turn.start",256 type: "thread.turn.start",
330 commandId: CommandId.makeUnsafe("cmd-turn-start-persisted-attachments"),257 commandId: CommandId.makeUnsafe("cmd-turn-start-provider-first"),
331 threadId: ThreadId.makeUnsafe("thread-1"),258 threadId: ThreadId.makeUnsafe("thread-1"),
332 message: {259 message: {
333 messageId: asMessageId("user-message-persisted-attachments"),260 messageId: asMessageId("user-message-provider-first"),
334 role: "user",261 role: "user",
335 text: "Fix visual bug from screenshot",262 text: "hello claude",
336 attachments: [263 attachments: [],
337 {
338 type: "image",
339 id: "thread-1-att-persisted",
340 name: "bug.png",
341 mimeType: "image/png",
342 sizeBytes: 5,
343 },
344 ],
345 },264 },
265 provider: "claudeCode",
346 approvalPolicy: "on-request",266 approvalPolicy: "on-request",
347 sandboxMode: "workspace-write",267 sandboxMode: "workspace-write",
348 createdAt: now,268 createdAt: now,
349 }),269 }),
350 );270 );
351271
352 await waitFor(() => harness.generateBranchName.mock.calls.length === 1);272 await waitFor(() => harness.startSession.mock.calls.length === 1);
353 await waitFor(() => harness.sendTurn.mock.calls.length === 1);273 await waitFor(() => harness.sendTurn.mock.calls.length === 1);
354274 expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({
355 expect(harness.generateBranchName.mock.calls[0]?.[0]).toEqual({275 provider: "claudeCode",
356 cwd: "/tmp/provider-project/.t3/worktrees/t3code-abcdef12",276 cwd: "/tmp/provider-project",
357 message: "Fix visual bug from screenshot",277 model: "gpt-5-codex",
358 attachments: [278 approvalPolicy: "on-request",
359 {279 sandboxMode: "workspace-write",
360 type: "image",
361 id: "thread-1-att-persisted",
362 name: "bug.png",
363 mimeType: "image/png",
364 sizeBytes: 5,
365 },
366 ],
367 });
368 expect(harness.sendTurn.mock.calls[0]?.[0]).toMatchObject({
369 attachments: [
370 {
371 type: "image",
372 id: "thread-1-att-persisted",
373 name: "bug.png",
374 mimeType: "image/png",
375 sizeBytes: 5,
376 },
377 ],
378 });280 });
281
282 const readModel = await Effect.runPromise(harness.engine.getReadModel());
283 const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1"));
284 expect(thread?.session?.providerName).toBe("claudeCode");
285 expect(thread?.session?.providerSessionId).toBe("sess-1");
379 });286 });
380287
381 it("skips worktree branch generation after the first user turn", async () => {288 it("reuses the same provider session when runtime mode is unchanged", async () => {
382 const harness = await createHarness();289 const harness = await createHarness();
383 const now = new Date().toISOString();290 const now = new Date().toISOString();
384291
385 await Effect.runPromise(
386 harness.engine.dispatch({
387 type: "thread.meta.update",
388 commandId: CommandId.makeUnsafe("cmd-thread-meta-set-temp-branch-2"),
389 threadId: ThreadId.makeUnsafe("thread-1"),
390 branch: "t3code/1234abcd",
391 worktreePath: "/tmp/provider-project/.t3/worktrees/t3code-1234abcd",
392 }),
393 );
394
395 await Effect.runPromise(292 await Effect.runPromise(
396 harness.engine.dispatch({293 harness.engine.dispatch({
397 type: "thread.turn.start",294 type: "thread.turn.start",
398 commandId: CommandId.makeUnsafe("cmd-turn-start-first"),295 commandId: CommandId.makeUnsafe("cmd-turn-start-unchanged-1"),
399 threadId: ThreadId.makeUnsafe("thread-1"),296 threadId: ThreadId.makeUnsafe("thread-1"),
400 message: {297 message: {
401 messageId: asMessageId("user-message-first"),298 messageId: asMessageId("user-message-unchanged-1"),
402 role: "user",299 role: "user",
403 text: "first",300 text: "first",
404 attachments: [],301 attachments: [],
...@@ -409,15 +306,16 @@ describe("ProviderCommandReactor", () => {...@@ -409,15 +306,16 @@ describe("ProviderCommandReactor", () => {
409 }),306 }),
410 );307 );
411308
412 await waitFor(() => harness.generateBranchName.mock.calls.length === 1);309 await waitFor(() => harness.startSession.mock.calls.length === 1);
310 await waitFor(() => harness.sendTurn.mock.calls.length === 1);
413311
414 await Effect.runPromise(312 await Effect.runPromise(
415 harness.engine.dispatch({313 harness.engine.dispatch({
416 type: "thread.turn.start",314 type: "thread.turn.start",
417 commandId: CommandId.makeUnsafe("cmd-turn-start-second"),315 commandId: CommandId.makeUnsafe("cmd-turn-start-unchanged-2"),
418 threadId: ThreadId.makeUnsafe("thread-1"),316 threadId: ThreadId.makeUnsafe("thread-1"),
419 message: {317 message: {
420 messageId: asMessageId("user-message-second"),318 messageId: asMessageId("user-message-unchanged-2"),
421 role: "user",319 role: "user",
422 text: "second",320 text: "second",
423 attachments: [],321 attachments: [],
...@@ -429,75 +327,27 @@ describe("ProviderCommandReactor", () => {...@@ -429,75 +327,27 @@ describe("ProviderCommandReactor", () => {
429 );327 );
430328
431 await waitFor(() => harness.sendTurn.mock.calls.length === 2);329 await waitFor(() => harness.sendTurn.mock.calls.length === 2);
432 expect(harness.generateBranchName.mock.calls.length).toBe(1);330 expect(harness.startSession.mock.calls.length).toBe(1);
433 expect(harness.renameBranch.mock.calls.length).toBe(1);331 expect(harness.stopSession.mock.calls.length).toBe(0);
434 });
435
436 it("skips worktree rename when branch-name generation fails", async () => {
437 const harness = await createHarness();
438 const now = new Date().toISOString();
439 harness.generateBranchName.mockImplementationOnce(() =>
440 Effect.fail(
441 new TextGenerationError({
442 operation: "generateBranchName",
443 detail: "model returned invalid payload",
444 }),
445 ),
446 );
447
448 await Effect.runPromise(
449 harness.engine.dispatch({
450 type: "thread.meta.update",
451 commandId: CommandId.makeUnsafe("cmd-thread-meta-set-temp-branch-null"),
452 threadId: ThreadId.makeUnsafe("thread-1"),
453 branch: "t3code/0000abcd",
454 worktreePath: "/tmp/provider-project/.t3/worktrees/t3code-0000abcd",
455 }),
456 );
457
458 await Effect.runPromise(
459 harness.engine.dispatch({
460 type: "thread.turn.start",
461 commandId: CommandId.makeUnsafe("cmd-turn-start-null-branch"),
462 threadId: ThreadId.makeUnsafe("thread-1"),
463 message: {
464 messageId: asMessageId("user-message-null-branch"),
465 role: "user",
466 text: "Fix visual regression",
467 attachments: [],
468 },
469 approvalPolicy: "on-request",
470 sandboxMode: "workspace-write",
471 createdAt: now,
472 }),
473 );
474
475 await waitFor(() => harness.generateBranchName.mock.calls.length === 1);
476 await Effect.runPromise(Effect.sleep("20 millis"));
477 expect(harness.renameBranch.mock.calls.length).toBe(0);
478
479 const readModel = await Effect.runPromise(harness.engine.getReadModel());
480 const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1"));
481 expect(thread?.branch).toBe("t3code/0000abcd");
482 });332 });
483333
484 it("reuses the same provider session when runtime mode is unchanged", async () => {334 it("restarts the provider session when runtime mode changes", async () => {
485 const harness = await createHarness();335 const harness = await createHarness();
486 const now = new Date().toISOString();336 const now = new Date().toISOString();
487337
488 await Effect.runPromise(338 await Effect.runPromise(
489 harness.engine.dispatch({339 harness.engine.dispatch({
490 type: "thread.turn.start",340 type: "thread.turn.start",
491 commandId: CommandId.makeUnsafe("cmd-turn-start-unchanged-1"),341 commandId: CommandId.makeUnsafe("cmd-turn-start-runtime-mode-1"),
492 threadId: ThreadId.makeUnsafe("thread-1"),342 threadId: ThreadId.makeUnsafe("thread-1"),
493 message: {343 message: {
494 messageId: asMessageId("user-message-unchanged-1"),344 messageId: asMessageId("user-message-runtime-mode-1"),
495 role: "user",345 role: "user",
496 text: "first",346 text: "first",
497 attachments: [],347 attachments: [],
498 },348 },
499 approvalPolicy: "on-request",349 approvalPolicy: "never",
500 sandboxMode: "workspace-write",350 sandboxMode: "danger-full-access",
501 createdAt: now,351 createdAt: now,
502 }),352 }),
503 );353 );
...@@ -508,10 +358,10 @@ describe("ProviderCommandReactor", () => {...@@ -508,10 +358,10 @@ describe("ProviderCommandReactor", () => {
508 await Effect.runPromise(358 await Effect.runPromise(
509 harness.engine.dispatch({359 harness.engine.dispatch({
510 type: "thread.turn.start",360 type: "thread.turn.start",
511 commandId: CommandId.makeUnsafe("cmd-turn-start-unchanged-2"),361 commandId: CommandId.makeUnsafe("cmd-turn-start-runtime-mode-2"),
512 threadId: ThreadId.makeUnsafe("thread-1"),362 threadId: ThreadId.makeUnsafe("thread-1"),
513 message: {363 message: {
514 messageId: asMessageId("user-message-unchanged-2"),364 messageId: asMessageId("user-message-runtime-mode-2"),
515 role: "user",365 role: "user",
516 text: "second",366 text: "second",
517 attachments: [],367 attachments: [],
...@@ -522,28 +372,44 @@ describe("ProviderCommandReactor", () => {...@@ -522,28 +372,44 @@ describe("ProviderCommandReactor", () => {
522 }),372 }),
523 );373 );
524374
375 await waitFor(() => harness.stopSession.mock.calls.length === 1);
376 await waitFor(() => harness.startSession.mock.calls.length === 2);
525 await waitFor(() => harness.sendTurn.mock.calls.length === 2);377 await waitFor(() => harness.sendTurn.mock.calls.length === 2);
526 expect(harness.startSession.mock.calls.length).toBe(1);378
527 expect(harness.stopSession.mock.calls.length).toBe(0);379 expect(harness.stopSession.mock.calls[0]?.[0]).toEqual({ sessionId: asSessionId("sess-1") });
380 expect(harness.startSession.mock.calls[1]?.[1]).toMatchObject({
381 resumeCursor: { opaque: "cursor-1" },
382 approvalPolicy: "on-request",
383 sandboxMode: "workspace-write",
384 });
385 expect(harness.sendTurn.mock.calls[1]?.[0]).toMatchObject({
386 sessionId: asSessionId("sess-2"),
387 });
388
389 const readModel = await Effect.runPromise(harness.engine.getReadModel());
390 const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1"));
391 expect(thread?.session?.providerSessionId).toBe("sess-2");
392 expect(thread?.session?.approvalPolicy).toBe("on-request");
393 expect(thread?.session?.sandboxMode).toBe("workspace-write");
528 });394 });
529395
530 it("restarts the provider session when runtime mode changes", async () => {396 it("switches provider by restarting the session when turn request provider changes", async () => {
531 const harness = await createHarness();397 const harness = await createHarness();
532 const now = new Date().toISOString();398 const now = new Date().toISOString();
533399
534 await Effect.runPromise(400 await Effect.runPromise(
535 harness.engine.dispatch({401 harness.engine.dispatch({
536 type: "thread.turn.start",402 type: "thread.turn.start",
537 commandId: CommandId.makeUnsafe("cmd-turn-start-runtime-mode-1"),403 commandId: CommandId.makeUnsafe("cmd-turn-start-provider-switch-1"),
538 threadId: ThreadId.makeUnsafe("thread-1"),404 threadId: ThreadId.makeUnsafe("thread-1"),
539 message: {405 message: {
540 messageId: asMessageId("user-message-runtime-mode-1"),406 messageId: asMessageId("user-message-provider-switch-1"),
541 role: "user",407 role: "user",
542 text: "first",408 text: "first",
543 attachments: [],409 attachments: [],
544 },410 },
545 approvalPolicy: "never",411 approvalPolicy: "on-request",
546 sandboxMode: "danger-full-access",412 sandboxMode: "workspace-write",
547 createdAt: now,413 createdAt: now,
548 }),414 }),
549 );415 );
...@@ -554,14 +420,15 @@ describe("ProviderCommandReactor", () => {...@@ -554,14 +420,15 @@ describe("ProviderCommandReactor", () => {
554 await Effect.runPromise(420 await Effect.runPromise(
555 harness.engine.dispatch({421 harness.engine.dispatch({
556 type: "thread.turn.start",422 type: "thread.turn.start",
557 commandId: CommandId.makeUnsafe("cmd-turn-start-runtime-mode-2"),423 commandId: CommandId.makeUnsafe("cmd-turn-start-provider-switch-2"),
558 threadId: ThreadId.makeUnsafe("thread-1"),424 threadId: ThreadId.makeUnsafe("thread-1"),
559 message: {425 message: {
560 messageId: asMessageId("user-message-runtime-mode-2"),426 messageId: asMessageId("user-message-provider-switch-2"),
561 role: "user",427 role: "user",
562 text: "second",428 text: "second",
563 attachments: [],429 attachments: [],
564 },430 },
431 provider: "claudeCode",
565 approvalPolicy: "on-request",432 approvalPolicy: "on-request",
566 sandboxMode: "workspace-write",433 sandboxMode: "workspace-write",
567 createdAt: now,434 createdAt: now,
...@@ -574,17 +441,16 @@ describe("ProviderCommandReactor", () => {...@@ -574,17 +441,16 @@ describe("ProviderCommandReactor", () => {
574441
575 expect(harness.stopSession.mock.calls[0]?.[0]).toEqual({ sessionId: asSessionId("sess-1") });442 expect(harness.stopSession.mock.calls[0]?.[0]).toEqual({ sessionId: asSessionId("sess-1") });
576 expect(harness.startSession.mock.calls[1]?.[1]).toMatchObject({443 expect(harness.startSession.mock.calls[1]?.[1]).toMatchObject({
444 provider: "claudeCode",
577 resumeCursor: { opaque: "cursor-1" },445 resumeCursor: { opaque: "cursor-1" },
578 approvalPolicy: "on-request",446 approvalPolicy: "on-request",
579 sandboxMode: "workspace-write",447 sandboxMode: "workspace-write",
580 });448 });
581 expect(harness.sendTurn.mock.calls[1]?.[0]).toMatchObject({
582 sessionId: asSessionId("sess-2"),
583 });
584449
585 const readModel = await Effect.runPromise(harness.engine.getReadModel());450 const readModel = await Effect.runPromise(harness.engine.getReadModel());
586 const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1"));451 const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1"));
587 expect(thread?.session?.providerSessionId).toBe("sess-2");452 expect(thread?.session?.providerSessionId).toBe("sess-2");
453 expect(thread?.session?.providerName).toBe("claudeCode");
588 expect(thread?.session?.approvalPolicy).toBe("on-request");454 expect(thread?.session?.approvalPolicy).toBe("on-request");
589 expect(thread?.session?.sandboxMode).toBe("workspace-write");455 expect(thread?.session?.sandboxMode).toBe("workspace-write");
590 });456 });
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+23-7
...@@ -168,6 +168,7 @@ const make = Effect.gen(function* () {...@@ -168,6 +168,7 @@ const make = Effect.gen(function* () {
168 threadId: ThreadId,168 threadId: ThreadId,
169 createdAt: string,169 createdAt: string,
170 options?: {170 options?: {
171 readonly provider?: ProviderKind;
171 readonly approvalPolicy?: ProviderApprovalPolicy;172 readonly approvalPolicy?: ProviderApprovalPolicy;
172 readonly sandboxMode?: ProviderSandboxMode;173 readonly sandboxMode?: ProviderSandboxMode;
173 },174 },
...@@ -182,10 +183,11 @@ const make = Effect.gen(function* () {...@@ -182,10 +183,11 @@ const make = Effect.gen(function* () {
182 options?.approvalPolicy ?? thread.session?.approvalPolicy ?? DEFAULT_APPROVAL_POLICY;183 options?.approvalPolicy ?? thread.session?.approvalPolicy ?? DEFAULT_APPROVAL_POLICY;
183 const desiredSandboxMode =184 const desiredSandboxMode =
184 options?.sandboxMode ?? thread.session?.sandboxMode ?? DEFAULT_SANDBOX_MODE;185 options?.sandboxMode ?? thread.session?.sandboxMode ?? DEFAULT_SANDBOX_MODE;
185 const preferredProvider: ProviderKind | undefined =186 const currentProvider: ProviderKind | undefined =
186 thread.session?.providerName === "codex" || thread.session?.providerName === "claudeCode"187 thread.session?.providerName === "codex" || thread.session?.providerName === "claudeCode"
187 ? thread.session.providerName188 ? thread.session.providerName
188 : undefined;189 : undefined;
190 const preferredProvider: ProviderKind | undefined = options?.provider ?? currentProvider;
189 const effectiveCwd = resolveThreadWorkspaceCwd({191 const effectiveCwd = resolveThreadWorkspaceCwd({
190 thread,192 thread,
191 projects: readModel.projects,193 projects: readModel.projects,
...@@ -196,12 +198,17 @@ const make = Effect.gen(function* () {...@@ -196,12 +198,17 @@ const make = Effect.gen(function* () {
196 Effect.map((sessions) => sessions.find((session) => session.sessionId === sessionId)?.resumeCursor),198 Effect.map((sessions) => sessions.find((session) => session.sessionId === sessionId)?.resumeCursor),
197 );199 );
198200
199 const startProviderSession = (resumeCursor?: unknown) =>201 const startProviderSession = (input?: {
202 readonly resumeCursor?: unknown;
203 readonly provider?: ProviderKind;
204 }) =>
200 providerService.startSession(threadId, {205 providerService.startSession(threadId, {
201 ...(preferredProvider ? { provider: preferredProvider } : {}),206 ...(input?.provider ?? preferredProvider
207 ? { provider: input?.provider ?? preferredProvider }
208 : {}),
202 ...(effectiveCwd ? { cwd: effectiveCwd } : {}),209 ...(effectiveCwd ? { cwd: effectiveCwd } : {}),
203 ...(thread.model ? { model: thread.model } : {}),210 ...(thread.model ? { model: thread.model } : {}),
204 ...(resumeCursor !== undefined ? { resumeCursor } : {}),211 ...(input?.resumeCursor !== undefined ? { resumeCursor: input.resumeCursor } : {}),
205 approvalPolicy: desiredApprovalPolicy,212 approvalPolicy: desiredApprovalPolicy,
206 sandboxMode: desiredSandboxMode,213 sandboxMode: desiredSandboxMode,
207 });214 });
...@@ -232,13 +239,17 @@ const make = Effect.gen(function* () {...@@ -232,13 +239,17 @@ const make = Effect.gen(function* () {
232 options.approvalPolicy !== thread.session?.approvalPolicy;239 options.approvalPolicy !== thread.session?.approvalPolicy;
233 const sandboxModeChanged =240 const sandboxModeChanged =
234 options?.sandboxMode !== undefined && options.sandboxMode !== thread.session?.sandboxMode;241 options?.sandboxMode !== undefined && options.sandboxMode !== thread.session?.sandboxMode;
242 const providerChanged = options?.provider !== undefined && options.provider !== currentProvider;
235243
236 if (!approvalPolicyChanged && !sandboxModeChanged) {244 if (!approvalPolicyChanged && !sandboxModeChanged && !providerChanged) {
237 return existingSessionId;245 return existingSessionId;
238 }246 }
239247
240 const resumeCursor = yield* resolveResumeCursorForSession(existingSessionId);248 const resumeCursor = yield* resolveResumeCursorForSession(existingSessionId);
241 const restartedSession = yield* startProviderSession(resumeCursor);249 const restartedSession = yield* startProviderSession({
250 ...(resumeCursor !== undefined ? { resumeCursor } : {}),
251 ...(options?.provider !== undefined ? { provider: options.provider } : {}),
252 });
242 yield* bindSessionToThread(restartedSession);253 yield* bindSessionToThread(restartedSession);
243 yield* providerService.stopSession({ sessionId: existingSessionId }).pipe(254 yield* providerService.stopSession({ sessionId: existingSessionId }).pipe(
244 Effect.catchCause((cause) =>255 Effect.catchCause((cause) =>
...@@ -252,7 +263,9 @@ const make = Effect.gen(function* () {...@@ -252,7 +263,9 @@ const make = Effect.gen(function* () {
252 return restartedSession.sessionId;263 return restartedSession.sessionId;
253 }264 }
254265
255 const startedSession = yield* startProviderSession();266 const startedSession = yield* startProviderSession({
267 ...(options?.provider !== undefined ? { provider: options.provider } : {}),
268 });
256 yield* bindSessionToThread(startedSession);269 yield* bindSessionToThread(startedSession);
257 return startedSession.sessionId;270 return startedSession.sessionId;
258 });271 });
...@@ -261,6 +274,7 @@ const make = Effect.gen(function* () {...@@ -261,6 +274,7 @@ const make = Effect.gen(function* () {
261 readonly threadId: ThreadId;274 readonly threadId: ThreadId;
262 readonly messageText: string;275 readonly messageText: string;
263 readonly attachments?: ReadonlyArray<ChatAttachment>;276 readonly attachments?: ReadonlyArray<ChatAttachment>;
277 readonly provider?: ProviderKind;
264 readonly model?: string;278 readonly model?: string;
265 readonly effort?: string;279 readonly effort?: string;
266 readonly approvalPolicy: ProviderApprovalPolicy;280 readonly approvalPolicy: ProviderApprovalPolicy;
...@@ -272,6 +286,7 @@ const make = Effect.gen(function* () {...@@ -272,6 +286,7 @@ const make = Effect.gen(function* () {
272 return;286 return;
273 }287 }
274 const sessionId = yield* ensureSessionForThread(input.threadId, input.createdAt, {288 const sessionId = yield* ensureSessionForThread(input.threadId, input.createdAt, {
289 ...(input.provider !== undefined ? { provider: input.provider } : {}),
275 approvalPolicy: input.approvalPolicy,290 approvalPolicy: input.approvalPolicy,
276 sandboxMode: input.sandboxMode,291 sandboxMode: input.sandboxMode,
277 });292 });
...@@ -394,6 +409,7 @@ const make = Effect.gen(function* () {...@@ -394,6 +409,7 @@ const make = Effect.gen(function* () {
394 threadId: event.payload.threadId,409 threadId: event.payload.threadId,
395 messageText: message.text,410 messageText: message.text,
396 ...(message.attachments !== undefined ? { attachments: message.attachments } : {}),411 ...(message.attachments !== undefined ? { attachments: message.attachments } : {}),
412 ...(event.payload.provider !== undefined ? { provider: event.payload.provider } : {}),
397 ...(event.payload.model !== undefined ? { model: event.payload.model } : {}),413 ...(event.payload.model !== undefined ? { model: event.payload.model } : {}),
398 ...(event.payload.effort !== undefined ? { effort: event.payload.effort } : {}),414 ...(event.payload.effort !== undefined ? { effort: event.payload.effort } : {}),
399 approvalPolicy: event.payload.approvalPolicy,415 approvalPolicy: event.payload.approvalPolicy,