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') */