Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions docs/tools/task.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,12 @@ requests additional per-task isolation in the session's own repository; it is ex
only when `task.isolation.mode` enables an isolation backend.

`move_session` only narrows to a descendant directory, not a sibling repository.
User-driven `/move` and SDK cwd relocation are separate surfaces; an already-created
task tool retains its original repository binding. A fresh target-rooted session avoids
carrying that old delegation authority across a repository move.
User-driven `/move` and SDK cwd relocation are separate authorized surfaces. After a
committed move, new task admissions use the target repository authority and refreshed
agent resources, even when the task tool was materialized before the move. Already
admitted tasks (including queued and resumed work) retain their original execution
scope. A failed move or arbitrary cwd mutation does not grant target authority;
caller-provided `repositoryBinding` still cannot relocate the session.

`tasks[].tier` is inert while `task.autorouting.enabled` is `false`. When autorouting is active it selects the model chain for that item, an omitted `tier` routes as `balanced`, and the routed pin overrides the manual model chain. See [Autorouting](#autorouting).

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Fixed

- Allow new tasks to use the committed target repository after an authorized session move, including previously materialized task tools, while preserving admitted tasks' execution scope, repository checks, and output ownership.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Fixed

- Close the superseded session-scoped artifact manager's store when transitioning to a managed task artifact owner, preventing retention of derived authorities and ensuring proper lifecycle management.
2 changes: 1 addition & 1 deletion packages/coding-agent/src/prompts/tools/task.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Use `irc` for live coordination when it is available.
{{#if customSchemaEnabled}}- `schema`: JTD schema for expected structured output (do not put format rules in assignments){{/if}}
- `spawnPlan` (optional): required before any batch with more than 4 tasks; include whyParallel, whyNotLocal, independence, expectedReceiptShape, and maxInlineTokens.
{{#if isolationEnabled}}- `isolated`: run in an isolated environment; REQUIRED when the user explicitly requests a worktree (for example, "use worktree"), and use when tasks edit overlapping files{{/if}}
- `repositoryBinding`: optional authoritative repository/worktree identity. It must match the active session and cannot authorize a foreign repository from this session. For another approved repository, start a fresh session rooted there (for example, `gjc --cwd <approved-worktree>`); do not use `/move` because materialized tasks retain their original binding.
- `repositoryBinding`: optional authoritative repository/worktree identity. It must match the active session and cannot authorize a foreign repository from this session. For another approved repository, start a fresh session rooted there (for example, `gjc --cwd <approved-worktree>`). After an authorized committed session move, new tasks use the committed target scope; already-admitted tasks retain their original scope.
</parameters>

<rules>
Expand Down
84 changes: 71 additions & 13 deletions packages/coding-agent/src/sdk/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ import {
loadProjectContextFilesResult as loadContextFilesResultInternal,
} from "../system-prompt";
import { AgentOutputManager } from "../task/output-manager";
import type { TaskScopeAuthority } from "../task/scope";
import { parseThinkingLevel, resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
import { isMCPBridgeTool, selectRestorableDiscoveredBuiltinToolNames } from "../tool-discovery/tool-index";
import {
Expand Down Expand Up @@ -1802,6 +1803,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
);
const evalKernelOwnerId = `agent-session:${Snowflake.next()}`;
let disposeLocalProtocolOverride: (() => void) | undefined;
let unregisterTaskScopeSettings: (() => void) | undefined;
const releaseTaskScopeSettings = (): void => {
const unregister = unregisterTaskScopeSettings;
unregisterTaskScopeSettings = undefined;
unregister?.();
};
let localProtocolOverrideReleased = false;
const releaseLocalProtocolOverride = (): void => {
if (localProtocolOverrideReleased) return;
Expand Down Expand Up @@ -2845,18 +2852,23 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
* root's AGENTS.md and tree, and subagents inherit the same mismatch.
*/
const applyRescopedReadState = async (to: string): Promise<void> => {
try {
const rediscovered = await loadContextFilesResultInternal({ cwd: to, agentDir, profileAuthority });
contextFiles = rediscovered.contextFiles;
} catch (error) {
logger.warn("Failed to re-discover context files after session rescope", {
error: safeErrorForLog(error),
});
if (options.contextFiles === undefined) {
contextFiles = [];
try {
const rediscovered = await loadContextFilesResultInternal({ cwd: to, agentDir, profileAuthority });
contextFiles = rediscovered.contextFiles;
} catch (error) {
logger.warn("Failed to re-discover context files after session rescope", {
error: safeErrorForLog(error),
});
}
}
if (options.skills === undefined && settings.get("skills.enabled")) {
if (options.skills === undefined) {
try {
await session?.reloadSkills(to);
} catch (error) {
skills = getEmbeddedDefaultGjcSkills();
skillWarnings = [];
logger.warn("Failed to reload skills after session rescope", { error: safeErrorForLog(error) });
}
}
Expand All @@ -2882,10 +2894,50 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
}
};

const toolSession: ToolSession = {
const taskScopeOriginGeneration = sessionManager.getCwdGeneration();
let taskScopeSettings = settings;
const refreshTaskScopeSettings = async (): Promise<void> => {
taskScopeSettings =
sessionManager.getCwdGeneration() === taskScopeOriginGeneration
? settings
: await settings.snapshotForCwd(sessionManager.getCwd());
};
const refreshTaskScopeAfterMove = async (): Promise<void> => {
let settingsRefreshFailure: { error: unknown } | undefined;
try {
await refreshTaskScopeSettings();
} catch (error) {
settingsRefreshFailure = { error };
}
await applyRescopedReadState(sessionManager.getCwd());
if (options.promptTemplates === undefined) {
toolSession.promptTemplates = [];
try {
toolSession.promptTemplates = await discoverPromptTemplates(sessionManager.getCwd(), agentDir);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update the live prompt-template registry after moves

When templates are discovered automatically and a session moves from A to B, this assignment updates only the task-facing toolSession; the AgentSession retains the A templates supplied during construction in its private #promptTemplates array. Consequently, subsequent parent prompt(), steer(), or followUp() calls that expand a template can still inject A's repository-specific template even though new tasks use B's template. Add a live-session template replacement path and update both registries after discovery.

Useful? React with 👍 / 👎.

} catch (error) {
logger.warn("Failed to rediscover task prompt templates after session rescope", {
error: safeErrorForLog(error),
});
}
}
if (settingsRefreshFailure) throw settingsRefreshFailure.error;
};

const toolSession: ToolSession & TaskScopeAuthority = {
get cwd() {
return sessionManager.getCwd();
},
getTaskScopeIdentity: () => ({
cwd: sessionManager.getCwd(),
generation: sessionManager.getCwdGeneration(),
}),
getTaskScopeSettings: () => taskScopeSettings,
runWithTaskAdmission: admit =>
sessionManager.runWithCwdReadLease(async () => {
await refreshTaskScopeSettings();
return admit();
}),
runWithTaskOwnerReadLease: resolve => sessionManager.runWithCwdReadLease(resolve),
hasUI: options.hasUI ?? false,
profileAuthority,
workflowGateEligible: true,
Expand Down Expand Up @@ -3182,7 +3234,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
// Cwd-derived read-only state the prompt and subagents consume.
// Best-effort by design: the move is committed, and a failed
// re-discovery must not present a committed move as a failure.
await applyRescopedReadState(sessionManager.getCwd());
try {
await session?.refreshSshTool({ activateIfAvailable: true });
} catch (error) {
Expand Down Expand Up @@ -5394,15 +5445,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
reloadSkills:
options.skills === undefined
? async reloadCwd => {
if (!settings.get("skills.enabled")) {
const scopedSettings =
sessionManager.getCwdGeneration() === taskScopeOriginGeneration
? settings
: await settings.snapshotForCwd(sessionManager.getCwd());
if (!scopedSettings.get("skills.enabled")) {
return { skills: getEmbeddedDefaultGjcSkills(), warnings: [] };
} else {
const reloaded = await loadSkills({
...settings.getGroup("skills"),
...scopedSettings.getGroup("skills"),
agentDir,
profileAuthority,
cwd: reloadCwd,
disabledExtensions: settings.get("disabledExtensions"),
disabledExtensions: scopedSettings.get("disabledExtensions"),
});
return { skills: withEmbeddedDefaultGjcSkills(reloaded.skills), warnings: reloaded.warnings };
}
Expand Down Expand Up @@ -5487,6 +5542,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
// carried by the host replay ring through the internal runtime seam above.
if (autoroutingInactive) session.configWarnings.push(AUTOROUTING_INACTIVE_WARNING);
hasSession = true;
unregisterTaskScopeSettings = sessionManager.registerAfterMoveListener(refreshTaskScopeAfterMove);
const cleanupOwnedManager = cleanupOwnedMcpManager;
const sessionOwnedMcpManager = ownsMcpManager ? mcpManager : undefined;
if (cleanupOwnedManager && cleanupOwnedMcpManagerOwner !== sessionOwnedMcpManager) {
Expand Down Expand Up @@ -5558,6 +5614,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
try {
agentRegistry.unregister(resolvedAgentId);
releaseCredentialDisabledSubscription();
releaseTaskScopeSettings();
releaseLocalProtocolOverride();
} catch (error) {
failures.push(error);
Expand Down Expand Up @@ -6121,6 +6178,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
SessionManager.releaseProcessCwdOwnership(sessionManager);
processCwdClaimed = false;
});
await attemptCleanup(releaseTaskScopeSettings);
await attemptCleanup(releaseLocalProtocolOverride);
await attemptCleanup(closeOwnedAuthStorage);
if (cleanupDiagnostic !== undefined) {
Expand Down
Loading
Loading