From 097d7dc547c3dd2df4ff894b15e54e5ebc58e0b6 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 17 May 2026 00:08:44 +0900 Subject: [PATCH] fix(background-agent): keep delegated skill, permission, and child agent across retries Three coupled gaps surfaced after the initial spawn fix: 1. fallback-retry-handler dropped task.skillContent and task.sessionPermission when rebuilding LaunchInput, so the retried background task lost the delegated system prompt and question-deny permission rule. 2. manager.startTask never bound the child sessionID to the resolved agent via setSessionAgent, leaving runtime fallback and other hooks with no idea which agent owned the new child session. 3. The fallback-to-general path in spawner.ts rebuilt the prompt body without going through buildFallbackBody, so bootstrap state, session tools, and session agent updates drifted apart. Persist skillContent and sessionPermission on BackgroundTask, bind setSessionAgent/updateSessionAgent at session creation and on fallback, and route the FALLBACK_AGENT retry through buildFallbackBody so the prompt body, bootstrap tools, and session registries all agree. --- .../fallback-retry-handler.test.ts | 24 ++- .../fallback-retry-handler.ts | 2 + src/features/background-agent/manager.test.ts | 155 ++++++++++++++---- src/features/background-agent/manager.ts | 73 +++++---- src/features/background-agent/spawner.test.ts | 6 +- src/features/background-agent/spawner.ts | 42 +++-- src/features/background-agent/types.ts | 2 + 7 files changed, 230 insertions(+), 74 deletions(-) diff --git a/src/features/background-agent/fallback-retry-handler.test.ts b/src/features/background-agent/fallback-retry-handler.test.ts index 53b7b7ef8..fc86f554f 100644 --- a/src/features/background-agent/fallback-retry-handler.test.ts +++ b/src/features/background-agent/fallback-retry-handler.test.ts @@ -1,10 +1,12 @@ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test" import { tryFallbackRetry, type FallbackRetryHandlerDeps } from "./fallback-retry-handler" import type { FallbackEntry } from "../../shared/model-requirements" +import type { ProviderModelsCache } from "../../shared/connected-providers-cache" +import { QUESTION_DENIED_SESSION_PERMISSION } from "../../shared/question-denied-session-permission" const sharedLogMock = mock(() => {}) const readConnectedProvidersCacheMock = mock(() => null) -const readProviderModelsCacheMock = mock((): { connected: string[] } | null => null) +const readProviderModelsCacheMock = mock((): ProviderModelsCache | null => null) const shouldRetryErrorMock = mock(() => true) const getNextFallbackMock = mock((chain: FallbackEntry[], attempt: number) => chain[attempt]) const hasMoreFallbacksMock = mock((chain: FallbackEntry[], attempt: number) => attempt < chain.length) @@ -258,6 +260,20 @@ describe("tryFallbackRetry", () => { expect(retryInput?.onSessionCreated).toBe(onSessionCreated) }) + test("preserves delegated launch context in retry input", async () => { + const args = createDefaultArgs({ + skillContent: "delegated skill system", + sessionPermission: QUESTION_DENIED_SESSION_PERMISSION, + }) + + await tryFallbackRetry(args) + + const key = `${args.task.model!.providerID}/${args.task.model!.modelID}` + const retryInput = args.queuesByKey.get(key)?.[0]?.input + expect(retryInput?.skillContent).toBe("delegated skill system") + expect(retryInput?.sessionPermission).toEqual(QUESTION_DENIED_SESSION_PERMISSION) + }) + test("finalizes the failed attempt, creates a new pending attempt, and enqueues its explicit attemptID", async () => { const args = createDefaultArgs({ status: "running", @@ -416,7 +432,11 @@ describe("tryFallbackRetry", () => { describe("#given disconnected fallback providers with connected preferred provider", () => { test("keeps fallback entry and selects connected preferred provider", async () => { - readProviderModelsCacheMock.mockReturnValueOnce({ connected: ["provider-a"] }) + readProviderModelsCacheMock.mockReturnValueOnce({ + connected: ["provider-a"], + models: {}, + updatedAt: new Date("2026-05-16T00:00:00.000Z").toISOString(), + }) selectFallbackProviderMock.mockImplementationOnce( (_providers: string[], preferredProviderID?: string) => preferredProviderID ?? "provider-b", ) diff --git a/src/features/background-agent/fallback-retry-handler.ts b/src/features/background-agent/fallback-retry-handler.ts index 3cf1453ef..92a92de95 100644 --- a/src/features/background-agent/fallback-retry-handler.ts +++ b/src/features/background-agent/fallback-retry-handler.ts @@ -197,6 +197,8 @@ export async function tryFallbackRetry(args: { teamRunId: task.teamRunId, model: nextModel, fallbackChain: task.fallbackChain, + skillContent: task.skillContent, + sessionPermission: task.sessionPermission, category: task.category, isUnstableAgent: task.isUnstableAgent, onSessionCreated: task.onSessionCreated, diff --git a/src/features/background-agent/manager.test.ts b/src/features/background-agent/manager.test.ts index d1fca8183..123f6e23b 100644 --- a/src/features/background-agent/manager.test.ts +++ b/src/features/background-agent/manager.test.ts @@ -1,25 +1,28 @@ -declare const require: (name: string) => any -const { describe, test, expect, beforeEach, afterEach, afterAll, spyOn, mock } = require("bun:test") - -afterAll(() => { mock.restore() }) - -import { getSessionPromptParams, clearSessionPromptParams } from "../../shared/session-prompt-params-state" import { tmpdir } from "node:os" +import { describe, test, expect, beforeEach, afterEach, afterAll, spyOn, mock } from "bun:test" import type { PluginInput } from "@opencode-ai/plugin" import * as sharedModule from "../../shared" -import { _resetForTesting as resetClaudeCodeSessionState, registerAgentName, subagentSessions } from "../claude-code-session-state" -import type { BackgroundTask, ResumeInput } from "./types" -import { MIN_IDLE_TIME_MS } from "./constants" -import { BackgroundManager } from "./manager" -import { ConcurrencyManager } from "./concurrency" -import { promptAsyncAfterSessionIdle } from "../../shared/prompt-async-gate" -import { initTaskToastManager, _resetTaskToastManagerForTesting } from "../task-toast-manager/manager" -import { _resetForTesting as resetProcessCleanupState } from "./process-cleanup" -import { clearBackgroundTaskRegistryForTesting } from "./task-registry" import { clearAllDelegatedChildSessionBootstrap, getDelegatedChildSessionBootstrap, } from "../../shared/delegated-child-session-bootstrap" +import { promptAsyncAfterSessionIdle } from "../../shared/prompt-async-gate" +import { clearSessionPromptParams, getSessionPromptParams } from "../../shared/session-prompt-params-state" +import { + getSessionAgent, + registerAgentName, + _resetForTesting as resetClaudeCodeSessionState, + subagentSessions, +} from "../claude-code-session-state" +import { _resetTaskToastManagerForTesting, initTaskToastManager } from "../task-toast-manager/manager" +import type { ConcurrencyManager } from "./concurrency" +import { MIN_IDLE_TIME_MS } from "./constants" +import { BackgroundManager } from "./manager" +import { _resetForTesting as resetProcessCleanupState } from "./process-cleanup" +import { clearBackgroundTaskRegistryForTesting } from "./task-registry" +import type { BackgroundTask, ResumeInput } from "./types" + +afterAll(() => { mock.restore() }) afterEach(() => { clearBackgroundTaskRegistryForTesting() @@ -194,6 +197,30 @@ function cast(value: unknown): T { return value as T } +async function expectRejectsWithMessage(promise: Promise, expectedMessage: string): Promise { + await promise.then( + () => { + throw new Error(`Expected promise to reject with ${expectedMessage}`) + }, + (error: unknown) => { + expect(String(error)).toContain(expectedMessage) + }, + ) +} + +async function expectResolvesDefined(promise: Promise): Promise { + const result = await promise + expect(result).toBeDefined() +} + +async function expectResolvesMatchObject( + promise: Promise, + expected: Partial, +): Promise { + const result = await promise + expect(result).toMatchObject(expected) +} + function createPluginInput(client: unknown, directory = tmpdir()): PluginInput { return cast({ client, directory }) } @@ -492,6 +519,7 @@ describe("BackgroundManager delegated child-session bootstrap", () => { queuedAt: new Date(), prompt: "background bootstrap prompt", agent: "sisyphus-junior", + skillContent: "background delegated skill system", category: "quick", model: { providerID: "anthropic", modelID: "claude-haiku-4-5" }, fallbackChain: [{ model: "gpt-5.4", providers: ["openai"], variant: "high" }], @@ -508,6 +536,7 @@ describe("BackgroundManager delegated child-session bootstrap", () => { parentTools: task.parentTools, model: task.model, fallbackChain: task.fallbackChain, + skillContent: task.skillContent, category: task.category, } @@ -519,6 +548,10 @@ describe("BackgroundManager delegated child-session bootstrap", () => { //#then expect(observedBootstrapPrompts[0]).toContain("background bootstrap prompt") + const bootstrap = getDelegatedChildSessionBootstrap("ses_background_bootstrap") + expect(bootstrap?.system).toBe("background delegated skill system") + expect(bootstrap?.tools?.question).toBe(false) + expect(bootstrap?.tools?.task).toBe(false) expect(getDelegatedChildSessionBootstrap("ses_background_bootstrap")).toBeDefined() const completed = await tryCompleteTaskForTest(manager, task) @@ -708,7 +741,13 @@ describe("BackgroundManager retry observability", () => { //#then expect(queuePendingParentWake).toHaveBeenCalledTimes(1) - const [sessionID, notification, promptContext, shouldReply] = queuePendingParentWake.mock.calls[0] + const retryingCall = cast, boolean]>>( + queuePendingParentWake.mock.calls, + )[0] + if (!retryingCall) { + throw new Error("Expected retrying parent wake call") + } + const [sessionID, notification, promptContext, shouldReply] = retryingCall expect(sessionID).toBe("parent-session") expect(promptContext).toEqual({}) expect(shouldReply).toBe(false) @@ -2840,7 +2879,7 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => { const result = manager.launch(input) // then - await expect(result).rejects.toThrow("Agent parameter is required after sanitization") + await expectRejectsWithMessage(result, "Agent parameter is required after sanitization") }) test("should initialize attempt state for a newly launched task", async () => { @@ -3162,7 +3201,7 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => { const result = manager.launch(input) // then - await expect(result).rejects.toThrow("background_task.maxDepth=3") + await expectRejectsWithMessage(result, "background_task.maxDepth=3") }) test("allows multiple descendants without a root spawn cap", async () => { @@ -3191,7 +3230,7 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => { const result = manager.launch(input) // then - await expect(result).resolves.toBeDefined() + await expectResolvesDefined(result) }) test("allows spawn assertions after reserveSubagentSpawn without a root spawn cap", async () => { @@ -3212,7 +3251,7 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => { const result = manager.assertCanSpawn("session-root") // then - await expect(result).resolves.toMatchObject({ + await expectResolvesMatchObject(result, { rootSessionID: "session-root", childDepth: 1, }) @@ -3245,7 +3284,7 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => { const result = manager.launch(input) // then - await expect(result).rejects.toThrow("background_task.maxDepth cannot be enforced safely") + await expectRejectsWithMessage(result, "background_task.maxDepth cannot be enforced safely") }) test("allows replacement launch when a queued task is cancelled before session starts", async () => { @@ -3745,7 +3784,7 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => { // Complete via internal method (session.status events go through the poller, not handleEvent) await tryCompleteTaskForTest(manager, internalTask) - await expect(manager.launch(input)).resolves.toBeDefined() + await expectResolvesDefined(manager.launch(input)) }) test("allows relaunch after running task is cancelled", async () => { @@ -3774,7 +3813,7 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => { await manager.cancelTask(task.id) - await expect(manager.launch(input)).resolves.toBeDefined() + await expectResolvesDefined(manager.launch(input)) }) test("allows relaunch after task errors", async () => { @@ -3807,7 +3846,7 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => { }) await new Promise((resolve) => setTimeout(resolve, 100)) - await expect(manager.launch(input)).resolves.toBeDefined() + await expectResolvesDefined(manager.launch(input)) }) test("allows repeated relaunch after pending tasks are cancelled", async () => { @@ -3835,8 +3874,8 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => { await manager.cancelTask(task1.id) await manager.cancelTask(task2.id) - await expect(manager.launch(input)).resolves.toBeDefined() - await expect(manager.launch(input)).resolves.toBeDefined() + await expectResolvesDefined(manager.launch(input)) + await expectResolvesDefined(manager.launch(input)) }) }) @@ -5050,10 +5089,12 @@ describe("BackgroundManager.handleEvent - session.error", () => { const mockVerifySessionExists = (manager: BackgroundManager, sessionExists: boolean): void => { verifySessionExistsSpy?.mockRestore() - verifySessionExistsSpy = spyOn( + const spy = spyOn( cast<{ verifySessionExists: (sessionID: string) => Promise }>(manager), "verifySessionExists", - ).mockResolvedValue(sessionExists) + ) + spy.mockImplementation(async () => sessionExists) + verifySessionExistsSpy = spy } const stubProcessKey = (manager: BackgroundManager) => { @@ -7072,6 +7113,62 @@ describe("BackgroundManager - tool permission spread order", () => { manager.shutdown() }) + test("startTask updates tracked session agent when launch falls back to general", async () => { + //#given + const promptCalls: Array<{ path: { id: string }; body: Record }> = [] + let promptCallCount = 0 + const client = { + session: { + get: async () => ({ data: { directory: "/test/dir" } }), + create: async () => ({ data: { id: "session-manager-fallback" } }), + promptAsync: async (args: { path: { id: string }; body: Record }) => { + promptCallCount++ + promptCalls.push(args) + if (promptCallCount === 1) { + throw new Error("Agent not found: missing-agent") + } + return {} + }, + }, + } + const manager = new BackgroundManager({ pluginContext: createPluginInput(client) }) + const task: BackgroundTask = { + id: "task-manager-fallback", + status: "pending", + queuedAt: new Date(), + description: "test task", + prompt: "test prompt", + agent: "missing-agent", + parentSessionId: "parent-session", + parentMessageId: "parent-message", + } + const input: import("./types").LaunchInput = { + description: task.description, + prompt: task.prompt, + agent: task.agent, + parentSessionId: task.parentSessionId, + parentMessageId: task.parentMessageId, + } + + try { + //#when + await (cast<{ startTask: (item: { task: BackgroundTask; input: import("./types").LaunchInput }) => Promise }>(manager)) + .startTask({ task, input }) + await new Promise((resolve) => setTimeout(resolve, 50)) + + //#then + expect(promptCalls).toHaveLength(2) + expect(promptCalls[0].body.agent).toBe("missing-agent") + expect(promptCalls[1].body.agent).toBe("general") + expect(task.agent).toBe("general") + expect(getSessionAgent("session-manager-fallback")).toBe("general") + expect(getDelegatedChildSessionBootstrap("session-manager-fallback")?.tools?.call_omo_agent).toBe(true) + } finally { + manager.shutdown() + clearAllDelegatedChildSessionBootstrap() + } + }) + test("resume respects explore agent restrictions", async () => { //#given let capturedTools: Record | undefined @@ -7216,6 +7313,7 @@ describe("BackgroundManager.launch - attempt state initialization", () => { describe("BackgroundManager attempt lifecycle bindings", () => { test("startTask binds the created session to the queued attempt ID and mirrors task projection", async () => { //#given + resetClaudeCodeSessionState() const client = { session: { get: async () => ({ data: { directory: "/test/dir" } }), @@ -7288,6 +7386,7 @@ describe("BackgroundManager attempt lifecycle bindings", () => { status: "error", error: "first attempt failed", }) + expect(getSessionAgent("session-attempt-2")).toBe("sisyphus-junior") manager.shutdown() }) diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index 9f96aa4e4..354db77bf 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -28,7 +28,7 @@ import { SessionCategoryRegistry } from "../../shared/session-category-registry" import { applySessionPromptParams } from "../../shared/session-prompt-params-helpers" import { setSessionTools } from "../../shared/session-tools-store" import { isInsideTmux } from "../../shared/tmux" -import { subagentSessions } from "../claude-code-session-state" +import { setSessionAgent, subagentSessions, updateSessionAgent } from "../claude-code-session-state" import { MESSAGE_STORAGE } from "../hook-message-injector" import { getTaskToastManager } from "../task-toast-manager" import { abortWithTimeout } from "./abort-with-timeout" @@ -574,6 +574,8 @@ export class BackgroundManager { parentTools: input.parentTools, model: input.model, fallbackChain: input.fallbackChain, + skillContent: input.skillContent, + sessionPermission: input.sessionPermission, attemptCount: 0, category: input.category, onSessionCreated: input.onSessionCreated, @@ -756,6 +758,7 @@ export class BackgroundManager { await input.onSessionCreated?.(sessionID) this.settlePreStartDescendantReservation(task) subagentSessions.add(sessionID) + setSessionAgent(sessionID, input.agent) if (this.tasks.get(task.id)?.status === "cancelled") { clearDelegatedChildSessionBootstrap(sessionID) @@ -826,12 +829,39 @@ The fallback retry session is now created and can be inspected directly. this.taskHistory.record(input.parentSessionId, { id: task.id, sessionID, agent: input.agent, description: input.description, status: "running", category: input.category, startedAt: task.startedAt }) this.startPolling() + // Fire-and-forget prompt via promptAsync (no response body needed) + // OpenCode prompt payload accepts model provider/model IDs and top-level variant only. + // Temperature/topP and provider-specific options are applied through chat.params. + const launchModel = input.model + ? { + providerID: input.model.providerID, + modelID: input.model.modelID, + } + : undefined + const launchVariant = input.model?.variant + + if (input.model) { + applySessionPromptParams(sessionID, input.model) + } + + const launchTools = { + task: false, + call_omo_agent: true, + question: false, + ...getAgentToolRestrictions(input.agent, { + includeTeamToolDenylist: input.teamRunId === undefined, + }), + } + setSessionTools(sessionID, launchTools) + log("[background-agent] Launching task:", { taskId: task.id, sessionID, agent: input.agent }) registerDelegatedChildSessionBootstrap({ sessionID, promptText: input.prompt, fallbackChain: input.fallbackChain, category: input.category, + system: input.skillContent, + tools: launchTools, modelFallbackControllerAccessor: this.modelFallbackControllerAccessor, }) @@ -848,38 +878,12 @@ The fallback retry session is now created and can be inspected directly. promptLength: input.prompt.length, }) - // Fire-and-forget prompt via promptAsync (no response body needed) - // OpenCode prompt payload accepts model provider/model IDs and top-level variant only. - // Temperature/topP and provider-specific options are applied through chat.params. - const launchModel = input.model - ? { - providerID: input.model.providerID, - modelID: input.model.modelID, - } - : undefined - const launchVariant = input.model?.variant - - if (input.model) { - applySessionPromptParams(sessionID, input.model) - } - const promptBody = { agent: input.agent, ...(launchModel ? { model: launchModel } : {}), ...(launchVariant ? { variant: launchVariant } : {}), system: input.skillContent, - tools: (() => { - const tools = { - task: false, - call_omo_agent: true, - question: false, - ...getAgentToolRestrictions(input.agent, { - includeTeamToolDenylist: input.teamRunId === undefined, - }), - } - setSessionTools(sessionID, tools) - return tools - })(), + tools: launchTools, parts: [createInternalAgentTextPart(input.prompt)], } @@ -898,7 +902,18 @@ The fallback retry session is now created and can be inspected directly. const fallbackBody = buildFallbackBody(promptBody, FALLBACK_AGENT, { includeTeamToolDenylist: input.teamRunId === undefined, }) - setSessionTools(sessionID, fallbackBody.tools as Record) + const fallbackTools = fallbackBody.tools as Record + setSessionTools(sessionID, fallbackTools) + updateSessionAgent(sessionID, FALLBACK_AGENT) + registerDelegatedChildSessionBootstrap({ + sessionID, + promptText: input.prompt, + fallbackChain: input.fallbackChain, + category: input.category, + system: input.skillContent, + tools: fallbackTools, + modelFallbackControllerAccessor: this.modelFallbackControllerAccessor, + }) await promptWithRetryInDirectory(this.client, { path: { id: sessionID }, body: fallbackBody, diff --git a/src/features/background-agent/spawner.test.ts b/src/features/background-agent/spawner.test.ts index f19c6ea7b..c1a31e01c 100644 --- a/src/features/background-agent/spawner.test.ts +++ b/src/features/background-agent/spawner.test.ts @@ -1,10 +1,10 @@ -import { describe, test, expect, mock, afterEach } from "bun:test" -import { createTask, startTask } from "./spawner" -import type { BackgroundTask } from "./types" +import { afterEach, describe, expect, mock, test } from "bun:test" import { clearSessionPromptParams, getSessionPromptParams, } from "../../shared/session-prompt-params-state" +import { createTask, startTask } from "./spawner" +import type { BackgroundTask } from "./types" describe("background-agent spawner agent-not-found fallback", () => { afterEach(() => { diff --git a/src/features/background-agent/spawner.ts b/src/features/background-agent/spawner.ts index e690c2f18..b61fe5e04 100644 --- a/src/features/background-agent/spawner.ts +++ b/src/features/background-agent/spawner.ts @@ -1,13 +1,14 @@ -import type { BackgroundTask, LaunchInput, ResumeInput } from "./types" -import type { OpencodeClient, OnSubagentSessionCreated, QueueItem } from "./constants" -import { log, getAgentToolRestrictions, createInternalAgentTextPart, promptWithRetryInDirectory } from "../../shared" +import { createInternalAgentTextPart, getAgentToolRestrictions, log, promptWithRetryInDirectory } from "../../shared" +import { stripAgentListSortPrefix } from "../../shared/agent-display-names" import { releasePromptAsyncReservation } from "../../shared/prompt-async-gate" import { applySessionPromptParams } from "../../shared/session-prompt-params-helpers" -import { subagentSessions } from "../claude-code-session-state" -import { getTaskToastManager } from "../task-toast-manager" +import { setSessionTools } from "../../shared/session-tools-store" import { isInsideTmux } from "../../shared/tmux" -import { stripAgentListSortPrefix } from "../../shared/agent-display-names" +import { setSessionAgent, subagentSessions, updateSessionAgent } from "../claude-code-session-state" +import { getTaskToastManager } from "../task-toast-manager" import type { ConcurrencyManager } from "./concurrency" +import type { OnSubagentSessionCreated, OpencodeClient, QueueItem } from "./constants" +import type { BackgroundTask, LaunchInput, ResumeInput } from "./types" export const FALLBACK_AGENT = "general" @@ -65,7 +66,13 @@ export function createTask(input: LaunchInput): BackgroundTask { teamRunId: input.teamRunId, parentModel: input.parentModel, parentAgent: input.parentAgent, + parentTools: input.parentTools, model: input.model, + fallbackChain: input.fallbackChain, + skillContent: input.skillContent, + sessionPermission: input.sessionPermission, + category: input.category, + isUnstableAgent: input.isUnstableAgent, onSessionCreated: input.onSessionCreated, } } @@ -118,6 +125,7 @@ export async function startTask( const sessionID = createResult.data.id await input.onSessionCreated?.(sessionID) subagentSessions.add(sessionID) + setSessionAgent(sessionID, input.agent) task.status = "running" task.startedAt = new Date() @@ -170,6 +178,7 @@ export async function startTask( }, parts: [createInternalAgentTextPart(input.prompt)], } + setSessionTools(sessionID, promptBody.tools) // Must fire BEFORE tmux callback: attach client needs session activity to render TUI. const promptChain = promptWithRetryInDirectory(client, { @@ -184,11 +193,15 @@ export async function startTask( }) try { releasePromptAsyncReservation(sessionID, "model-suggestion-retry") + const fallbackBody = buildFallbackBody(promptBody, FALLBACK_AGENT, { + includeTeamToolDenylist: input.teamRunId === undefined, + }) + const fallbackTools = fallbackBody.tools as Record + setSessionTools(sessionID, fallbackTools) + updateSessionAgent(sessionID, FALLBACK_AGENT) await promptWithRetryInDirectory(client, { path: { id: sessionID }, - body: buildFallbackBody(promptBody, FALLBACK_AGENT, { - includeTeamToolDenylist: input.teamRunId === undefined, - }), + body: fallbackBody, }, parentDirectory) task.agent = FALLBACK_AGENT return @@ -310,6 +323,7 @@ export async function resumeTask( }, parts: [createInternalAgentTextPart(input.prompt)], } + setSessionTools(sessionID, resumeBody.tools) promptWithRetryInDirectory(client, { path: { id: sessionID }, @@ -323,11 +337,15 @@ export async function resumeTask( }) try { releasePromptAsyncReservation(sessionID, "model-suggestion-retry") + const fallbackBody = buildFallbackBody(resumeBody, FALLBACK_AGENT, { + includeTeamToolDenylist: task.teamRunId === undefined, + }) + const fallbackTools = fallbackBody.tools as Record + setSessionTools(sessionID, fallbackTools) + updateSessionAgent(sessionID, FALLBACK_AGENT) await promptWithRetryInDirectory(client, { path: { id: sessionID }, - body: buildFallbackBody(resumeBody, FALLBACK_AGENT, { - includeTeamToolDenylist: task.teamRunId === undefined, - }), + body: fallbackBody, }, directory) task.agent = FALLBACK_AGENT return diff --git a/src/features/background-agent/types.ts b/src/features/background-agent/types.ts index 76c242a41..f864a3fa3 100644 --- a/src/features/background-agent/types.ts +++ b/src/features/background-agent/types.ts @@ -73,6 +73,8 @@ export interface BackgroundTask { parentAgent?: string /** Parent session's tool restrictions for notification prompts */ parentTools?: Record + skillContent?: string + sessionPermission?: SessionPermissionRule[] /** Marks if the task was launched from an unstable agent/category */ isUnstableAgent?: boolean /** Category used for this task (e.g., 'quick', 'visual-engineering') */