Problem
Sessions launched from a multi-repo root (e.g. ~/Projects with 180+ repos) pay a heavy per-turn cost: the volatile workspace-tree snapshot scans the entire root, and none of it is relevant once the conversation converges on one repo. /move solves this — but it is a user-only surface. The agent, which is usually the first to know which repo the task belongs to, cannot invoke it. The user has to notice and type /move <repo> manually every session.
Related: #4628 fixes /move so the workspace tree actually follows the move.
Proposal
An agent-invokable session rescope tool (working name: move_session), wired through the same optional-accessor pattern goal uses:
ToolSession.rescopeSessionCwd?: (path: string) => Promise<{ from: string; to: string }> — optional accessor, absent in contexts where relocation must not happen (subagents, read-only bash restriction profiles).
- Wired in
sdk/session.ts next to the existing accessors: validate the target is an existing directory, then run the same sequence as the text/ACP /move handler — sessionManager.flush() → sessionManager.moveTo(resolved) → setProjectDir(resolved) → session.rescopeWorkspaceTree() → plugin/capability cache reset.
- Tool exposed to the model with a description scoped to intent: "when the session's working directory is a broad launcher root and the task has converged on one subdirectory/repo, rescope the session there."
With this, a root-launched session auto-descends: the model resolves the repo from the conversation and calls the tool; the next turn's workspace tree, bash default cwd, and relative path resolution are all repo-scoped. Root-as-launcher becomes a first-class workflow instead of a token sink.
Design questions for maintainers
- Gating: should the tool be permission-gated like
bash/edit (it is no more powerful than bash, which can already operate anywhere), or behind a setting (session.agentMove: true), or allowed by default?
- Naming:
move_session vs rescope vs reusing the /move name.
- Subagents: propose the accessor is simply not provided at
taskDepth > 0.
- TUI: the text/ACP
/move handler already runs without InteractiveModeContext; status-line refresh happens on the next render. Acceptable, or should the tool emit an event the TUI host consumes?
Happy to implement once the shape is agreed — the /move internals are already shared, so the diff is mostly the accessor, the tool definition, and tests.
Problem
Sessions launched from a multi-repo root (e.g.
~/Projectswith 180+ repos) pay a heavy per-turn cost: the volatile workspace-tree snapshot scans the entire root, and none of it is relevant once the conversation converges on one repo./movesolves this — but it is a user-only surface. The agent, which is usually the first to know which repo the task belongs to, cannot invoke it. The user has to notice and type/move <repo>manually every session.Related: #4628 fixes
/moveso the workspace tree actually follows the move.Proposal
An agent-invokable session rescope tool (working name:
move_session), wired through the same optional-accessor patterngoaluses:ToolSession.rescopeSessionCwd?: (path: string) => Promise<{ from: string; to: string }>— optional accessor, absent in contexts where relocation must not happen (subagents, read-only bash restriction profiles).sdk/session.tsnext to the existing accessors: validate the target is an existing directory, then run the same sequence as the text/ACP/movehandler —sessionManager.flush()→sessionManager.moveTo(resolved)→setProjectDir(resolved)→session.rescopeWorkspaceTree()→ plugin/capability cache reset.With this, a root-launched session auto-descends: the model resolves the repo from the conversation and calls the tool; the next turn's workspace tree, bash default cwd, and relative path resolution are all repo-scoped. Root-as-launcher becomes a first-class workflow instead of a token sink.
Design questions for maintainers
bash/edit(it is no more powerful thanbash, which can already operate anywhere), or behind a setting (session.agentMove: true), or allowed by default?move_sessionvsrescopevs reusing the/movename.taskDepth > 0./movehandler already runs withoutInteractiveModeContext; status-line refresh happens on the next render. Acceptable, or should the tool emit an event the TUI host consumes?Happy to implement once the shape is agreed — the
/moveinternals are already shared, so the diff is mostly the accessor, the tool definition, and tests.