Unified · Split
| @@ -138,6 +138,12 @@ describe("ProviderCommandReactor", () => { · 137 unmodified lines | ||
| 138 | 138 | (input.runtimeMode === "approval-required" || input.runtimeMode === "full-access") |
| 139 | 139 | ? input.runtimeMode |
| 140 | 140 | : "full-access", |
| 141 | + ...(typeof input === "object" && | |
| 142 | + input !== null && | |
| 143 | + "cwd" in input && | |
| 144 | + typeof input.cwd === "string" | |
| 145 | + ? { cwd: input.cwd } | |
| 146 | + : {}), | |
| 141 | 147 | ...(modelSelection.model !== undefined ? { model: modelSelection.model } : {}), |
| 142 | 148 | threadId, |
| 143 | 149 | resumeCursor: resumeCursor ?? { opaque: `resume-${sessionIndex}` }, |
| @@ -864,6 +870,76 @@ describe("ProviderCommandReactor", () => { · 720 unmodified lines | ||
| 864 | 870 | expect(harness.stopSession.mock.calls.length).toBe(0); |
| 865 | 871 | }); |
| 866 | 872 | |
| 873 | + it("restarts the provider session when the thread workspace changes", async () => { | |
| 874 | + const harness = await createHarness({ | |
| 875 | + threadModelSelection: { provider: "claudeAgent", model: "claude-sonnet-4-6" }, | |
| 876 | + }); | |
| 877 | + const now = new Date().toISOString(); | |
| 878 | + | |
| 879 | + await Effect.runPromise( | |
| 880 | + harness.engine.dispatch({ | |
| 881 | + type: "thread.turn.start", | |
| 882 | + commandId: CommandId.make("cmd-turn-start-workspace-1"), | |
| 883 | + threadId: ThreadId.make("thread-1"), | |
| 884 | + message: { | |
| 885 | + messageId: asMessageId("user-message-workspace-1"), | |
| 886 | + role: "user", | |
| 887 | + text: "first in project root", | |
| 888 | + attachments: [], | |
| 889 | + }, | |
| 890 | + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, | |
| 891 | + runtimeMode: "approval-required", | |
| 892 | + createdAt: now, | |
| 893 | + }), | |
| 894 | + ); | |
| 895 | + | |
| 896 | + await waitFor(() => harness.startSession.mock.calls.length === 1); | |
| 897 | + await waitFor(() => harness.sendTurn.mock.calls.length === 1); | |
| 898 | + expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({ | |
| 899 | + cwd: "/tmp/provider-project", | |
| 900 | + }); | |
| 901 | + | |
| 902 | + await Effect.runPromise( | |
| 903 | + harness.engine.dispatch({ | |
| 904 | + type: "thread.meta.update", | |
| 905 | + commandId: CommandId.make("cmd-thread-worktree-change"), | |
| 906 | + threadId: ThreadId.make("thread-1"), | |
| 907 | + worktreePath: "/tmp/provider-project-worktree", | |
| 908 | + }), | |
| 909 | + ); | |
| 910 | + | |
| 911 | + await Effect.runPromise( | |
| 912 | + harness.engine.dispatch({ | |
| 913 | + type: "thread.turn.start", | |
| 914 | + commandId: CommandId.make("cmd-turn-start-workspace-2"), | |
| 915 | + threadId: ThreadId.make("thread-1"), | |
| 916 | + message: { | |
| 917 | + messageId: asMessageId("user-message-workspace-2"), | |
| 918 | + role: "user", | |
| 919 | + text: "second in worktree", | |
| 920 | + attachments: [], | |
| 921 | + }, | |
| 922 | + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, | |
| 923 | + runtimeMode: "approval-required", | |
| 924 | + createdAt: now, | |
| 925 | + }), | |
| 926 | + ); | |
| 927 | + | |
| 928 | + await waitFor(() => harness.startSession.mock.calls.length === 2); | |
| 929 | + await waitFor(() => harness.sendTurn.mock.calls.length === 2); | |
| 930 | + expect(harness.stopSession.mock.calls.length).toBe(0); | |
| 931 | + expect(harness.startSession.mock.calls[1]?.[1]).toMatchObject({ | |
| 932 | + threadId: ThreadId.make("thread-1"), | |
| 933 | + cwd: "/tmp/provider-project-worktree", | |
| 934 | + resumeCursor: { opaque: "resume-1" }, | |
| 935 | + modelSelection: { | |
| 936 | + provider: "claudeAgent", | |
| 937 | + model: "claude-sonnet-4-6", | |
| 938 | + }, | |
| 939 | + runtimeMode: "approval-required", | |
| 940 | + }); | |
| 941 | + }); | |
| 942 | + | |
| 867 | 943 | it("restarts claude sessions when claude effort changes", async () => { |
| 868 | 944 | const harness = await createHarness({ |
| 869 | 945 | threadModelSelection: { provider: "claudeAgent", model: "claude-sonnet-4-6" }, |
| @@ -334,6 +334,7 @@ const make = Effect.gen(function* () { · 333 unmodified lines | ||
| 334 | 334 | thread.session && thread.session.status !== "stopped" && activeSession ? thread.id : null; |
| 335 | 335 | if (existingSessionThreadId) { |
| 336 | 336 | const runtimeModeChanged = thread.runtimeMode !== thread.session?.runtimeMode; |
| 337 | + const cwdChanged = effectiveCwd !== activeSession?.cwd; | |
| 337 | 338 | const sessionModelSwitch = |
| 338 | 339 | currentProvider === undefined |
| 339 | 340 | ? "in-session" |
| @@ -350,6 +351,7 @@ const make = Effect.gen(function* () { · 10 unmodified lines | ||
| 350 | 351 | |
| 351 | 352 | if ( |
| 352 | 353 | !runtimeModeChanged && |
| 354 | + !cwdChanged && | |
| 353 | 355 | !shouldRestartForModelChange && |
| 354 | 356 | !shouldRestartForModelSelectionChange |
| 355 | 357 | ) { |
| @@ -367,6 +369,9 @@ const make = Effect.gen(function* () { · 11 unmodified lines | ||
| 367 | 369 | currentRuntimeMode: thread.session?.runtimeMode, |
| 368 | 370 | desiredRuntimeMode: thread.runtimeMode, |
| 369 | 371 | runtimeModeChanged, |
| 372 | + previousCwd: activeSession?.cwd, | |
| 373 | + desiredCwd: effectiveCwd, | |
| 374 | + cwdChanged, | |
| 370 | 375 | modelChanged, |
| 371 | 376 | shouldRestartForModelChange, |
| 372 | 377 | shouldRestartForModelSelectionChange, |
| @@ -381,6 +386,7 @@ const make = Effect.gen(function* () { · 8 unmodified lines | ||
| 381 | 386 | restartedSessionThreadId: restartedSession.threadId, |
| 382 | 387 | provider: restartedSession.provider, |
| 383 | 388 | runtimeMode: restartedSession.runtimeMode, |
| 389 | + cwd: restartedSession.cwd, | |
| 384 | 390 | }); |
| 385 | 391 | yield* bindSessionToThread(restartedSession); |
| 386 | 392 | return restartedSession.threadId; |
| @@ -2561,6 +2561,96 @@ describe("ClaudeAdapterLive", () => { · 2560 unmodified lines | ||
| 2561 | 2561 | ); |
| 2562 | 2562 | }); |
| 2563 | 2563 | |
| 2564 | + it.effect("preserves durable resume ids across Claude resume hooks", () => { | |
| 2565 | + const harness = makeHarness(); | |
| 2566 | + return Effect.gen(function* () { | |
| 2567 | + const adapter = yield* ClaudeAdapter; | |
| 2568 | + const durableSessionId = "550e8400-e29b-41d4-a716-446655440000"; | |
| 2569 | + const transientHookSessionId = "7368d0c7-40a3-4d8a-bcc1-ac80c49f2719"; | |
| 2570 | + | |
| 2571 | + const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 7).pipe( | |
| 2572 | + Stream.runCollect, | |
| 2573 | + Effect.forkChild, | |
| 2574 | + ); | |
| 2575 | + | |
| 2576 | + yield* adapter.startSession({ | |
| 2577 | + threadId: RESUME_THREAD_ID, | |
| 2578 | + provider: "claudeAgent", | |
| 2579 | + resumeCursor: { | |
| 2580 | + threadId: RESUME_THREAD_ID, | |
| 2581 | + resume: durableSessionId, | |
| 2582 | + resumeSessionAt: "assistant-99", | |
| 2583 | + turnCount: 3, | |
| 2584 | + }, | |
| 2585 | + runtimeMode: "full-access", | |
| 2586 | + }); | |
| 2587 | + | |
| 2588 | + harness.query.emit({ | |
| 2589 | + type: "system", | |
| 2590 | + subtype: "hook_started", | |
| 2591 | + hook_id: "resume-hook-1", | |
| 2592 | + hook_name: "SessionStart:resume", | |
| 2593 | + hook_event: "SessionStart", | |
| 2594 | + session_id: transientHookSessionId, | |
| 2595 | + uuid: "resume-hook-started", | |
| 2596 | + } as unknown as SDKMessage); | |
| 2597 | + | |
| 2598 | + harness.query.emit({ | |
| 2599 | + type: "system", | |
| 2600 | + subtype: "hook_response", | |
| 2601 | + hook_id: "resume-hook-1", | |
| 2602 | + hook_name: "SessionStart:resume", | |
| 2603 | + hook_event: "SessionStart", | |
| 2604 | + output: "", | |
| 2605 | + stdout: "", | |
| 2606 | + stderr: "", | |
| 2607 | + outcome: "success", | |
| 2608 | + session_id: transientHookSessionId, | |
| 2609 | + uuid: "resume-hook-response", | |
| 2610 | + } as unknown as SDKMessage); | |
| 2611 | + | |
| 2612 | + harness.query.emit({ | |
| 2613 | + type: "system", | |
| 2614 | + subtype: "init", | |
| 2615 | + apiKeySource: "none", | |
| 2616 | + claude_code_version: "test", | |
| 2617 | + cwd: "/tmp/claude-adapter-test", | |
| 2618 | + tools: [], | |
| 2619 | + mcp_servers: [], | |
| 2620 | + model: "claude-sonnet-4-5", | |
| 2621 | + permissionMode: "bypassPermissions", | |
| 2622 | + slash_commands: [], | |
| 2623 | + output_style: "default", | |
| 2624 | + skills: [], | |
| 2625 | + plugins: [], | |
| 2626 | + session_id: durableSessionId, | |
| 2627 | + uuid: "resume-init", | |
| 2628 | + } as unknown as SDKMessage); | |
| 2629 | + | |
| 2630 | + const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); | |
| 2631 | + const threadStartedEvents = runtimeEvents.filter((event) => event.type === "thread.started"); | |
| 2632 | + assert.equal(threadStartedEvents.length, 1); | |
| 2633 | + const threadStarted = threadStartedEvents[0]; | |
| 2634 | + assert.equal(threadStarted?.type, "thread.started"); | |
| 2635 | + if (threadStarted?.type === "thread.started") { | |
| 2636 | + assert.deepEqual(threadStarted.payload, { | |
| 2637 | + providerThreadId: durableSessionId, | |
| 2638 | + }); | |
| 2639 | + } | |
| 2640 | + | |
| 2641 | + const activeSessions = yield* adapter.listSessions(); | |
| 2642 | + const resumeCursor = activeSessions[0]?.resumeCursor as | |
| 2643 | + | { | |
| 2644 | + readonly resume?: string; | |
| 2645 | + } | |
| 2646 | + | undefined; | |
| 2647 | + assert.equal(resumeCursor?.resume, durableSessionId); | |
| 2648 | + }).pipe( | |
| 2649 | + Effect.provideService(Random.Random, makeDeterministicRandomService()), | |
| 2650 | + Effect.provide(harness.layer), | |
| 2651 | + ); | |
| 2652 | + }); | |
| 2653 | + | |
| 2564 | 2654 | it.effect("uses an app-generated Claude session id for fresh sessions", () => { |
| 2565 | 2655 | const harness = makeHarness(); |
| 2566 | 2656 | return Effect.gen(function* () { |
| @@ -198,6 +198,18 @@ function isSyntheticClaudeThreadId(value: string): boolean { · 197 unmodified lines | ||
| 198 | 198 | return value.startsWith("claude-thread-"); |
| 199 | 199 | } |
| 200 | 200 | |
| 201 | +function hasDurableClaudeSessionId(message: SDKMessage): boolean { | |
| 202 | + if (message.type !== "system") { | |
| 203 | + return true; | |
| 204 | + } | |
| 205 | + | |
| 206 | + return ( | |
| 207 | + message.subtype !== "hook_started" && | |
| 208 | + message.subtype !== "hook_progress" && | |
| 209 | + message.subtype !== "hook_response" | |
| 210 | + ); | |
| 211 | +} | |
| 212 | + | |
| 201 | 213 | function toMessage(cause: unknown, fallback: string): string { |
| 202 | 214 | if (cause instanceof Error && cause.message.length > 0) { |
| 203 | 215 | return cause.message; |
| @@ -1249,6 +1261,9 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( · 1045 unmodified lines | ||
| 1249 | 1261 | if (typeof message.session_id !== "string" || message.session_id.length === 0) { |
| 1250 | 1262 | return; |
| 1251 | 1263 | } |
| 1264 | + if (!hasDurableClaudeSessionId(message)) { | |
| 1265 | + return; | |
| 1266 | + } | |
| 1252 | 1267 | const nextThreadId = message.session_id; |
| 1253 | 1268 | context.resumeSessionId = message.session_id; |
| 1254 | 1269 | yield* updateResumeCursor(context); |
| @@ -2875,6 +2890,31 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( · 1620 unmodified lines | ||
| 2875 | 2890 | ...(Object.keys(extraArgs).length > 0 ? { extraArgs } : {}), |
| 2876 | 2891 | }; |
| 2877 | 2892 | |
| 2893 | + yield* Effect.annotateCurrentSpan({ | |
| 2894 | + "provider.kind": PROVIDER, | |
| 2895 | + "provider.thread_id": threadId, | |
| 2896 | + "provider.runtime_mode": input.runtimeMode, | |
| 2897 | + "claude.resume.source": | |
| 2898 | + existingResumeSessionId !== undefined ? "resume-session" : "generated-session", | |
| 2899 | + "claude.resume.thread_id": resumeState?.threadId ?? "", | |
| 2900 | + "claude.resume.session_id": existingResumeSessionId ?? "", | |
| 2901 | + "claude.resume.session_at": resumeState?.resumeSessionAt ?? "", | |
| 2902 | + "claude.resume.turn_count": resumeState?.turnCount ?? -1, | |
| 2903 | + "claude.query.cwd": input.cwd ?? "", | |
| 2904 | + "claude.query.model": apiModelId ?? "", | |
| 2905 | + "claude.query.effort": effectiveEffort ?? "", | |
| 2906 | + "claude.query.permission_mode": permissionMode ?? "", | |
| 2907 | + "claude.query.allow_dangerously_skip_permissions": permissionMode === "bypassPermissions", | |
| 2908 | + "claude.query.resume": existingResumeSessionId ?? "", | |
| 2909 | + "claude.query.session_id": newSessionId ?? "", | |
| 2910 | + "claude.query.include_partial_messages": true, | |
| 2911 | + "claude.query.additional_directories": input.cwd ? [input.cwd] : [], | |
| 2912 | + "claude.query.setting_sources": [...CLAUDE_SETTING_SOURCES], | |
| 2913 | + "claude.query.settings_json": JSON.stringify(settings), | |
| 2914 | + "claude.query.extra_args_json": JSON.stringify(extraArgs), | |
| 2915 | + "claude.query.path_to_executable": claudeBinaryPath, | |
| 2916 | + }); | |
| 2917 | + | |
| 2878 | 2918 | const queryRuntime = yield* Effect.try({ |
| 2879 | 2919 | try: () => |
| 2880 | 2920 | createQuery({ |
| @@ -929,9 +929,10 @@ routing.layer("ProviderServiceLive routing", (it) => { · 928 unmodified lines | ||
| 929 | 929 | const provider = yield* ProviderService; |
| 930 | 930 | const runtimeRepository = yield* ProviderSessionRuntimeRepository; |
| 931 | 931 | |
| 932 | − const session = yield* provider.startSession(asThreadId("thread-1"), { | |
| 932 | + const threadId = asThreadId("thread-runtime-status"); | |
| 933 | + const session = yield* provider.startSession(threadId, { | |
| 933 | 934 | provider: "codex", |
| 934 | − threadId: asThreadId("thread-1"), | |
| 935 | + threadId, | |
| 935 | 936 | runtimeMode: "full-access", |
| 936 | 937 | }); |
| 937 | 938 | yield* provider.sendTurn({ |
| @@ -957,7 +958,7 @@ routing.layer("ProviderServiceLive routing", (it) => { · 19 unmodified lines | ||
| 957 | 958 | lastError: string | null; |
| 958 | 959 | lastRuntimeEvent: string | null; |
| 959 | 960 | }; |
| 960 | − assert.equal(runtimePayload.cwd, process.cwd()); | |
| 961 | + assert.equal(runtimePayload.cwd, session.cwd); | |
| 961 | 962 | assert.equal(runtimePayload.model, null); |
| 962 | 963 | assert.equal(runtimePayload.activeTurnId, `turn-${String(session.threadId)}`); |
| 963 | 964 | assert.equal(runtimePayload.lastError, null); |
| @@ -1058,6 +1059,94 @@ routing.layer("ProviderServiceLive routing", (it) => { · 94 unmodified lines | ||
| 1058 | 1059 | fs.rmSync(tempDir, { recursive: true, force: true }); |
| 1059 | 1060 | }).pipe(Effect.provide(NodeServices.layer)), |
| 1060 | 1061 | ); |
| 1062 | + | |
| 1063 | + it.effect( | |
| 1064 | + "reuses persisted cwd when startSession resumes a claude session without cwd input", | |
| 1065 | + () => | |
| 1066 | + Effect.gen(function* () { | |
| 1067 | + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-provider-service-cwd-")); | |
| 1068 | + const dbPath = path.join(tempDir, "orchestration.sqlite"); | |
| 1069 | + const persistenceLayer = makeSqlitePersistenceLive(dbPath); | |
| 1070 | + const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive.pipe( | |
| 1071 | + Layer.provide(persistenceLayer), | |
| 1072 | + ); | |
| 1073 | + | |
| 1074 | + const firstClaude = makeFakeCodexAdapter("claudeAgent"); | |
| 1075 | + const firstRegistry: typeof ProviderAdapterRegistry.Service = { | |
| 1076 | + getByProvider: (provider) => | |
| 1077 | + provider === "claudeAgent" | |
| 1078 | + ? Effect.succeed(firstClaude.adapter) | |
| 1079 | + : Effect.fail(new ProviderUnsupportedError({ provider })), | |
| 1080 | + listProviders: () => Effect.succeed(["claudeAgent"]), | |
| 1081 | + }; | |
| 1082 | + const firstDirectoryLayer = ProviderSessionDirectoryLive.pipe( | |
| 1083 | + Layer.provide(runtimeRepositoryLayer), | |
| 1084 | + ); | |
| 1085 | + const firstProviderLayer = makeProviderServiceLive().pipe( | |
| 1086 | + Layer.provide(Layer.succeed(ProviderAdapterRegistry, firstRegistry)), | |
| 1087 | + Layer.provide(firstDirectoryLayer), | |
| 1088 | + Layer.provide(defaultServerSettingsLayer), | |
| 1089 | + Layer.provide(AnalyticsService.layerTest), | |
| 1090 | + ); | |
| 1091 | + | |
| 1092 | + const initial = yield* Effect.gen(function* () { | |
| 1093 | + const provider = yield* ProviderService; | |
| 1094 | + return yield* provider.startSession(asThreadId("thread-claude-cwd"), { | |
| 1095 | + provider: "claudeAgent", | |
| 1096 | + threadId: asThreadId("thread-claude-cwd"), | |
| 1097 | + cwd: "/tmp/project-claude-cwd", | |
| 1098 | + runtimeMode: "full-access", | |
| 1099 | + }); | |
| 1100 | + }).pipe(Effect.provide(firstProviderLayer)); | |
| 1101 | + | |
| 1102 | + const secondClaude = makeFakeCodexAdapter("claudeAgent"); | |
| 1103 | + const secondRegistry: typeof ProviderAdapterRegistry.Service = { | |
| 1104 | + getByProvider: (provider) => | |
| 1105 | + provider === "claudeAgent" | |
| 1106 | + ? Effect.succeed(secondClaude.adapter) | |
| 1107 | + : Effect.fail(new ProviderUnsupportedError({ provider })), | |
| 1108 | + listProviders: () => Effect.succeed(["claudeAgent"]), | |
| 1109 | + }; | |
| 1110 | + const secondDirectoryLayer = ProviderSessionDirectoryLive.pipe( | |
| 1111 | + Layer.provide(runtimeRepositoryLayer), | |
| 1112 | + ); | |
| 1113 | + const secondProviderLayer = makeProviderServiceLive().pipe( | |
| 1114 | + Layer.provide(Layer.succeed(ProviderAdapterRegistry, secondRegistry)), | |
| 1115 | + Layer.provide(secondDirectoryLayer), | |
| 1116 | + Layer.provide(defaultServerSettingsLayer), | |
| 1117 | + Layer.provide(AnalyticsService.layerTest), | |
| 1118 | + ); | |
| 1119 | + | |
| 1120 | + secondClaude.startSession.mockClear(); | |
| 1121 | + | |
| 1122 | + yield* Effect.gen(function* () { | |
| 1123 | + const provider = yield* ProviderService; | |
| 1124 | + yield* provider.startSession(initial.threadId, { | |
| 1125 | + provider: "claudeAgent", | |
| 1126 | + threadId: initial.threadId, | |
| 1127 | + runtimeMode: "full-access", | |
| 1128 | + }); | |
| 1129 | + }).pipe(Effect.provide(secondProviderLayer)); | |
| 1130 | + | |
| 1131 | + assert.equal(secondClaude.startSession.mock.calls.length, 1); | |
| 1132 | + const resumedStartInput = secondClaude.startSession.mock.calls[0]?.[0]; | |
| 1133 | + assert.equal(typeof resumedStartInput === "object" && resumedStartInput !== null, true); | |
| 1134 | + if (resumedStartInput && typeof resumedStartInput === "object") { | |
| 1135 | + const startPayload = resumedStartInput as { | |
| 1136 | + provider?: string; | |
| 1137 | + cwd?: string; | |
| 1138 | + resumeCursor?: unknown; | |
| 1139 | + threadId?: string; | |
| 1140 | + }; | |
| 1141 | + assert.equal(startPayload.provider, "claudeAgent"); | |
| 1142 | + assert.equal(startPayload.cwd, "/tmp/project-claude-cwd"); | |
| 1143 | + assert.deepEqual(startPayload.resumeCursor, initial.resumeCursor); | |
| 1144 | + assert.equal(startPayload.threadId, initial.threadId); | |
| 1145 | + } | |
| 1146 | + | |
| 1147 | + fs.rmSync(tempDir, { recursive: true, force: true }); | |
| 1148 | + }).pipe(Effect.provide(NodeServices.layer)), | |
| 1149 | + ); | |
| 1061 | 1150 | }); |
| 1062 | 1151 | |
| 1063 | 1152 | const fanout = makeProviderServiceLayer(); |
| @@ -374,9 +374,31 @@ const makeProviderService = Effect.fn("makeProviderService")(function* ( · 373 unmodified lines | ||
| 374 | 374 | (persistedBinding?.provider === input.provider |
| 375 | 375 | ? persistedBinding.resumeCursor |
| 376 | 376 | : undefined); |
| 377 | + const effectiveCwd = | |
| 378 | + input.cwd ?? | |
| 379 | + (persistedBinding?.provider === input.provider | |
| 380 | + ? readPersistedCwd(persistedBinding.runtimePayload) | |
| 381 | + : undefined); | |
| 382 | + yield* Effect.annotateCurrentSpan({ | |
| 383 | + "provider.resume_cursor.source": | |
| 384 | + input.resumeCursor !== undefined | |
| 385 | + ? "request" | |
| 386 | + : effectiveResumeCursor !== undefined && persistedBinding?.provider === input.provider | |
| 387 | + ? "persisted" | |
| 388 | + : "none", | |
| 389 | + "provider.resume_cursor.present": effectiveResumeCursor !== undefined, | |
| 390 | + "provider.cwd.source": | |
| 391 | + input.cwd !== undefined | |
| 392 | + ? "request" | |
| 393 | + : effectiveCwd !== undefined && persistedBinding?.provider === input.provider | |
| 394 | + ? "persisted" | |
| 395 | + : "none", | |
| 396 | + "provider.cwd.effective": effectiveCwd ?? "", | |
| 397 | + }); | |
| 377 | 398 | const adapter = yield* registry.getByProvider(input.provider); |
| 378 | 399 | const session = yield* adapter.startSession({ |
| 379 | 400 | ...input, |
| 401 | + ...(effectiveCwd !== undefined ? { cwd: effectiveCwd } : {}), | |
| 380 | 402 | ...(effectiveResumeCursor !== undefined ? { resumeCursor: effectiveResumeCursor } : {}), |
| 381 | 403 | }); |
| 382 | 404 | |
| @@ -398,7 +420,7 @@ const makeProviderService = Effect.fn("makeProviderService")(function* ( · 15 unmodified lines | ||
| 398 | 420 | provider: session.provider, |
| 399 | 421 | runtimeMode: input.runtimeMode, |
| 400 | 422 | hasResumeCursor: session.resumeCursor !== undefined, |
| 401 | − hasCwd: typeof input.cwd === "string" && input.cwd.trim().length > 0, | |
| 423 | + hasCwd: typeof effectiveCwd === "string" && effectiveCwd.trim().length > 0, | |
| 402 | 424 | hasModel: |
| 403 | 425 | typeof input.modelSelection?.model === "string" && |
| 404 | 426 | input.modelSelection.model.trim().length > 0, |