Skip to content

fix(sdk): resolve symlinked workspace cwd scope in SessionRouter endpoint reads - #4639

Closed
thisisjun786 wants to merge 1 commit into
Yeachan-Heo:devfrom
thisisjun786:fix/session-router-symlinked-cwd-scope
Closed

fix(sdk): resolve symlinked workspace cwd scope in SessionRouter endpoint reads#4639
thisisjun786 wants to merge 1 commit into
Yeachan-Heo:devfrom
thisisjun786:fix/session-router-symlinked-cwd-scope

Conversation

@thisisjun786

@thisisjun786 thisisjun786 commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

What

SessionRouter#readEndpoint now compares the indexed state root against the repo-derived default/chat state roots by path identity (resolveEquivalentPath) instead of lexical path.resolve spelling equality.

Why

GJC 0.14.0 ACP session/new fails with lost exact Router authority for every symlinked workspace cwd, making the provider unusable from external ACP clients (observed with Paseo 0.4.0: spawn ok, initialize ok, session/new error, Models unavailable, Status Error). Restarting the daemon and the SDK broker does not help.

The session index deliberately stores two different spellings for one session (reconcileReadyScope, packages/coding-agent/src/sdk/broker/lifecycle.ts):

  • locator.repo — the lifecycle caller's lexical cwd (re-scoped to launch.cwd; the comment there documents the macOS /var -> /private/var case),
  • locator.stateRoot — the host process's physical path, because the host derives it from process.cwd(), which resolves symlinks (session-runtime.ts:2700,2981).

#readEndpoint derived the expected state root from repo and compared the two spellings with plain path.resolve equality. With cwd = /home/jun/desk -> /data/Lina-Desk:

repo=/home/jun/desk           -> defaultStateRoot=/home/jun/desk/.gjc/state
indexedStateRoot=/data/Lina-Desk/.gjc/state
scope = undefined -> #readEndpoint returns null

Every reconcile then retires the adopted attachment (#reconcile adopted branch → #retireAttachment), attachment(id) stays null through the ACP agent's 40×50 ms publication poll, and acp-agent.ts#attachEndpoint throws lost exact Router authority — after which the cleanup tears down a healthy, fully registered host (index shows host_registeredhost_heartbeatrecord_reconciled all clean before the failure).

The broker side is already symlink-tolerant (sameResumeLocator uses resolveEquivalentPath, and the 0.14.0 index fence-row predicate was itself introduced to fix "a plain path.resolve match … under a symlinked agent dir") — this gate was the one place still comparing spellings.

Testing

  • Regression test (this PR): publishes an attachment whose indexed repo is a symlinked spelling of the state root — fails on dev (clients: 0, attachment never created), passes with the fix; full suite bun test packages/coding-agent/test/sdk-session-router-authority.test.ts41 pass.
  • Live ACP e2e against 0.14.0 with the same patch applied locally, minimal stdio JSON-RPC client (initializesession/newsession/close):
    • symlinked cwd /home/jun/desk: session/new ERROR: lost exact Router authority before → session/new OK + session/close ok after
    • realpath cwd /data/Lina-Desk and plain cwd /home/jun/tmp: pass before and after (no regression)
  • Paseo provider diagnostic (paseo provider diagnostic gjc) after the local patch: spawn/initialize/session-new/cleanup all ok, Models: 185, Status: Ready (was: session/new error, Models failed, Status Error).
  • bun --cwd=packages/coding-agent run check (biome + tsc) clean.

GJC verdict

gajae.pr-review-verdict.v1 needs-human sha256:cdabb4b14e62f8545ccb0dc3d7257e49e13f7b59193e9c7b56ee13155ee58dac reviewer:human reviewer-id:thisisjun786 evidence:bun test packages/coding-agent/test/sdk-session-router-authority.test.ts (41 pass) + CI run 32050202452

Author-submitted fix; no independent review of this exact head yet, so the verdict is needs-human per the template.


  • Target branch is dev
  • bun check passes (biome + tsc --noEmit on packages/coding-agent; CI state gates green)
  • Tested locally
  • CHANGELOG updated (user-facing fix)
  • Verdict above matches the exact PR head, not an earlier commit
Notes
  • Repo-side workarounds without this fix: point the ACP client's workspace cwd at the realpath, or use a non-symlinked path.
  • Separately observed while verifying (not addressed here, may deserve its own issue): when a launch fails and rollback runs against artifact paths that traverse a symlinked cwd, the removal proof can end in cleanup_pending: reparse_point.

…oint reads

The broker's session index stores the lifecycle caller's lexical cwd in
locator.repo (reconcileReadyScope re-scopes only that field) while
locator.stateRoot stays the host process's physical path, because the host
derives it from process.cwd(), which resolves symlinks. #readEndpoint derived
the expected state root from repo and compared spellings with plain
path.resolve equality, so any symlinked cwd made the scope test fail on every
reconcile: the adopted attachment was retired, attachment() stayed null
through the ACP agent's publication poll, and session/new surfaced
"lost exact Router authority" before tearing down a healthy host.

Observed end to end with Paseo 0.4.0 driving gjc 0.14.0 over ACP with a
symlinked workspace cwd (/home/jun/desk -> /data/Lina-Desk): spawn and
initialize ok, session/new error, Models unavailable, Status Error, while the
session host had registered and reconciled cleanly and was only killed by the
resulting cleanup. The scope test now compares path identity through
resolveEquivalentPath, the same symlink-equivalent comparison
sameResumeLocator and the index fence-row predicate already use.

Confidence: high
Scope-risk: narrow
Reversibility: trivial
Tested: bun test packages/coding-agent/test/sdk-session-router-authority.test.ts (41 pass); live ACP e2e session/new+session/close with a symlinked cwd, its realpath, and a plain cwd
Not-tested: Windows reparse-point workspace cwds
@Yeachan-Heo

Copy link
Copy Markdown
Owner

This PR is superseded by #4645 (maintainer successor, exact same patch).

Why: the PR base f3726ef05e fell behind current dev 6696988b6d after #4631/#4613, and the contributor fork does not accept maintainer pushes, so the stale head could not be refreshed in place.

Equivalence proof for #4645 vs this PR's head ec39d177: identical stable patch-id c08e92a0a5109c128daea918bd560c730aa4220d, and a 0-byte tree diff when the external patch is applied onto current dev. Authorship preserved (thisisjun786 remains the commit author); credit to @thisisjun786 for the original analysis, patch, and regression test is explicit in #4645.

This PR will be closed in favor of #4645, which carries the refreshed verification (focused tests, 10/10 adversarial authority controls, live ACP e2e for symlinked + canonical cwds, package check, state-writers gate) and drives the merge for #4637.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo

Copy link
Copy Markdown
Owner

Superseded by maintainer successor #4645 on current dev.

The successor preserves @thisisjun786's original commit authorship and credits the original analysis, patch, regression test, and live ACP/Paseo diagnosis. Equivalence is proven by identical stable patch-id c08e92a0a5109c128daea918bd560c730aa4220d, a zero-byte tree diff against current-dev plus the external patch, and byte-identical PR-touched files. The contributor fork reports push=false, so the stale base/head could not be refreshed in place.

Terminal delivery continues only in #4645; closing this duplicate mutation path to preserve one-item/one-owner discipline.


[repo owner's gaebal-gajae (clawdbot) 🦞]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants