fix(core): handle wrapped config session names for dot project paths#1552
fix(core): handle wrapped config session names for dot project paths#1552ChiragArora31 wants to merge 8 commits into
Conversation
Greptile SummaryThis PR hardens session prefix and storage-key generation for project paths that collapse to invalid identifiers (
Confidence Score: 4/5The PR is safe to merge and correctly addresses the dot-path session naming bug across all three affected code paths. All three previously-flagged code paths have been consistently updated. One gap remains in packages/core/src/config.ts — the
|
| Filename | Overview |
|---|---|
| packages/core/src/paths.ts | Adds sanitizeIdentifierComponent and deriveSessionPrefixFromProjectPath; threads sanitization into generateSessionPrefix. Logic is correct and backward-compatible for normal project names. |
| packages/core/src/config.ts | Exports generateLegacyWrappedStorageKey and switches applyProjectDefaults to deriveSessionPrefixFromProjectPath. Storage key now correctly resolves relative to config dir; session prefix still resolves relative to CWD (see comment). |
| packages/core/src/global-config.ts | Introduces getRegisteredSessionPrefix that re-derives invalid stored prefixes from the project path; used consistently in findSessionPrefixOwner, registerProjectInGlobalConfig, and resolveProjectIdentity. |
| packages/core/src/migration/storage-v2.ts | Updates extractProjectPrefixes to validate stored session prefixes with sanitizeIdentifierComponent before using them, fixing the active-session-detection gap for projects with invalid stored prefixes. |
| packages/core/tests/config.test.ts | Adds three regression tests for dot-path storage key sanitization, fingerprint disambiguation, and collision avoidance across risky paths. Tests cover the fixed code path directly via the exported function. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[project path e.g. '.'] --> B[sanitizeIdentifierComponent]
B --> C{sanitized == 'project'?}
C -- No --> D[generateSessionPrefix basename]
C -- Yes --> E[SHA-256 fingerprint of resolved path]
E --> F[generateSessionPrefix 'x-fingerprint']
D --> G[tmux-safe sessionPrefix]
F --> G
A --> H[generateLegacyWrappedStorageKey]
H --> I[resolve relative to configDir]
I --> J[sanitizeIdentifierComponent basename]
J --> K{component == 'project'?}
K -- No --> L[hash-component]
K -- Yes --> M[path fingerprint]
M --> N[hash-project-fingerprint]
P[stored sessionPrefix in global config] --> Q[getRegisteredSessionPrefix]
Q --> R{sanitizeIdentifierComponent stored == stored?}
R -- Yes --> S[use stored prefix]
R -- No --> T[deriveSessionPrefixFromProjectPath entry.path]
T --> G
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
packages/core/src/config.ts:600-602
When `applyProjectDefaults` calls `deriveSessionPrefixFromProjectPath(project.path)` with a relative path like `"."`, `resolve()` inside that function anchors to the process CWD — not the config file's directory. `generateLegacyWrappedStorageKey` (called earlier in the load pipeline) correctly resolves relative to `configDir`. If the caller's CWD differs from the config directory (e.g. running `ao` from `/home/user/` against a config at `/repos/myapp/agent-orchestrator.yaml`), the two calls resolve `"."` to different directories, producing a storage key fingerprinted on `/repos/myapp/` but a session prefix derived from `/home/user/` — and the prefix silently changes each time the CWD changes.
```suggestion
if (!project.sessionPrefix) {
project.sessionPrefix = deriveSessionPrefixFromProjectPath(
resolve(configDir, project.path),
);
}
```
Reviews (3): Last reviewed commit: "fix(core): sanitize migration active-ses..." | Re-trigger Greptile
i-trytoohard
left a comment
There was a problem hiding this comment.
Review: fix(core): handle wrapped config session names for dot project paths
Verdict: The fix in config.ts is correct. The test needs rework.
✅ The Fix (config.ts) — Looks Good
The sanitizeStorageKeyComponent function and the resolve() before basename() are both correct fixes for the reported bug:
resolve(configDir, projectPath)— resolves.relative to the config directory before takingbasename, sopath: "."becomes the actual directory name instead of.. This is the right approach.sanitizeStorageKeyComponent()— belt-and-suspenders sanitization that replaces invalid chars with-, strips leading/trailing dashes, and falls back to"project"if the result is still invalid. Clean implementation.
The storage key ${hash}-${basename} feeds into paths.ts:generateSessionId() which formats it as ${storageKey}-${prefix}-${num} — that composite string gets validated against /^[a-zA-Z0-9_-]+$/, so a bare . would explode. The fix prevents that.
❌ The Test (spawn.test.ts) — Does Not Exercise the Fix
Greptile's comment is correct and I'm confirming it independently. The test is not a valid regression test:
sessionPrefix: "app"is hardcoded insetupTestContext()(line 323). The spread...projectcopies it. The test project always hassessionPrefix: "app"regardless ofpath.- The wrapped config path is never triggered.
classifyConfigShape()reads the YAML from disk, but the test constructs an in-memory config and passes it throughvalidateConfig(). The YAML file containsprojects: {}, soapplyWrappedLocalStorageKeysprocesses the parsed YAML (which has no projects), not the in-memory config. - Session ID
"app-1"is determined bysessionPrefix: "app"alone. Even ifgenerateLegacyWrappedStorageKeyproducedhash-., the prefix derivation at line 586 checksif (!project.sessionPrefix)— since"app"is set, the storage key is irrelevant.
The test would pass identically on the pre-patch code. It does not test what it claims.
How to Fix the Test
Option A (preferred): Unit-test sanitizeStorageKeyComponent and generateLegacyWrappedStorageKey directly:
describe("generateLegacyWrappedStorageKey", () => {
it("sanitizes dot path to valid component", () => {
const key = generateLegacyWrappedStorageKey(configPath, ".");
expect(key).toMatch(/^[a-zA-Z0-9_-]+$/);
expect(key).not.toContain(".");
});
});This requires exporting the functions or testing via internals.
Option B: Construct a config without sessionPrefix, write a real wrapped YAML to disk, and load it through loadConfig() for a full end-to-end test.
Minor Notes
- The regex and sanitizer are top-level but only used in one place. Fine — small and self-contained.
- Fallback to
"project"is reasonable but worth a brief inline comment explaining when it triggers (e.g. path resolving to empty string).
Sanitize path-derived session prefixes so dot-path projects cannot generate invalid tmux names, and replace the prior spawn regression with a config-level test that directly exercises wrapped storage-key generation. Made-with: Cursor
|
Addressed both review concerns in
Validation: |
i-trytoohard
left a comment
There was a problem hiding this comment.
Re-review after test rework
Verdict: LGTM. Ready to merge. ✅
Changes since last review
- Removed the bad spawn test that didn't exercise the fixed path
- Replaced with a proper config-level test in
config.test.tsthat:- Writes a real YAML with
path: .to disk - Loads it through
loadConfig()(full pipeline — classify → applyWrappedKeys → validate → applyDefaults) - Calls
generateLegacyWrappedStorageKeydirectly and asserts no.in the result - Asserts
sessionPrefixis also sanitized (valid regex, not bare.)
- Writes a real YAML with
- Exported
generateLegacyWrappedStorageKeyfor direct testing — clean, minimal change - Added defense-in-depth:
sanitizeStorageKeyComponentnow also guards thesessionPrefixderivation path (line 595), not just the storage key path. Good call —basename(".")→"."would also blow up ingenerateSessionPrefix→ prefix collision detection.
CI
All checks passing — Test, Typecheck, Lint, Web Tests, Integration Tests, Fresh Onboarding. Green across the board.
Nice fix, Chirag. Ship it.
|
We dont care about backward compatibility, right now |
harshitsinghbhandari
left a comment
There was a problem hiding this comment.
Nice iteration on the test after the first round — the rewrite that drives loadConfig end-to-end and exports generateLegacyWrappedStorageKey is exactly right, and hardening applyProjectDefaults was a good catch. Just one thing before merge:
The same bug still reproduces through global-config.ts. The PR sanitizes the basename at two sites (config.ts:86, config.ts:595), but generateSessionPrefix(basename(...)) is still called unsanitized in three places:
global-config.ts:593—getRegisteredSessionPrefix, used by prefix-collision detectionglobal-config.ts:711— registration flow (ao project add)global-config.ts:806—resolveProjectIdentity
paths.ts:65 short-circuits on length <= 4 and returns the input verbatim, so generateSessionPrefix(".") → ".". Anyone going through the global-config flow with a dot path still gets an invalid tmux prefix.
Per CLAUDE.md ("WE DONOT APPLY SURFACE FIXES. WE FIX THE CORE FIRST"), the cleanest move is to sanitize inside generateSessionPrefix itself — move sanitizeStorageKeyComponent to paths.ts and call it on entry. That fixes all five present call sites and any future ones by construction, and the rename to something like sanitizeIdentifierComponent falls out naturally (since it's no longer storage-key-specific).
Two smaller things:
- Fallback collision.
sanitizeStorageKeyComponent("")returns"project". Two projects in the same wrapped config that both sanitize-empty (path: "/","..") collide on${hash}-project. Realistic? No. Worth a Zod-time guard or a${hash}-project-${id}fallback? Probably yes. - Tests. Worth pinning
path: "/",path: "..", and the collision case while you're in there.
No changeset either — the storage-key format change is user-visible (even if, as @ashish921998 noted, the old layout was never functional, so no migration needed).
Once generateSessionPrefix is hardened at the core, this should be ready.
… keys Move basename sanitization into paths via sanitizeIdentifierComponent and generateSessionPrefix; add deriveSessionPrefixFromProjectPath for edge paths that collapse to generic tokens. Disambiguate legacy wrapped storage keys when needed; align global registry and migration callers; add regression tests and a changeset. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Thanks @harshitsinghbhandari — implemented the core-first approach you suggested. What changed
Merged latest Happy to tweak naming ( |
|
This is great — verified end-to-end:
Non-blocking: |
…ed-storage-key Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # packages/core/src/config.ts
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…-1552 # Conflicts: # packages/core/src/global-config.ts
Resolves #1521.
Summary
..app-1).Root cause
Legacy wrapped config handling used
basename(projectPath)directly, sopath: .could produce a storage key suffix of..Approach
Resolve the project path relative to the config directory and normalize unsafe storage-key characters. Current V2 runtime naming remains prefix-based and is covered by the new spawn test.
Verification
pnpm --filter @aoagents/ao-core test -- project-resolver.test.ts session-manager/spawn.test.tspnpm --filter @aoagents/ao-core typecheckpnpm --filter @aoagents/ao-core buildpnpm --filter @aoagents/ao-core testwas also run; it still has existing timeout failures inmigration-storage-v2.test.tsandorchestrator-prompt.dist.test.ts.