fix(delegate-task): start child prompts reliably

Preserve delegated child prompt/bootstrap metadata for early runtime fallback before OpenCode has persisted the first user turn. Bind prompt gate calls to the SDK session receiver and keep completed background task lookup visible across plugin manager instances.
This commit is contained in:
YeonGyu-Kim
2026-05-16 16:21:48 +09:00
parent 76e573a920
commit 982fa81367
11 changed files with 742 additions and 88 deletions
+60
View File
@@ -27,6 +27,8 @@ describe("executeSyncTask - cleanup on error paths", () => {
addTaskCalls = []
deleteCalls = []
addCalls = []
const { clearAllDelegatedChildSessionBootstrap } = require("../../shared/delegated-child-session-bootstrap")
clearAllDelegatedChildSessionBootstrap()
clearRequireCache("./sync-task")
@@ -62,6 +64,8 @@ describe("executeSyncTask - cleanup on error paths", () => {
mock.restore()
resetToastManager?.()
resetToastManager = null
const { clearAllDelegatedChildSessionBootstrap } = require("../../shared/delegated-child-session-bootstrap")
clearAllDelegatedChildSessionBootstrap()
})
test("cleans up toast and subagentSessions when fetchSyncResult returns ok: false", async () => {
@@ -664,6 +668,62 @@ describe("executeSyncTask - cleanup on error paths", () => {
})
})
test("registers child-session bootstrap before sync prompt and clears it after completion", async () => {
const mockClient = {
session: {
create: async () => ({ data: { id: "ignored" } }),
},
}
const { executeSyncTask } = require("./sync-task")
const { getDelegatedChildSessionBootstrap } = require("../../shared/delegated-child-session-bootstrap")
const observedBootstrapPrompts: string[] = []
const deps = {
createSyncSession: async () => ({ ok: true as const, sessionID: "ses_bootstrap_sync" }),
sendSyncPrompt: async (_client: unknown, input: { sessionID: string }) => {
const bootstrap = getDelegatedChildSessionBootstrap(input.sessionID)
observedBootstrapPrompts.push(bootstrap?.retryParts[0]?.text ?? "")
return null
},
pollSyncSession: async () => null,
fetchSyncResult: async () => ({ ok: true as const, textContent: "sync result" }),
}
const mockCtx = {
sessionID: "parent-session",
callID: "call-123",
metadata: () => {},
}
const mockExecutorCtx = {
client: mockClient,
directory: "/tmp",
onSyncSessionCreated: null,
modelFallbackControllerAccessor: {
setSessionFallbackChain: () => {},
clearSessionFallbackChain: () => {},
},
}
const args = {
prompt: "sync bootstrap prompt",
description: "sync bootstrap task",
category: "quick",
load_skills: [],
run_in_background: false,
command: null,
}
const result = await executeSyncTask(args, mockCtx, mockExecutorCtx, {
sessionID: "parent-session",
}, "sisyphus-junior", undefined, undefined, undefined, undefined, deps)
expect(result).toContain("sync result")
expect(observedBootstrapPrompts[0]).toContain("sync bootstrap prompt")
expect(getDelegatedChildSessionBootstrap("ses_bootstrap_sync")).toBeUndefined()
})
test("replays sync session side effects for retry-created sessions", async () => {
const mockClient = {
session: {
+26 -17
View File
@@ -1,19 +1,24 @@
import type { ModelFallbackInfo } from "../../features/task-toast-manager/types"
import type { DelegateTaskArgs, ToolContextWithMetadata, DelegatedModelConfig } from "./types"
import type { ExecutorContext, ParentContext } from "./executor-types"
import { setSessionAgent, subagentSessions, syncSubagentSessions } from "../../features/claude-code-session-state"
import { getTaskToastManager } from "../../features/task-toast-manager"
import type { ModelFallbackInfo } from "../../features/task-toast-manager/types"
import { publishToolMetadata } from "../../features/tool-metadata-store"
import { subagentSessions, syncSubagentSessions, setSessionAgent } from "../../features/claude-code-session-state"
import { log } from "../../shared/logger"
import { SessionCategoryRegistry } from "../../shared/session-category-registry"
import { formatDuration } from "./time-formatter"
import { formatDetailedError } from "./error-formatting"
import { syncTaskDeps, type SyncTaskDeps } from "./sync-task-deps"
import { getNextSyncFallbackModel, retrySyncPromptWithFallbacks } from "./sync-task-fallback"
import { buildTaskMetadataBlock } from "../../features/tool-metadata-store/task-metadata-contract"
import { resolveMetadataModel } from "./resolve-metadata-model"
import { shouldRetryError } from "../../shared/model-error-classifier"
import type { ModelFallbackState } from "../../hooks/model-fallback/hook"
import {
clearDelegatedChildSessionBootstrap,
registerDelegatedChildSessionBootstrap,
} from "../../shared/delegated-child-session-bootstrap"
import { log } from "../../shared/logger"
import { shouldRetryError } from "../../shared/model-error-classifier"
import { SessionCategoryRegistry } from "../../shared/session-category-registry"
import { formatDetailedError } from "./error-formatting"
import type { ExecutorContext, ParentContext } from "./executor-types"
import { buildTaskPrompt } from "./prompt-builder"
import { resolveMetadataModel } from "./resolve-metadata-model"
import { type SyncTaskDeps, syncTaskDeps } from "./sync-task-deps"
import { getNextSyncFallbackModel, retrySyncPromptWithFallbacks } from "./sync-task-fallback"
import { formatDuration } from "./time-formatter"
import type { DelegatedModelConfig, DelegateTaskArgs, ToolContextWithMetadata } from "./types"
function shouldAttemptPollErrorRecovery(pollError: string): boolean {
const trimmed = pollError.trim()
@@ -107,11 +112,13 @@ export async function executeSyncTask(
subagentSessions.add(newSessionID)
syncSubagentSessions.add(newSessionID)
setSessionAgent(newSessionID, agentToUse)
executorCtx.modelFallbackControllerAccessor?.setSessionFallbackChain(newSessionID, fallbackChain)
if (args.category) {
SessionCategoryRegistry.register(newSessionID, args.category)
}
registerDelegatedChildSessionBootstrap({
sessionID: newSessionID,
promptText: buildTaskPrompt(args.prompt, agentToUse, executorCtx.sisyphusAgentConfig?.tdd),
fallbackChain,
category: args.category,
modelFallbackControllerAccessor: executorCtx.modelFallbackControllerAccessor,
})
if (onSyncSessionCreated) {
log("[task] Invoking onSyncSessionCreated callback", { sessionID: newSessionID, parentID: parentContext.sessionID })
@@ -199,6 +206,7 @@ export async function executeSyncTask(
const cleanupRetrySession = (currentSessionID: string): void => {
subagentSessions.delete(currentSessionID)
syncSubagentSessions.delete(currentSessionID)
clearDelegatedChildSessionBootstrap(currentSessionID)
executorCtx.modelFallbackControllerAccessor?.clearSessionFallbackChain(currentSessionID)
SessionCategoryRegistry.remove(currentSessionID)
}
@@ -364,6 +372,7 @@ ${buildTaskMetadataBlock({
if (syncSessionID) {
subagentSessions.delete(syncSessionID)
syncSubagentSessions.delete(syncSessionID)
clearDelegatedChildSessionBootstrap(syncSessionID)
executorCtx.modelFallbackControllerAccessor?.clearSessionFallbackChain(syncSessionID)
SessionCategoryRegistry.remove(syncSessionID)
}