`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 `<sanitized>_<hash>` (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 <noreply@anthropic.com>
This commit is contained in:
parent
3b9ba3122e
commit
e465a4702d
|
|
@ -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 `<sanitized>_<hash>` 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.
|
||||
|
|
@ -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 `<sanitized>_<hash>`. 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", () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
* `<sanitized>_<hash>`, 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 */
|
||||
|
|
|
|||
Loading…
Reference in New Issue