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
@@ -0,0 +1,71 @@
import type { ModelFallbackControllerAccessor } from "../hooks/model-fallback"
import { createInternalAgentTextPart } from "./internal-initiator-marker"
import type { FallbackEntry } from "./model-requirements"
import { SessionCategoryRegistry } from "./session-category-registry"
export type DelegatedChildSessionRetryPart = {
type: "text"
text: string
}
export type DelegatedChildSessionBootstrap = {
retryParts: DelegatedChildSessionRetryPart[]
fallbackChain?: FallbackEntry[]
category?: string
}
const delegatedChildSessionBootstraps = new Map<string, DelegatedChildSessionBootstrap>()
function cloneRetryParts(parts: DelegatedChildSessionRetryPart[]): DelegatedChildSessionRetryPart[] {
return parts.map((part) => ({ type: part.type, text: part.text }))
}
function cloneFallbackChain(fallbackChain: FallbackEntry[] | undefined): FallbackEntry[] | undefined {
return fallbackChain?.map((entry) => ({
...entry,
providers: [...entry.providers],
}))
}
export function registerDelegatedChildSessionBootstrap(_args: {
sessionID: string
promptText: string
fallbackChain?: FallbackEntry[]
category?: string
modelFallbackControllerAccessor?: ModelFallbackControllerAccessor
}): void {
const retryParts = [createInternalAgentTextPart(_args.promptText)]
const fallbackChain = cloneFallbackChain(_args.fallbackChain)
delegatedChildSessionBootstraps.set(_args.sessionID, {
retryParts,
...(fallbackChain ? { fallbackChain } : {}),
...(_args.category ? { category: _args.category } : {}),
})
_args.modelFallbackControllerAccessor?.setSessionFallbackChain(_args.sessionID, fallbackChain)
if (_args.category) {
SessionCategoryRegistry.register(_args.sessionID, _args.category)
}
}
export function getDelegatedChildSessionBootstrap(_sessionID: string): DelegatedChildSessionBootstrap | undefined {
const bootstrap = delegatedChildSessionBootstraps.get(_sessionID)
if (!bootstrap) {
return undefined
}
const fallbackChain = cloneFallbackChain(bootstrap.fallbackChain)
return {
retryParts: cloneRetryParts(bootstrap.retryParts),
...(fallbackChain ? { fallbackChain } : {}),
...(bootstrap.category ? { category: bootstrap.category } : {}),
}
}
export function clearDelegatedChildSessionBootstrap(_sessionID: string): void {
delegatedChildSessionBootstraps.delete(_sessionID)
}
export function clearAllDelegatedChildSessionBootstrap(): void {
delegatedChildSessionBootstraps.clear()
}
+8 -6
View File
@@ -217,12 +217,13 @@ export async function promptAsyncAfterSessionIdle<TInput = PromptAsyncInput>(arg
} = args
const postDispatchHoldMs = args.postDispatchHoldMs ?? DEFAULT_PROMPT_ASYNC_POST_DISPATCH_HOLD_MS
const dispatchTimeoutMs = args.dispatchTimeoutMs ?? DEFAULT_PROMPT_DISPATCH_TIMEOUT_MS
const promptAsync = client.session?.promptAsync
const session = client.session
if (typeof promptAsync !== "function") {
if (typeof session?.promptAsync !== "function") {
log("[prompt-async-gate] promptAsync unavailable", { sessionID, source })
return { status: "unavailable" }
}
const dispatchPromptAsync = session.promptAsync.bind(session)
return dispatchAfterSessionIdle({
sessionName: "promptAsync",
@@ -234,7 +235,7 @@ export async function promptAsyncAfterSessionIdle<TInput = PromptAsyncInput>(arg
postDispatchHoldMs,
dispatchTimeoutMs,
checkStatus: args.checkStatus !== false,
dispatch: (dispatchInput) => promptAsync(dispatchInput),
dispatch: (dispatchInput) => dispatchPromptAsync(dispatchInput),
})
}
@@ -257,12 +258,13 @@ export async function promptAfterSessionIdle<TInput = PromptAsyncInput>(args: {
} = args
const postDispatchHoldMs = args.postDispatchHoldMs ?? DEFAULT_PROMPT_ASYNC_POST_DISPATCH_HOLD_MS
const dispatchTimeoutMs = args.dispatchTimeoutMs ?? DEFAULT_PROMPT_DISPATCH_TIMEOUT_MS
const prompt = client.session?.prompt
const session = client.session
if (typeof prompt !== "function") {
if (typeof session?.prompt !== "function") {
log("[prompt-async-gate] prompt unavailable", { sessionID, source })
return { status: "unavailable" }
}
const dispatchPrompt = session.prompt.bind(session)
return dispatchAfterSessionIdle({
sessionName: "prompt",
@@ -274,7 +276,7 @@ export async function promptAfterSessionIdle<TInput = PromptAsyncInput>(args: {
postDispatchHoldMs,
dispatchTimeoutMs,
checkStatus: args.checkStatus !== false,
dispatch: (dispatchInput) => prompt(dispatchInput),
dispatch: (dispatchInput) => dispatchPrompt(dispatchInput),
})
}