Skip to content
Merged
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
13 changes: 13 additions & 0 deletions packages/core/src/agents/background-agent-resume.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,17 @@ describe('BackgroundAgentResumeService', () => {
fireSubagentStartEvent: vi.fn().mockResolvedValue(undefined),
fireSubagentStopEvent: vi.fn().mockResolvedValue(undefined),
};
// Stub registry exposed on both `parent.getToolRegistry()` and the
// override built by `createApprovalModeOverride` (which now rebuilds
// the tool registry on the resumed agent's Config so bound tools
// resolve to the resumed agent — see PR #3873). Without these
// mocks the override helper throws and every resume test fails.
const stubToolRegistry = {
copyDiscoveredToolsFrom: vi.fn(),
getAllTools: vi.fn().mockReturnValue([]),
getAllToolNames: vi.fn().mockReturnValue([]),
stop: vi.fn().mockResolvedValue(undefined),
};
const config = {
storage: {
getProjectDir: () => tempDir,
Expand All @@ -72,6 +83,8 @@ describe('BackgroundAgentResumeService', () => {
getGeminiClient: () => undefined,
getSkipStartupContext: () => true,
getTranscriptPath: () => path.join(tempDir, 'session.jsonl'),
getToolRegistry: () => stubToolRegistry,
createToolRegistry: vi.fn().mockResolvedValue(stubToolRegistry),
} as unknown as Config;

return {
Expand Down
38 changes: 24 additions & 14 deletions packages/core/src/agents/background-agent-resume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import { getInitialChatHistory } from '../utils/environmentContext.js';
import { getGitBranch } from '../utils/gitUtils.js';
import { PermissionMode, type StopHookOutput } from '../hooks/types.js';
import { runWithAgentContext } from '../tools/agent/agent-context.js';
import { createApprovalModeOverride } from '../tools/agent/agent.js';
import type { ApprovalMode } from '../config/config.js';
import {
FORK_AGENT,
FORK_SUBAGENT_TYPE,
Expand Down Expand Up @@ -139,16 +141,6 @@ function reconcileResumedApprovalMode(
return 'default';
}

function createApprovalModeOverride(
base: Config,
mode: ApprovalModeValue,
): Config {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const override = Object.create(base) as any;
override.getApprovalMode = () => mode;
return override as Config;
}

function persistBackgroundCancellation(
metaPath: string,
persistedStatus: 'running' | 'cancelled',
Expand Down Expand Up @@ -527,10 +519,18 @@ export class BackgroundAgentResumeService {
parentApprovalMode,
this.config.isTrustedFolder(),
);
const agentConfig =
resolvedApprovalMode !== this.config.getApprovalMode()
? createApprovalModeOverride(this.config, resolvedApprovalMode)
: this.config;
// Always wrap, even when the resolved approval mode matches the
// parent's. The wrapper rebuilds the tool registry on the
// override Config so bound `EditTool` / `WriteFileTool` /
// `ReadFileTool` instances resolve `this.config` to the resumed
// agent and use the resumed agent's `FileReadCache`, instead of
// continuing to read the parent's. Reusing `this.config`
// directly here would short-circuit that isolation. See the
// matching wrapper in `agent.ts:createApprovalModeOverride`.
const agentConfig = await createApprovalModeOverride(
this.config,
resolvedApprovalMode as ApprovalMode,
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const bgConfig = Object.create(agentConfig) as any;
bgConfig.getShouldAvoidPermissionPrompts = () => true;
Expand Down Expand Up @@ -771,6 +771,16 @@ export class BackgroundAgentResumeService {
bgEmitter.off(AgentEventType.TOOL_CALL, onToolCall);
bgEmitter.off(AgentEventType.USAGE_METADATA, onUsageMetadata);
cleanupJsonl?.();
// Release the per-subagent ToolRegistry the resumed agent's
// wrapper Config built in `createApprovalModeOverride` so any
// AgentTool / SkillTool the model instantiated during this
// run disposes its change-listeners on shared
// SubagentManager / SkillManager. Without this, every resume
// accumulates listeners for the rest of the session.
void agentConfig
.getToolRegistry()
.stop()
.catch(() => {});
}
};

Expand Down
Loading
Loading