From f3cfa3047c5f5a36f87e23808365a4062777eeff Mon Sep 17 00:00:00 2001 From: Raihanullah Shamsi <73234279+Rehan959@users.noreply.github.com> Date: Sat, 4 Apr 2026 22:32:40 +0530 Subject: [PATCH 1/2] Fixed the issue with #1726 --- .sisyphus/plans/fix-project-path-error.md | 75 ++++++++++ .../Layers/ProviderCommandReactor.test.ts | 129 ++++++++++++++++++ .../Layers/ProviderCommandReactor.ts | 22 ++- 3 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 .sisyphus/plans/fix-project-path-error.md diff --git a/.sisyphus/plans/fix-project-path-error.md b/.sisyphus/plans/fix-project-path-error.md new file mode 100644 index 0000000000..07e58d4a62 --- /dev/null +++ b/.sisyphus/plans/fix-project-path-error.md @@ -0,0 +1,75 @@ +# Plan: Fix Misleading Error Message When Project Directory is Moved/Deleted + +## Issue Summary +When a user creates a project in T3 Code and then renames/moves/deletes the project directory externally (via terminal or file explorer), the error message displayed is misleading: + +- **Expected**: "path could not be found" or similar clear message about the missing project directory +- **Actual**: "Claude Code native binary not found at claude. Please ensure Claude Code is installed..." + +This causes confusion because users think Claude Code is not installed when the actual issue is the project directory no longer exists. + +## Root Cause Analysis + +1. **Path Resolution Flow**: + - When sending a message to Claude, the system resolves the workspace root from the project database + - The database stores the original workspace root path (e.g., `/home/user/my-project`) + - When the directory is renamed/moved, the stored path still points to the old location + +2. **Error Handling Gap**: + - `resolveThreadWorkspaceCwd()` in `checkpointing/Utils.ts` retrieves the stored workspace root + - This path is passed as `cwd` to `ProviderService.startSession()` + - In `ClaudeAdapter.ts`, this `cwd` is passed to the Claude SDK via `pathToClaudeCodeExecutable` + - When the directory doesn't exist, the SDK fails with "native binary not found" instead of detecting the path issue + +3. **Missing Validation**: + - The system never validates that the workspace root exists before attempting to start a provider session + - There is no early check to detect if the project directory has been moved/deleted + +## Solution + +Add workspace root existence validation in `ProviderCommandReactor.ts` before starting a provider session. This ensures a clear, accurate error message is shown when the project directory is missing. + +### Changes Made + +**File**: `apps/server/src/orchestration/Layers/ProviderCommandReactor.ts` + +1. Added imports: + - `FileSystem` from "effect" + - `Result` from "effect" + +2. Added validation block after resolving `effectiveCwd`: + ```typescript + // Validate that the workspace root exists before attempting to start a session + if (effectiveCwd) { + const pathExists = yield* FileSystem.FileSystem.pipe( + Effect.flatMap((fs) => fs.stat(effectiveCwd)), + Effect.map(() => true), + Effect.catchTag("SystemError", (e) => + e.reason === "NotFound" ? Effect.succeed(false) : Effect.fail(e), + ), + Effect.result, + ); + if (Result.isFailure(pathExists) || pathExists.success === Option.none()) { + return yield* new ProviderAdapterRequestError({ + provider: preferredProvider ?? "claudeAgent", + method: "thread.turn.start", + detail: `Project workspace path '${effectiveCwd}' no longer exists. The project directory may have been moved or deleted. Please recreate the project or select a different project.`, + }); + } + } + ``` + +## Expected Result + +After this fix, when a user sends a message after the project directory has been moved/deleted: +- Clear error: "Project workspace path '/path/to/project' no longer exists. The project directory may have been moved or deleted. Please recreate the project or select a different project." +- Instead of: "Claude Code native binary not found..." + +## Test Verification + +To verify the fix works: +1. Start T3 Code desktop app +2. Create a new project +3. Rename the project directory via terminal/file explorer +4. Begin a new chat and send a message to Claude +5. Verify the error message is now about the missing path, not the missing Claude binary \ No newline at end of file diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts index ca3dc04517..607ac18fe0 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts @@ -1579,4 +1579,133 @@ describe("ProviderCommandReactor", () => { expect(thread?.session?.threadId).toBe("thread-1"); expect(thread?.session?.activeTurnId).toBeNull(); }); + + it("returns clear error when project workspace path does not exist", async () => { + const now = new Date().toISOString(); + const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3code-reactor-missing-path-")); + createdBaseDirs.add(baseDir); + const { stateDir } = deriveServerPathsSync(baseDir, undefined); + createdStateDirs.add(stateDir); + const runtimeEventPubSub = Effect.runSync(PubSub.unbounded()); + const modelSelection = { + provider: "claudeAgent", + model: "claude-sonnet-4-6", + }; + + const startSession = vi.fn(() => Effect.die(new Error("Should not be called"))); + const sendTurn = vi.fn(() => Effect.die(new Error("Should not be called"))); + const interruptTurn = vi.fn(() => Effect.void); + const respondToRequest = vi.fn(() => Effect.void); + const respondToUserInput = vi.fn(() => Effect.void); + const stopSession = vi.fn(() => Effect.void); + const renameBranch = vi.fn(() => Effect.succeed({ branch: "test-branch" })); + const generateBranchName = vi.fn(() => + Effect.fail(new TextGenerationError({ operation: "generateBranchName", detail: "disabled" })), + ); + const generateThreadTitle = vi.fn(() => + Effect.fail(new TextGenerationError({ operation: "generateThreadTitle", detail: "disabled" })), + ); + + const service: ProviderServiceShape = { + startSession: startSession as ProviderServiceShape["startSession"], + sendTurn: sendTurn as ProviderServiceShape["sendTurn"], + interruptTurn: interruptTurn as ProviderServiceShape["interruptTurn"], + respondToRequest: respondToRequest as ProviderServiceShape["respondToRequest"], + respondToUserInput: respondToUserInput as ProviderServiceShape["respondToUserInput"], + stopSession: stopSession as ProviderServiceShape["stopSession"], + listSessions: () => Effect.succeed([]), + getCapabilities: () => Effect.succeed({ sessionModelSwitch: "in-session" }), + rollbackConversation: () => Effect.die(new Error("Unsupported")) as never, + streamEvents: Stream.fromPubSub(runtimeEventPubSub), + }; + + const orchestrationLayer = OrchestrationEngineLive.pipe( + Layer.provide(OrchestrationProjectionSnapshotQueryLive), + Layer.provide(OrchestrationProjectionPipelineLive), + Layer.provide(OrchestrationEventStoreLive), + Layer.provide(OrchestrationCommandReceiptRepositoryLive), + Layer.provide(SqlitePersistenceMemory), + ); + const layer = ProviderCommandReactorLive.pipe( + Layer.provideMerge(orchestrationLayer), + Layer.provideMerge(Layer.succeed(ProviderService, service)), + Layer.provideMerge(Layer.succeed(GitCore, { renameBranch } as unknown as GitCoreShape)), + Layer.provideMerge( + Layer.mock(TextGeneration, { + generateBranchName, + generateThreadTitle, + }), + ), + Layer.provideMerge(ServerSettingsService.layerTest()), + Layer.provideMerge(ServerConfig.layerTest(process.cwd(), baseDir)), + Layer.provideMerge(NodeServices.layer), + ); + const runtime = ManagedRuntime.make(layer); + + const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService)); + const reactor = await runtime.runPromise(Effect.service(ProviderCommandReactor)); + scope = await Effect.runPromise(Scope.make("sequential")); + await Effect.runPromise(reactor.start().pipe(Scope.provide(scope))); + + const nonExistentPath = "/tmp/this-path-definitely-does-not-exist-t3code-test-12345"; + await Effect.runPromise( + engine.dispatch({ + type: "project.create", + commandId: CommandId.makeUnsafe("cmd-project-missing"), + projectId: asProjectId("project-missing"), + title: "Missing Project", + workspaceRoot: nonExistentPath, + defaultModelSelection: modelSelection, + createdAt: now, + }), + ); + await Effect.runPromise( + engine.dispatch({ + type: "thread.create", + commandId: CommandId.makeUnsafe("cmd-thread-missing"), + threadId: ThreadId.makeUnsafe("thread-missing"), + projectId: asProjectId("project-missing"), + title: "Thread", + modelSelection: modelSelection, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + branch: null, + worktreePath: null, + createdAt: now, + }), + ); + + let errorOccurred = false; + let errorDetail: string | undefined; + try { + await Effect.runPromise( + engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.makeUnsafe("cmd-turn-start-missing"), + threadId: ThreadId.makeUnsafe("thread-missing"), + message: { + messageId: asMessageId("user-message-missing"), + role: "user", + text: "hello", + attachments: [], + }, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt: now, + }), + ); + } catch (e) { + errorOccurred = true; + if (e instanceof ProviderAdapterRequestError) { + errorDetail = e.detail; + } + } + + expect(errorOccurred).toBe(true); + expect(errorDetail).toContain("no longer exists"); + expect(errorDetail).toContain(nonExistentPath); + expect(startSession).not.toHaveBeenCalled(); + + await runtime.dispose(); + }); }); diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts index 419e3f3bf2..b7752a9878 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts @@ -11,7 +11,7 @@ import { type RuntimeMode, type TurnId, } from "@t3tools/contracts"; -import { Cache, Cause, Duration, Effect, Equal, Layer, Option, Schema, Stream } from "effect"; +import { Cache, Cause, Duration, Effect, Equal, FileSystem, Layer, Option, Result, Schema, Stream } from "effect"; import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker"; import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts"; @@ -256,6 +256,26 @@ const make = Effect.gen(function* () { projects: readModel.projects, }); + // Validate that the workspace root exists before attempting to start a session + // This provides a clear error message when a project directory has been moved/deleted + if (effectiveCwd) { + const pathExists = yield* FileSystem.FileSystem.pipe( + Effect.flatMap((fs) => fs.stat(effectiveCwd)), + Effect.map(() => true), + Effect.catchTag("SystemError", (e) => + e.reason === "NotFound" ? Effect.succeed(false) : Effect.fail(e), + ), + Effect.result, + ); + if (Result.isFailure(pathExists) || pathExists.success === Option.none()) { + return yield* new ProviderAdapterRequestError({ + provider: preferredProvider ?? "claudeAgent", + method: "thread.turn.start", + detail: `Project workspace path '${effectiveCwd}' no longer exists. The project directory may have been moved or deleted. Please recreate the project or select a different project.`, + }); + } + } + const resolveActiveSession = (threadId: ThreadId) => providerService .listSessions() From b949ec7bf31a366cbfec57adea2c8e0ca671d571 Mon Sep 17 00:00:00 2001 From: Raihanullah Shamsi <73234279+Rehan959@users.noreply.github.com> Date: Sat, 4 Apr 2026 22:34:22 +0530 Subject: [PATCH 2/2] Fixed the issue with #1726 --- .sisyphus/plans/fix-project-path-error.md | 75 ----------------------- 1 file changed, 75 deletions(-) delete mode 100644 .sisyphus/plans/fix-project-path-error.md diff --git a/.sisyphus/plans/fix-project-path-error.md b/.sisyphus/plans/fix-project-path-error.md deleted file mode 100644 index 07e58d4a62..0000000000 --- a/.sisyphus/plans/fix-project-path-error.md +++ /dev/null @@ -1,75 +0,0 @@ -# Plan: Fix Misleading Error Message When Project Directory is Moved/Deleted - -## Issue Summary -When a user creates a project in T3 Code and then renames/moves/deletes the project directory externally (via terminal or file explorer), the error message displayed is misleading: - -- **Expected**: "path could not be found" or similar clear message about the missing project directory -- **Actual**: "Claude Code native binary not found at claude. Please ensure Claude Code is installed..." - -This causes confusion because users think Claude Code is not installed when the actual issue is the project directory no longer exists. - -## Root Cause Analysis - -1. **Path Resolution Flow**: - - When sending a message to Claude, the system resolves the workspace root from the project database - - The database stores the original workspace root path (e.g., `/home/user/my-project`) - - When the directory is renamed/moved, the stored path still points to the old location - -2. **Error Handling Gap**: - - `resolveThreadWorkspaceCwd()` in `checkpointing/Utils.ts` retrieves the stored workspace root - - This path is passed as `cwd` to `ProviderService.startSession()` - - In `ClaudeAdapter.ts`, this `cwd` is passed to the Claude SDK via `pathToClaudeCodeExecutable` - - When the directory doesn't exist, the SDK fails with "native binary not found" instead of detecting the path issue - -3. **Missing Validation**: - - The system never validates that the workspace root exists before attempting to start a provider session - - There is no early check to detect if the project directory has been moved/deleted - -## Solution - -Add workspace root existence validation in `ProviderCommandReactor.ts` before starting a provider session. This ensures a clear, accurate error message is shown when the project directory is missing. - -### Changes Made - -**File**: `apps/server/src/orchestration/Layers/ProviderCommandReactor.ts` - -1. Added imports: - - `FileSystem` from "effect" - - `Result` from "effect" - -2. Added validation block after resolving `effectiveCwd`: - ```typescript - // Validate that the workspace root exists before attempting to start a session - if (effectiveCwd) { - const pathExists = yield* FileSystem.FileSystem.pipe( - Effect.flatMap((fs) => fs.stat(effectiveCwd)), - Effect.map(() => true), - Effect.catchTag("SystemError", (e) => - e.reason === "NotFound" ? Effect.succeed(false) : Effect.fail(e), - ), - Effect.result, - ); - if (Result.isFailure(pathExists) || pathExists.success === Option.none()) { - return yield* new ProviderAdapterRequestError({ - provider: preferredProvider ?? "claudeAgent", - method: "thread.turn.start", - detail: `Project workspace path '${effectiveCwd}' no longer exists. The project directory may have been moved or deleted. Please recreate the project or select a different project.`, - }); - } - } - ``` - -## Expected Result - -After this fix, when a user sends a message after the project directory has been moved/deleted: -- Clear error: "Project workspace path '/path/to/project' no longer exists. The project directory may have been moved or deleted. Please recreate the project or select a different project." -- Instead of: "Claude Code native binary not found..." - -## Test Verification - -To verify the fix works: -1. Start T3 Code desktop app -2. Create a new project -3. Rename the project directory via terminal/file explorer -4. Begin a new chat and send a message to Claude -5. Verify the error message is now about the missing path, not the missing Claude binary \ No newline at end of file