From e465a4702df60323da2980409d3eb687b669c4bf Mon Sep 17 00:00:00 2001 From: Harshit Singh Bhandari <24b4506@iitb.ac.in> Date: Sun, 3 May 2026 20:40:18 +0530 Subject: [PATCH] fix(agent-claude-code): fold underscores in Claude project slug (#1611) (#1612) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `toClaudeProjectPath` only normalized `/`, `.`, and `:` — but Claude Code's on-disk slug also folds underscores (and other non-alphanumerics) to `-`. AO project data dirs are named `_` (e.g. `graph-isomorphism_d185b44d56`), so the slug AO computed pointed at a directory that never existed. Cascading failures: - `getSessionInfo` couldn't read the JSONL → `claudeSessionUuid` never got persisted to session metadata. - On restore, `getRestoreCommand`'s metadata lookup found nothing AND its workspace-scan fallback also missed (same bad slug), returning `null`. - Session-manager's native-restore guard then threw `SessionNotRestorableError` → API returned 409. Verified on-disk: every session under projects without underscores has `claudeSessionUuid` persisted; every session under projects with underscores does not. The orchestrator angle in #1611 is the loudest symptom — orchestrators die early so they have nothing else to fall back on — but the same bug silently broke worker restore in any multi-project setup. Fix: replace `[/.]` with `[^a-zA-Z0-9-]` in the slug regex, matching Claude Code's actual encoding. Adds direct unit tests for `toClaudeProjectPath` covering the underscore case plus existing paths, and a regression test in the `getSessionInfo` path-conversion suite. Fixes #1611. Co-authored-by: Claude Opus 4.7 --- .../fix-claude-project-slug-underscore.md | 5 ++ .../agent-claude-code/src/index.test.ts | 48 +++++++++++++++++++ .../plugins/agent-claude-code/src/index.ts | 18 +++---- 3 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 .changeset/fix-claude-project-slug-underscore.md diff --git a/.changeset/fix-claude-project-slug-underscore.md b/.changeset/fix-claude-project-slug-underscore.md new file mode 100644 index 000000000..2a6f41ab2 --- /dev/null +++ b/.changeset/fix-claude-project-slug-underscore.md @@ -0,0 +1,5 @@ +--- +"@aoagents/ao-plugin-agent-claude-code": patch +--- + +Fix `toClaudeProjectPath` to fold underscores (and any other non-alphanumeric character) to dashes, matching Claude Code's actual on-disk slug encoding. Previously only `/`, `.`, and `:` were normalized, so AO project data dirs of the form `_` produced slugs that pointed to non-existent directories — `getSessionInfo` and `getRestoreCommand` could never locate the session JSONL, `claudeSessionUuid` never got persisted, and restoring orchestrator/worker sessions in any multi-project setup failed with a 409 "getRestoreCommand returned null". Fixes #1611. diff --git a/packages/plugins/agent-claude-code/src/index.test.ts b/packages/plugins/agent-claude-code/src/index.test.ts index c05df45c5..8d431e14e 100644 --- a/packages/plugins/agent-claude-code/src/index.test.ts +++ b/packages/plugins/agent-claude-code/src/index.test.ts @@ -61,6 +61,7 @@ import { manifest, default as defaultExport, resetPsCache, + toClaudeProjectPath, METADATA_UPDATER_SCRIPT, } from "./index.js"; @@ -149,6 +150,40 @@ beforeEach(() => { mockHomedir.mockReturnValue("/mock/home"); }); +describe("toClaudeProjectPath", () => { + it("encodes a plain unix path", () => { + expect(toClaudeProjectPath("/Users/dev/projects/foo")).toBe("-Users-dev-projects-foo"); + }); + + it("collapses dot directories like .worktrees into a leading double dash", () => { + expect(toClaudeProjectPath("/Users/dev/.worktrees/ao/ao-3")).toBe( + "-Users-dev--worktrees-ao-ao-3", + ); + }); + + it("normalizes underscores to dashes (issue #1611)", () => { + // AO project data dirs are named `_`. Claude Code converts + // underscores to dashes when computing its on-disk project slug; without + // matching that here the slug points to a non-existent directory and + // restore loses the conversation. + expect( + toClaudeProjectPath( + "/Users/dev/.agent-orchestrator/projects/graph-isomorphism_d185b44d56/worktrees/gi-orchestrator", + ), + ).toBe( + "-Users-dev--agent-orchestrator-projects-graph-isomorphism-d185b44d56-worktrees-gi-orchestrator", + ); + }); + + it("strips Windows drive colons and folds backslashes", () => { + expect(toClaudeProjectPath("C:\\Users\\dev\\foo")).toBe("C-Users-dev-foo"); + }); + + it("collapses any other non-alphanumeric character into a dash", () => { + expect(toClaudeProjectPath("/Users/dev/proj@v2/foo bar")).toBe("-Users-dev-proj-v2-foo-bar"); + }); +}); + describe("plugin manifest & exports", () => { it("has correct manifest", () => { expect(manifest).toEqual({ @@ -505,6 +540,19 @@ describe("getSessionInfo", () => { "/mock/home/.claude/projects/-Users-dev--worktrees-ao-ao-3", ); }); + + it("normalizes underscores to dashes (matches Claude Code on-disk slug, issue #1611)", async () => { + mockJsonlFiles('{"type":"user","message":{"content":"hello"}}'); + await agent.getSessionInfo( + makeSession({ + workspacePath: + "/Users/dev/.agent-orchestrator/projects/graph-isomorphism_d185b44d56/worktrees/gi-orchestrator", + }), + ); + expect(mockReaddir).toHaveBeenCalledWith( + "/mock/home/.claude/projects/-Users-dev--agent-orchestrator-projects-graph-isomorphism-d185b44d56-worktrees-gi-orchestrator", + ); + }); }); describe("summary extraction", () => { diff --git a/packages/plugins/agent-claude-code/src/index.ts b/packages/plugins/agent-claude-code/src/index.ts index 7b748016d..18927ee79 100644 --- a/packages/plugins/agent-claude-code/src/index.ts +++ b/packages/plugins/agent-claude-code/src/index.ts @@ -231,21 +231,21 @@ export const manifest = { * Convert a workspace path to Claude's project directory path. * Claude stores sessions at ~/.claude/projects/{encoded-path}/ * - * Verified against Claude Code's actual encoding (as of v1.x): - * the path has its leading / stripped, then all / and . are replaced with -. - * e.g. /Users/dev/.worktrees/ao → Users-dev--worktrees-ao + * Verified against Claude Code's actual on-disk slugs: every non-alphanumeric + * character (other than `-`) is replaced with `-`. That includes `/`, `.`, + * and crucially `_` — AO's per-project data dirs are named like + * `_`, and without underscore folding the slug AO computes + * misses the directory Claude actually wrote (issue #1611). * - * If Claude Code changes its encoding scheme this will silently break - * introspection. The path can be validated at runtime by checking whether - * the resulting directory exists. + * Windows drive letters keep their special handling: `C:\Users\...` → strip + * the colon, then encode → `C-Users-...`. * * Exported for testing purposes. */ export function toClaudeProjectPath(workspacePath: string): string { // Handle Windows drive letters (C:\Users\... → C-Users-...) - const normalized = workspacePath.replace(/\\/g, "/"); - // Claude Code replaces / and . with - (keeping the leading slash as a leading -) - return normalized.replace(/:/g, "").replace(/[/.]/g, "-"); + const normalized = workspacePath.replace(/\\/g, "/").replace(/:/g, ""); + return normalized.replace(/[^a-zA-Z0-9-]/g, "-"); } /** Find the most recently modified .jsonl session file in a directory */