Skip to content

Commit fa205dc

Browse files
authored
Merge pull request #4688 from Yeachan-Heo/owner/issue-4629-current-dev-0450
feat(session): agent-invokable session rescope via move_session tool
2 parents baf78ae + bca63ca commit fa205dc

23 files changed

Lines changed: 2171 additions & 77 deletions

packages/coding-agent/CHANGELOG.md

Lines changed: 7 additions & 2 deletions
Large diffs are not rendered by default.

packages/coding-agent/scripts/generate-sdk-operation-inventory.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,16 @@ const LOCKED_EXCLUSIONS: Readonly<Record<string, string>> = {
7373
"agent_session:setForcedToolChoice": "internal accessor/plumbing, not a user-facing control seam",
7474
"agent_session:getActiveSkillState": "internal accessor/plumbing, not a user-facing control seam",
7575
"agent_session:getActiveSkillPhase": "internal accessor/plumbing, not a user-facing control seam",
76+
"agent_session:getEffectiveActiveWorkflowSkillState":
77+
"internal restored-or-live workflow guard for session rescope, not a user-facing SDK control seam",
78+
"agent_session:replaceOwnedMcpManager":
79+
"internal cwd-rebinding helper for owned MCP authority after move_session, not a user-facing SDK control seam",
80+
"agent_session:replaceNamedCustomTools":
81+
"internal cwd-rebinding helper for named custom tools after move_session, not a user-facing SDK control seam",
82+
"agent_session:replaceSkills":
83+
"internal cwd-rebinding helper for session skills after move_session, not a user-facing SDK control seam",
84+
"agent_session:retireWorkspaceTreeForRescope":
85+
"internal cwd-rebinding helper that retires the cached workspace tree after move_session, not a user-facing SDK control seam",
7686
"agent_session:getDeepInterviewAskStage":
7787
"internal AskTool schema-selection accessor, not a user-facing SDK control seam",
7888
"agent_session:peekQueueInvoker": "internal accessor/plumbing, not a user-facing control seam",

packages/coding-agent/scripts/generate-tool-catalog.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ function makeSession(cwd: string): any {
116116
hasEditTool: true,
117117
taskDepth: 0,
118118
currentAgentType: "executor",
119+
rescopeSessionCwd: async () => ({ from: cwd, to: cwd }),
119120
getSessionFile: () => null,
120121
getSessionSpawns: () => null,
121122
getSessionId: () => "catalog",

packages/coding-agent/src/modes/controllers/command-controller.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1153,13 +1153,15 @@ export class CommandController {
11531153
}
11541154

11551155
try {
1156-
await this.ctx.sessionManager.flush();
1157-
await this.ctx.sessionManager.moveTo(resolvedPath);
1158-
setProjectDir(resolvedPath);
1159-
clearClaudePluginRootsCache(); // re-warms preloadedPluginRoots with new project dir (async)
1160-
resetCapabilities();
1161-
await this.ctx.refreshSlashCommandState(resolvedPath);
1162-
await this.ctx.session.refreshSshTool({ activateIfAvailable: true });
1156+
await this.ctx.sessionManager.runExclusiveCwdTransition(async () => {
1157+
await this.ctx.sessionManager.flush();
1158+
await this.ctx.sessionManager.moveTo(resolvedPath);
1159+
setProjectDir(resolvedPath);
1160+
clearClaudePluginRootsCache();
1161+
resetCapabilities();
1162+
await this.ctx.refreshSlashCommandState(resolvedPath);
1163+
await this.ctx.session.refreshSshTool({ activateIfAvailable: true });
1164+
});
11631165

11641166
this.ctx.statusLine.invalidate();
11651167
this.ctx.updateEditorTopBorder();
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Rescope the session to a narrower working directory.
2+
3+
Use this only when the session's working directory is a broad launcher root (for example a
4+
multi-repo workspace like `~/Projects`) and the task has clearly converged on one subdirectory
5+
or repository: after this call, every later turn resolves relative paths and the bash default
6+
cwd from the new directory, and project-scoped plugins/capabilities reload for it.
7+
8+
- `path` must be an existing directory; relative paths resolve against the current session cwd.
9+
The canonical target must be strictly inside the current session directory — moves to a
10+
parent, a sibling project, or an unrelated absolute path are refused.
11+
- A session can be moved this way at most once, and never while another move is running; a
12+
rejected call does not consume the move. Use it once the target repo is identified — not
13+
speculatively — because the session file and caches move with the session.
14+
- This tool is unavailable in subagent sessions and restricted profiles; ask the top-level
15+
session to rescope instead.

packages/coding-agent/src/runtime/optional-runtime-services.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ export type OptionalRuntimeServicesOverrides = Partial<OptionalRuntimeServices>;
2020

2121
/** Context needed by services whose identity is scoped to the session cwd. */
2222
export interface OptionalRuntimeServicesContext {
23-
cwd?: string;
23+
/** Session cwd. Pass a getter when the session can rescope (`move_session`). */
24+
cwd?: string | (() => string);
2425
}
2526

2627
/**
@@ -32,7 +33,7 @@ export function createOptionalRuntimeServices(
3233
overrides: OptionalRuntimeServicesOverrides = {},
3334
context: OptionalRuntimeServicesContext = {},
3435
): OptionalRuntimeServices {
35-
const cwd = context.cwd ?? process.cwd();
36+
const cwd = context.cwd ?? (() => process.cwd());
3637
return {
3738
memoryBackend: overrides.memoryBackend ?? createMemoryBackendService(settings),
3839
workspaceTree: overrides.workspaceTree ?? createWorkspaceTreeService(settings, cwd),

packages/coding-agent/src/runtime/workspace-tree-service.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,24 @@ export interface WorkspaceTreeRuntime {
1515
* Build the workspace-tree service without importing the native scanner until
1616
* the service is activated. The scan itself remains the single authority for
1717
* both eager startup and the lazy first-turn barrier.
18+
*
19+
* `cwd` is resolved per scan rather than captured once: a session that rescopes
20+
* (`move_session`, `/move`) must have its refreshes re-root at the new cwd,
21+
* otherwise every later tree describes the abandoned launcher root.
1822
*/
19-
export function createWorkspaceTreeService(settings: Settings, cwd: string): LazyService<WorkspaceTreeRuntime> {
23+
export function createWorkspaceTreeService(
24+
settings: Settings,
25+
cwd: string | (() => string),
26+
): LazyService<WorkspaceTreeRuntime> {
27+
const resolveCwd = typeof cwd === "function" ? cwd : () => cwd;
2028
return createLazyService({
2129
id: "workspaceTree",
2230
enabled: () => settings.get("workspaceTree.mode") === "eager" || settings.get("workspaceTree.mode") === "lazy",
2331
initialize: async ({ signal }) => {
2432
const scan = async (): Promise<WorkspaceTree> => {
2533
if (signal.aborted) throw new Error("Workspace-tree scan was aborted before it started.");
2634
const { buildWorkspaceTree } = await import("../workspace-tree");
27-
const tree = await buildWorkspaceTree(cwd, { timeoutMs: WORKSPACE_TREE_SCAN_TIMEOUT_MS });
35+
const tree = await buildWorkspaceTree(resolveCwd(), { timeoutMs: WORKSPACE_TREE_SCAN_TIMEOUT_MS });
2836
if (signal.aborted) throw new Error("Workspace-tree scan was aborted before it completed.");
2937
return tree;
3038
};
@@ -34,7 +42,7 @@ export function createWorkspaceTreeService(settings: Settings, cwd: string): Laz
3442
snapshot,
3543
refresh: async () => {
3644
const { buildWorkspaceTree } = await import("../workspace-tree");
37-
return buildWorkspaceTree(cwd, { timeoutMs: WORKSPACE_TREE_SCAN_TIMEOUT_MS });
45+
return buildWorkspaceTree(resolveCwd(), { timeoutMs: WORKSPACE_TREE_SCAN_TIMEOUT_MS });
3846
},
3947
},
4048
};

packages/coding-agent/src/sdk/protocol/operation-inventory.generated.json

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2446,6 +2446,39 @@
24462446
"testIds": "not_applicable"
24472447
}
24482448
},
2449+
{
2450+
"sourceId": "agent_session:getEffectiveActiveWorkflowSkillState",
2451+
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",
2452+
"sourceKind": "agent_session",
2453+
"decision": "exclude",
2454+
"rationale": "internal restored-or-live workflow guard for session rescope, not a user-facing SDK control seam",
2455+
"exclusionMetadata": {
2456+
"adapterMappings": "not_applicable",
2457+
"testIds": "not_applicable"
2458+
}
2459+
},
2460+
{
2461+
"sourceId": "agent_session:replaceOwnedMcpManager",
2462+
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",
2463+
"sourceKind": "agent_session",
2464+
"decision": "exclude",
2465+
"rationale": "internal cwd-rebinding helper for owned MCP authority after move_session, not a user-facing SDK control seam",
2466+
"exclusionMetadata": {
2467+
"adapterMappings": "not_applicable",
2468+
"testIds": "not_applicable"
2469+
}
2470+
},
2471+
{
2472+
"sourceId": "agent_session:replaceNamedCustomTools",
2473+
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",
2474+
"sourceKind": "agent_session",
2475+
"decision": "exclude",
2476+
"rationale": "internal cwd-rebinding helper for named custom tools after move_session, not a user-facing SDK control seam",
2477+
"exclusionMetadata": {
2478+
"adapterMappings": "not_applicable",
2479+
"testIds": "not_applicable"
2480+
}
2481+
},
24492482
{
24502483
"sourceId": "agent_session:getActiveSkillPhase",
24512484
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",
@@ -3693,6 +3726,28 @@
36933726
"testIds": "not_applicable"
36943727
}
36953728
},
3729+
{
3730+
"sourceId": "agent_session:replaceSkills",
3731+
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",
3732+
"sourceKind": "agent_session",
3733+
"decision": "exclude",
3734+
"rationale": "internal cwd-rebinding helper for session skills after move_session, not a user-facing SDK control seam",
3735+
"exclusionMetadata": {
3736+
"adapterMappings": "not_applicable",
3737+
"testIds": "not_applicable"
3738+
}
3739+
},
3740+
{
3741+
"sourceId": "agent_session:retireWorkspaceTreeForRescope",
3742+
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",
3743+
"sourceKind": "agent_session",
3744+
"decision": "exclude",
3745+
"rationale": "internal cwd-rebinding helper that retires the cached workspace tree after move_session, not a user-facing SDK control seam",
3746+
"exclusionMetadata": {
3747+
"adapterMappings": "not_applicable",
3748+
"testIds": "not_applicable"
3749+
}
3750+
},
36963751
{
36973752
"sourceId": "agent_session:getTodoPhases",
36983753
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",

0 commit comments

Comments
 (0)