authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-04-23 13:29:54-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-23 13:29:54-04:00
log188df6da074bf60af765881bdfd6e886ef83e6ee
treef44f72b0eefbb70bec3c5829909daa923376fc01
parent0d55a42840231122f8d7414b35f155343031f82d
signature Signed by PGP key B5690EEEBB952194

Fix Claude session cwd resume drift (#2292)

Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Julius Marminge <juliusmarminge@users.noreply.github.com> Co-authored-by: codex <codex@users.noreply.github.com>

6 files changed, 327 insertions(+), 4 deletions(-)

apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+76
...@@ -138,6 +138,12 @@ describe("ProviderCommandReactor", () => {...@@ -138,6 +138,12 @@ describe("ProviderCommandReactor", () => {
138 (input.runtimeMode === "approval-required" || input.runtimeMode === "full-access")138 (input.runtimeMode === "approval-required" || input.runtimeMode === "full-access")
139 ? input.runtimeMode139 ? input.runtimeMode
140 : "full-access",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 ...(modelSelection.model !== undefined ? { model: modelSelection.model } : {}),147 ...(modelSelection.model !== undefined ? { model: modelSelection.model } : {}),
142 threadId,148 threadId,
143 resumeCursor: resumeCursor ?? { opaque: `resume-${sessionIndex}` },149 resumeCursor: resumeCursor ?? { opaque: `resume-${sessionIndex}` },
...@@ -864,6 +870,76 @@ describe("ProviderCommandReactor", () => {...@@ -864,6 +870,76 @@ describe("ProviderCommandReactor", () => {
864 expect(harness.stopSession.mock.calls.length).toBe(0);870 expect(harness.stopSession.mock.calls.length).toBe(0);
865 });871 });
866872
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 it("restarts claude sessions when claude effort changes", async () => {943 it("restarts claude sessions when claude effort changes", async () => {
868 const harness = await createHarness({944 const harness = await createHarness({
869 threadModelSelection: { provider: "claudeAgent", model: "claude-sonnet-4-6" },945 threadModelSelection: { provider: "claudeAgent", model: "claude-sonnet-4-6" },
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+6
...@@ -334,6 +334,7 @@ const make = Effect.gen(function* () {...@@ -334,6 +334,7 @@ const make = Effect.gen(function* () {
334 thread.session && thread.session.status !== "stopped" && activeSession ? thread.id : null;334 thread.session && thread.session.status !== "stopped" && activeSession ? thread.id : null;
335 if (existingSessionThreadId) {335 if (existingSessionThreadId) {
336 const runtimeModeChanged = thread.runtimeMode !== thread.session?.runtimeMode;336 const runtimeModeChanged = thread.runtimeMode !== thread.session?.runtimeMode;
337 const cwdChanged = effectiveCwd !== activeSession?.cwd;
337 const sessionModelSwitch =338 const sessionModelSwitch =
338 currentProvider === undefined339 currentProvider === undefined
339 ? "in-session"340 ? "in-session"
...@@ -350,6 +351,7 @@ const make = Effect.gen(function* () {...@@ -350,6 +351,7 @@ const make = Effect.gen(function* () {
350351
351 if (352 if (
352 !runtimeModeChanged &&353 !runtimeModeChanged &&
354 !cwdChanged &&
353 !shouldRestartForModelChange &&355 !shouldRestartForModelChange &&
354 !shouldRestartForModelSelectionChange356 !shouldRestartForModelSelectionChange
355 ) {357 ) {
...@@ -367,6 +369,9 @@ const make = Effect.gen(function* () {...@@ -367,6 +369,9 @@ const make = Effect.gen(function* () {
367 currentRuntimeMode: thread.session?.runtimeMode,369 currentRuntimeMode: thread.session?.runtimeMode,
368 desiredRuntimeMode: thread.runtimeMode,370 desiredRuntimeMode: thread.runtimeMode,
369 runtimeModeChanged,371 runtimeModeChanged,
372 previousCwd: activeSession?.cwd,
373 desiredCwd: effectiveCwd,
374 cwdChanged,
370 modelChanged,375 modelChanged,
371 shouldRestartForModelChange,376 shouldRestartForModelChange,
372 shouldRestartForModelSelectionChange,377 shouldRestartForModelSelectionChange,
...@@ -381,6 +386,7 @@ const make = Effect.gen(function* () {...@@ -381,6 +386,7 @@ const make = Effect.gen(function* () {
381 restartedSessionThreadId: restartedSession.threadId,386 restartedSessionThreadId: restartedSession.threadId,
382 provider: restartedSession.provider,387 provider: restartedSession.provider,
383 runtimeMode: restartedSession.runtimeMode,388 runtimeMode: restartedSession.runtimeMode,
389 cwd: restartedSession.cwd,
384 });390 });
385 yield* bindSessionToThread(restartedSession);391 yield* bindSessionToThread(restartedSession);
386 return restartedSession.threadId;392 return restartedSession.threadId;
apps/server/src/provider/Layers/ClaudeAdapter.test.ts+90
...@@ -2561,6 +2561,96 @@ describe("ClaudeAdapterLive", () => {...@@ -2561,6 +2561,96 @@ describe("ClaudeAdapterLive", () => {
2561 );2561 );
2562 });2562 });
25632563
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 it.effect("uses an app-generated Claude session id for fresh sessions", () => {2654 it.effect("uses an app-generated Claude session id for fresh sessions", () => {
2565 const harness = makeHarness();2655 const harness = makeHarness();
2566 return Effect.gen(function* () {2656 return Effect.gen(function* () {
apps/server/src/provider/Layers/ClaudeAdapter.ts+40
...@@ -198,6 +198,18 @@ function isSyntheticClaudeThreadId(value: string): boolean {...@@ -198,6 +198,18 @@ function isSyntheticClaudeThreadId(value: string): boolean {
198 return value.startsWith("claude-thread-");198 return value.startsWith("claude-thread-");
199}199}
200200
201function 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
201function toMessage(cause: unknown, fallback: string): string {213function toMessage(cause: unknown, fallback: string): string {
202 if (cause instanceof Error && cause.message.length > 0) {214 if (cause instanceof Error && cause.message.length > 0) {
203 return cause.message;215 return cause.message;
...@@ -1249,6 +1261,9 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (...@@ -1249,6 +1261,9 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
1249 if (typeof message.session_id !== "string" || message.session_id.length === 0) {1261 if (typeof message.session_id !== "string" || message.session_id.length === 0) {
1250 return;1262 return;
1251 }1263 }
1264 if (!hasDurableClaudeSessionId(message)) {
1265 return;
1266 }
1252 const nextThreadId = message.session_id;1267 const nextThreadId = message.session_id;
1253 context.resumeSessionId = message.session_id;1268 context.resumeSessionId = message.session_id;
1254 yield* updateResumeCursor(context);1269 yield* updateResumeCursor(context);
...@@ -2875,6 +2890,31 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (...@@ -2875,6 +2890,31 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
2875 ...(Object.keys(extraArgs).length > 0 ? { extraArgs } : {}),2890 ...(Object.keys(extraArgs).length > 0 ? { extraArgs } : {}),
2876 };2891 };
28772892
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 const queryRuntime = yield* Effect.try({2918 const queryRuntime = yield* Effect.try({
2879 try: () =>2919 try: () =>
2880 createQuery({2920 createQuery({
apps/server/src/provider/Layers/ProviderService.test.ts+92-3
...@@ -929,9 +929,10 @@ routing.layer("ProviderServiceLive routing", (it) => {...@@ -929,9 +929,10 @@ routing.layer("ProviderServiceLive routing", (it) => {
929 const provider = yield* ProviderService;929 const provider = yield* ProviderService;
930 const runtimeRepository = yield* ProviderSessionRuntimeRepository;930 const runtimeRepository = yield* ProviderSessionRuntimeRepository;
931931
932 const session = yield* provider.startSession(asThreadId("thread-1"), {932 const threadId = asThreadId("thread-runtime-status");
933 const session = yield* provider.startSession(threadId, {
933 provider: "codex",934 provider: "codex",
934 threadId: asThreadId("thread-1"),935 threadId,
935 runtimeMode: "full-access",936 runtimeMode: "full-access",
936 });937 });
937 yield* provider.sendTurn({938 yield* provider.sendTurn({
...@@ -957,7 +958,7 @@ routing.layer("ProviderServiceLive routing", (it) => {...@@ -957,7 +958,7 @@ routing.layer("ProviderServiceLive routing", (it) => {
957 lastError: string | null;958 lastError: string | null;
958 lastRuntimeEvent: string | null;959 lastRuntimeEvent: string | null;
959 };960 };
960 assert.equal(runtimePayload.cwd, process.cwd());961 assert.equal(runtimePayload.cwd, session.cwd);
961 assert.equal(runtimePayload.model, null);962 assert.equal(runtimePayload.model, null);
962 assert.equal(runtimePayload.activeTurnId, `turn-${String(session.threadId)}`);963 assert.equal(runtimePayload.activeTurnId, `turn-${String(session.threadId)}`);
963 assert.equal(runtimePayload.lastError, null);964 assert.equal(runtimePayload.lastError, null);
...@@ -1058,6 +1059,94 @@ routing.layer("ProviderServiceLive routing", (it) => {...@@ -1058,6 +1059,94 @@ routing.layer("ProviderServiceLive routing", (it) => {
1058 fs.rmSync(tempDir, { recursive: true, force: true });1059 fs.rmSync(tempDir, { recursive: true, force: true });
1059 }).pipe(Effect.provide(NodeServices.layer)),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});
10621151
1063const fanout = makeProviderServiceLayer();1152const fanout = makeProviderServiceLayer();
apps/server/src/provider/Layers/ProviderService.ts+23-1
...@@ -374,9 +374,31 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (...@@ -374,9 +374,31 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
374 (persistedBinding?.provider === input.provider374 (persistedBinding?.provider === input.provider
375 ? persistedBinding.resumeCursor375 ? persistedBinding.resumeCursor
376 : undefined);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 const adapter = yield* registry.getByProvider(input.provider);398 const adapter = yield* registry.getByProvider(input.provider);
378 const session = yield* adapter.startSession({399 const session = yield* adapter.startSession({
379 ...input,400 ...input,
401 ...(effectiveCwd !== undefined ? { cwd: effectiveCwd } : {}),
380 ...(effectiveResumeCursor !== undefined ? { resumeCursor: effectiveResumeCursor } : {}),402 ...(effectiveResumeCursor !== undefined ? { resumeCursor: effectiveResumeCursor } : {}),
381 });403 });
382404
...@@ -398,7 +420,7 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (...@@ -398,7 +420,7 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
398 provider: session.provider,420 provider: session.provider,
399 runtimeMode: input.runtimeMode,421 runtimeMode: input.runtimeMode,
400 hasResumeCursor: session.resumeCursor !== undefined,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 hasModel:424 hasModel:
403 typeof input.modelSelection?.model === "string" &&425 typeof input.modelSelection?.model === "string" &&
404 input.modelSelection.model.trim().length > 0,426 input.modelSelection.model.trim().length > 0,