ISS-10812: route each session's calls to its own worktree (PLN-2070 PR 2) - #33
Merged
Merged
Conversation
PLN-2070 PR 2. The plugin's PreToolUse hook records each session's cwd; the daemon picks a request's root by D2's precedence (explicit root, worktree paths, recorded cwd, per-session bash cwd, workspace) for code tools, relative read/edit paths and cwd-less bash. Code tools say which worktree answered; edit's resolved-against note needs a relative path. Co-Authored-By: Claude <noreply@anthropic.com>
…m session routing
A session that searched in the main checkout and repeated the query after entering a worktree (or naming one through paths) got a blank result stamped with the worktree's repo_root. The recent-query bucket is now keyed by (session, answering checkout). Co-Authored-By: Claude <<redacted-email>>
…nd runs A cwd-less bash now runs in the session's worktree, but the "already read in full" check still resolved its relative path against the main checkout, so `cat` of a worktree file was refused after a full read of the main checkout's copy. Co-Authored-By: Claude <<redacted-email>>
…SSION_CWD Co-Authored-By: Claude <<redacted-email>>
…ftest instead of layering over its patch
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Campaign iss-10812-20260921, chunk C2 of 2: route each session's calls to its own worktree.
This PR builds PLN-2070 PR 2, for ISS-10812. It builds on C1 (#32), which gave each linked worktree an index copied from the main checkout's.
What this lands
Sessions start in the main checkout and enter a worktree when work begins, so they share the main checkout's daemon. Until now, about 90% of their code searches (1,642 of 1,817 in the transcripts ISS-10812 cites) were answered from the main checkout's index. With this PR:
The hook records where each session is working. The plugin's PreToolUse hook on every lc call (
mcp_read_allow.py) records the session'scwdunder<store>/session_cwd/<session_id>. It writes before any early return, only when the value changes, atomically and 0700.session_start.pyprunes records older than 7 days.Each request resolves a session root (
gateway/adapters/mcp/session_root.py), in this order:repo_rootorroot;pathsthat all fall inside one linked worktree (paths spanning two worktrees are refused, naming both);cwd, if it is the workspace root or a linked worktree of it;bashcwd, now kept per session (256 entries at most, dropped after 24 h idle);The process-global
_last_session_cwd, and its lc-debt note, are gone.Code tools and relative paths follow the session root:
code_search,smart_search,code_query/relations(through the engine factory), relativereadandeditpaths,bashwith nocwd, and the three review tools. Workspace-level state stays keyed by the workspace; the full call-site table is below.Results name the checkout when it isn't the workspace root:
repo_root: <path>.Edit's "resolved against worktree" note now appears only when a relative path was actually resolved against the worktree.
A fresh worktree's first code calls wait for its first refresh, for up to 10 s from the refresh's start, so the first answer is already the worktree's. On a timeout the answer is marked as refreshing.
Answers read while the index is refreshing don't count toward near-repeat suppression, so a retry gets answered.
Accepted limitation, the operator's decision: a session and its subagents share one session id. A subagent running in its own worktree (Agent
isolation: worktree) while its parent stays in the main checkout will keep switching the recordedcwd. Even then, calls that name the worktree throughpathsorrepo_rootroute correctly, and edits to existing files stay protected by the ambiguity refusal.Known limitation, pre-existing:
linked_worktree_of, which C1 moved over unchanged, resolves a relativegitdir:(fromgit worktree add --relative-paths) against the process cwd. It returns None, so such a worktree falls back safely to the workspace root.Acceptance ledger
8 criteria: 6 met, 2 met differently, 0 not met.
Mcp-Session-Ids each get their own worktree; relative creates land in each caller's worktreepathsinside a worktree route to itpathsinto wt_a still find the wt_a-only symbolcwdnever usedbashmap is capped and forgets idle sessionsGate (PR 2 change 1): passed. A headless
claude -prun (2.1.278) in a throwaway repo, with a stand-in MCP server, showed two things:EnterWorktree, the PreToolUse payload'scwdis the worktree path;session_idequalsCLAUDE_CODE_SESSION_IDin the MCP server's environment, which is what the bridge sends asMcp-Session-Id.Deviations from the plan
test_edit_mcp_handler.pythat monkeypatched_last_session_cwdnow set a per-sessionbashcwd. The constraint allowed this, and the tests are named in the build outcome._session_worktree_rootstays as a thin adapter over the session root, so the review-tool tests that monkeypatch it are unedited.cwdin a subdirectory of the main checkout is ignored and falls back to today's behavior.cwdequal to the main checkout outranks abashcwd in a worktree (D2 step 3 before step 4). Absolute-path edits no longer carry the "resolved against worktree" note (AC-2.5)._workspace_root()call sites10 follow the session, 13 stay on the workspace, and 5 grep hits are not call sites.
_default_workflow_agent_executortool_agent_symbol_recalltool_review_rationale/_evidence/_feedback_addressed_session_worktree_root→ session root_bootstrap_context_statustool_get_contextmode=symbolsengine call follows through the factory)_compress_contexttool_smart_edit_write_handover_packet/_compact_advise_code_repo_root/_code_context_engine/_scoped_context_capability/_workspace_code_routertool_scanrepo_rootis still honored_attach_code_search_savings/tool_code_search/tool_smart_searchReview
The deep review (5 reviewers, run cr-48218, at
a775115b) was NEEDS_ATTENTION: 3 verified findings and 1 rejected. It proved it read this worktree at that commit.Fixed:
code_searchsuppression keyed by session only: a session that searched in the main checkout and then repeated the search in its worktree got a blank answer. It is now keyed by the answering checkout too, with a regression test that fails on the old code (eec5e595).bash's "already read in full" check resolved relative paths against the main checkout while a cwd-less command ran in the worktree (12415c8b)."session_cwd"hardcoded twice: both places now usesession_root.SESSION_CWD(a2935c2d).Declined, reason verbatim:
routed_paths()re-derives thetoolbroker's call shape: "The duplication is real, but both readers decode the published input schema of thetoolbroker (action,name,arguments), which every MCP client and the server's own instructions already depend on. Renaming those keys would be a breaking wire change that has to touch clients, instructions and both readers whether or not they share a helper, so a shared extractor protects against a rename nobody plans. The two readers also need different behavior: _tool_broker_handler validates and raises on a malformed call, while routed_paths must quietly return no routing paths for anything malformed and let the handler report the error. A shared cross-module abstraction for two call sites with different error contracts costs more than it buys at this stage."Re-review: the gate fired because resolve touched a file with a HIGH finding. The re-review (cr-67896,
--since-last-review, ata2935c2d) was APPROVED, with one new MEDIUM:_session_worktree_cwd()lacked_run_bash_tool'sis_dir()fallback.Second round, the operator's decision. The orchestrator's own end-to-end check found that the first search in a fresh worktree answered from the copied main-checkout index, unmarked, while the first refresh ran. That missed C1's AC-1.3 "marked refreshing". The same session's reworded retry was then blanked as a near-repeat. The operator chose to wait for the refresh and fix it without a third fleet review:
4d1310b6).8474c560).37c1cefd).These last three commits were not reviewed by the fleet. Each has a regression test that fails on the pre-fix code, and the orchestrator's probe re-ran unchanged against them:
Verification
tests/gateway/test_session_root_routing.py: 20 passed.test_reads_during_reindex.py: 16 passed.test_mcp_read_allow_hook.py,test_plugin_runtime_hooks.py) and the review-tool suites passed in the build and in the first resolve.test_workspace_root_edit_reindexes_once_after_the_responsefailed once in an early build run. This change does not touch it, and it passed in 5 later runs and in isolation.Rollout
After this merges, reinstall. Then two live sessions in two worktrees of symphony-alpha should each find code that exists only in their own worktree (AC-2.8). Also record daemon RSS with five worktree sessions attached.
This is the last chunk of the campaign.