fix(background-agent): preserve parent agent on retry wakes
Background fallback retry notifications were queued as bare internal user messages, so OpenCode could treat the notification as a new default-agent turn. Reuse the same parent prompt context resolver used by completion notifications for retrying and retry-ready wakes, and pin regression coverage for Hephaestus parent sessions plus missing-context fallbacks.
This commit is contained in:
@@ -697,6 +697,19 @@ describe("BackgroundManager retry observability", () => {
|
|||||||
//#given
|
//#given
|
||||||
const client = {
|
const client = {
|
||||||
session: {
|
session: {
|
||||||
|
messages: async () => [
|
||||||
|
{
|
||||||
|
info: {
|
||||||
|
agent: "hephaestus",
|
||||||
|
model: {
|
||||||
|
providerID: "openai",
|
||||||
|
modelID: "gpt-5",
|
||||||
|
variant: "xhigh",
|
||||||
|
},
|
||||||
|
tools: { bash: "allow", edit: "deny" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
abort: async () => ({}),
|
abort: async () => ({}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -749,7 +762,12 @@ describe("BackgroundManager retry observability", () => {
|
|||||||
}
|
}
|
||||||
const [sessionID, notification, promptContext, shouldReply] = retryingCall
|
const [sessionID, notification, promptContext, shouldReply] = retryingCall
|
||||||
expect(sessionID).toBe("parent-session")
|
expect(sessionID).toBe("parent-session")
|
||||||
expect(promptContext).toEqual({})
|
expect(promptContext).toEqual({
|
||||||
|
agent: "hephaestus",
|
||||||
|
model: { providerID: "openai", modelID: "gpt-5" },
|
||||||
|
variant: "xhigh",
|
||||||
|
tools: { bash: true, edit: false },
|
||||||
|
})
|
||||||
expect(shouldReply).toBe(false)
|
expect(shouldReply).toBe(false)
|
||||||
expect(notification).toContain("[BACKGROUND TASK RETRYING]")
|
expect(notification).toContain("[BACKGROUND TASK RETRYING]")
|
||||||
expect(notification).toContain("ses_retry_visibility")
|
expect(notification).toContain("ses_retry_visibility")
|
||||||
@@ -757,6 +775,123 @@ describe("BackgroundManager retry observability", () => {
|
|||||||
expect(notification).toContain("anthropic/claude-haiku-4.5")
|
expect(notification).toContain("anthropic/claude-haiku-4.5")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("falls back to task parent agent when retrying wake cannot load parent messages", async () => {
|
||||||
|
//#given
|
||||||
|
const client = {
|
||||||
|
session: {
|
||||||
|
messages: async () => {
|
||||||
|
throw new Error("parent messages unavailable")
|
||||||
|
},
|
||||||
|
abort: async () => ({}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const manager = new BackgroundManager({ pluginContext: createPluginInput(client) })
|
||||||
|
const task = createMockTask({
|
||||||
|
id: "bg_retry_parent_agent_fallback",
|
||||||
|
parentSessionId: "parent-session-agent-fallback",
|
||||||
|
parentAgent: "hephaestus",
|
||||||
|
parentTools: { bash: true },
|
||||||
|
fallbackChain: [{ model: "claude-haiku-4-5", providers: ["anthropic"] }],
|
||||||
|
attemptCount: 0,
|
||||||
|
status: "running",
|
||||||
|
attempts: [
|
||||||
|
{
|
||||||
|
attemptId: "att_retry_parent_agent_fallback",
|
||||||
|
attemptNumber: 1,
|
||||||
|
sessionId: "ses_retry_parent_agent_fallback",
|
||||||
|
providerId: "genai-proxy-openai",
|
||||||
|
modelId: "gpt-5.4-mini",
|
||||||
|
status: "running",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
currentAttemptID: "att_retry_parent_agent_fallback",
|
||||||
|
})
|
||||||
|
getTaskMap(manager).set(task.id, task)
|
||||||
|
const queuePendingParentWake = mock(() => {})
|
||||||
|
;(cast<{
|
||||||
|
queuePendingParentWake: (
|
||||||
|
sessionId: string,
|
||||||
|
notification: string,
|
||||||
|
promptContext: Record<string, unknown>,
|
||||||
|
shouldReply: boolean,
|
||||||
|
delayMs?: number,
|
||||||
|
) => void
|
||||||
|
}>(manager)).queuePendingParentWake = queuePendingParentWake
|
||||||
|
|
||||||
|
//#when
|
||||||
|
await (cast<{
|
||||||
|
tryFallbackRetry: (task: BackgroundTask, errorInfo: { name?: string; message?: string }, source: string) => Promise<boolean>
|
||||||
|
}>(manager)).tryFallbackRetry(task, {
|
||||||
|
name: "APIError",
|
||||||
|
message: "Forbidden: Selected provider is forbidden",
|
||||||
|
}, "promptAsync.launch")
|
||||||
|
|
||||||
|
//#then
|
||||||
|
const retryingCall = cast<Array<[string, string, Record<string, unknown>, boolean]>>(
|
||||||
|
queuePendingParentWake.mock.calls,
|
||||||
|
)[0]
|
||||||
|
expect(retryingCall?.[2]).toEqual({
|
||||||
|
agent: "hephaestus",
|
||||||
|
tools: { bash: true },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("does not invent a parent agent when retrying wake has no context source", async () => {
|
||||||
|
//#given
|
||||||
|
const client = {
|
||||||
|
session: {
|
||||||
|
messages: async () => {
|
||||||
|
throw new Error("parent messages unavailable")
|
||||||
|
},
|
||||||
|
abort: async () => ({}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const manager = new BackgroundManager({ pluginContext: createPluginInput(client) })
|
||||||
|
const task = createMockTask({
|
||||||
|
id: "bg_retry_no_parent_context",
|
||||||
|
parentSessionId: "parent-session-no-context",
|
||||||
|
fallbackChain: [{ model: "claude-haiku-4-5", providers: ["anthropic"] }],
|
||||||
|
attemptCount: 0,
|
||||||
|
status: "running",
|
||||||
|
attempts: [
|
||||||
|
{
|
||||||
|
attemptId: "att_retry_no_parent_context",
|
||||||
|
attemptNumber: 1,
|
||||||
|
sessionId: "ses_retry_no_parent_context",
|
||||||
|
providerId: "genai-proxy-openai",
|
||||||
|
modelId: "gpt-5.4-mini",
|
||||||
|
status: "running",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
currentAttemptID: "att_retry_no_parent_context",
|
||||||
|
})
|
||||||
|
getTaskMap(manager).set(task.id, task)
|
||||||
|
const queuePendingParentWake = mock(() => {})
|
||||||
|
;(cast<{
|
||||||
|
queuePendingParentWake: (
|
||||||
|
sessionId: string,
|
||||||
|
notification: string,
|
||||||
|
promptContext: Record<string, unknown>,
|
||||||
|
shouldReply: boolean,
|
||||||
|
delayMs?: number,
|
||||||
|
) => void
|
||||||
|
}>(manager)).queuePendingParentWake = queuePendingParentWake
|
||||||
|
|
||||||
|
//#when
|
||||||
|
await (cast<{
|
||||||
|
tryFallbackRetry: (task: BackgroundTask, errorInfo: { name?: string; message?: string }, source: string) => Promise<boolean>
|
||||||
|
}>(manager)).tryFallbackRetry(task, {
|
||||||
|
name: "APIError",
|
||||||
|
message: "Forbidden: Selected provider is forbidden",
|
||||||
|
}, "promptAsync.launch")
|
||||||
|
|
||||||
|
//#then
|
||||||
|
const retryingCall = cast<Array<[string, string, Record<string, unknown>, boolean]>>(
|
||||||
|
queuePendingParentWake.mock.calls,
|
||||||
|
)[0]
|
||||||
|
expect(retryingCall?.[2]).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
test("queues a second parent-visible notification once the retry session ID is created", async () => {
|
test("queues a second parent-visible notification once the retry session ID is created", async () => {
|
||||||
//#given
|
//#given
|
||||||
const queuePendingParentWake = mock(() => {})
|
const queuePendingParentWake = mock(() => {})
|
||||||
@@ -764,6 +899,19 @@ describe("BackgroundManager retry observability", () => {
|
|||||||
session: {
|
session: {
|
||||||
get: async () => ({ data: { directory: tmpdir() } }),
|
get: async () => ({ data: { directory: tmpdir() } }),
|
||||||
create: async () => ({ data: { id: "ses_retry_created" } }),
|
create: async () => ({ data: { id: "ses_retry_created" } }),
|
||||||
|
messages: async () => [
|
||||||
|
{
|
||||||
|
info: {
|
||||||
|
agent: "hephaestus",
|
||||||
|
model: {
|
||||||
|
providerID: "openai",
|
||||||
|
modelID: "gpt-5",
|
||||||
|
variant: "xhigh",
|
||||||
|
},
|
||||||
|
tools: { bash: "allow", edit: "deny" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
promptAsync: async () => ({}),
|
promptAsync: async () => ({}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -837,12 +985,18 @@ describe("BackgroundManager retry observability", () => {
|
|||||||
}>(manager)).startTask(item)
|
}>(manager)).startTask(item)
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
const notifications = cast<Array<[string, string, Record<string, unknown>, boolean, number | undefined]>>(
|
const retryReadyCall = cast<Array<[string, string, Record<string, unknown>, boolean, number | undefined]>>(
|
||||||
queuePendingParentWake.mock.calls,
|
queuePendingParentWake.mock.calls,
|
||||||
).map((call) => call[1])
|
).find((call) => call[1].includes("[BACKGROUND TASK RETRY SESSION READY]"))
|
||||||
const retryReadyNotification = notifications.find((notification) => notification.includes("[BACKGROUND TASK RETRY SESSION READY]"))
|
const retryReadyNotification = retryReadyCall?.[1]
|
||||||
const expectedRetryLink = `http://127.0.0.1:4096/${Buffer.from(tmpdir()).toString("base64url")}/session/ses_retry_created`
|
const expectedRetryLink = `http://127.0.0.1:4096/${Buffer.from(tmpdir()).toString("base64url")}/session/ses_retry_created`
|
||||||
expect(retryReadyNotification).toBeDefined()
|
expect(retryReadyNotification).toBeDefined()
|
||||||
|
expect(retryReadyCall?.[2]).toEqual({
|
||||||
|
agent: "hephaestus",
|
||||||
|
model: { providerID: "openai", modelID: "gpt-5" },
|
||||||
|
variant: "xhigh",
|
||||||
|
tools: { bash: true, edit: false },
|
||||||
|
})
|
||||||
expect(retryReadyNotification).toContain("**Retry attempt:** 2")
|
expect(retryReadyNotification).toContain("**Retry attempt:** 2")
|
||||||
expect(retryReadyNotification).toContain("ses_retry_created")
|
expect(retryReadyNotification).toContain("ses_retry_created")
|
||||||
expect(retryReadyNotification).toContain(expectedRetryLink)
|
expect(retryReadyNotification).toContain(expectedRetryLink)
|
||||||
|
|||||||
@@ -818,6 +818,7 @@ export class BackgroundManager {
|
|||||||
? `\n- Error: ${failedError}`
|
? `\n- Error: ${failedError}`
|
||||||
: ""
|
: ""
|
||||||
const retryModel = formatAttemptModelSummary(boundAttempt) ?? task.retryNotification.nextModel
|
const retryModel = formatAttemptModelSummary(boundAttempt) ?? task.retryNotification.nextModel
|
||||||
|
const parentPromptContext = await this.resolveParentWakePromptContext(task)
|
||||||
this.queuePendingParentWake(
|
this.queuePendingParentWake(
|
||||||
task.parentSessionId,
|
task.parentSessionId,
|
||||||
`<system-reminder>
|
`<system-reminder>
|
||||||
@@ -830,7 +831,7 @@ export class BackgroundManager {
|
|||||||
|
|
||||||
The fallback retry session is now created and can be inspected directly.
|
The fallback retry session is now created and can be inspected directly.
|
||||||
</system-reminder>`,
|
</system-reminder>`,
|
||||||
{},
|
parentPromptContext,
|
||||||
false,
|
false,
|
||||||
PENDING_PARENT_WAKE_DEBOUNCE_MS,
|
PENDING_PARENT_WAKE_DEBOUNCE_MS,
|
||||||
)
|
)
|
||||||
@@ -1920,6 +1921,7 @@ The fallback retry session is now created and can be inspected directly.
|
|||||||
source: string,
|
source: string,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const previousSessionID = task.sessionId
|
const previousSessionID = task.sessionId
|
||||||
|
let retryingNotification: string | undefined
|
||||||
const result = tryFallbackRetry({
|
const result = tryFallbackRetry({
|
||||||
task,
|
task,
|
||||||
errorInfo,
|
errorInfo,
|
||||||
@@ -1938,22 +1940,26 @@ The fallback retry session is now created and can be inspected directly.
|
|||||||
const failedModelLine = failedModel ? `\n- Failed model: \`${failedModel}\`` : ""
|
const failedModelLine = failedModel ? `\n- Failed model: \`${failedModel}\`` : ""
|
||||||
const failedErrorLine = previousAttempt?.error ? `\n- Error: ${previousAttempt.error}` : ""
|
const failedErrorLine = previousAttempt?.error ? `\n- Error: ${previousAttempt.error}` : ""
|
||||||
const nextModel = formatAttemptModelSummary(currentAttempt)
|
const nextModel = formatAttemptModelSummary(currentAttempt)
|
||||||
this.queuePendingParentWake(
|
retryingNotification = `<system-reminder>
|
||||||
task.parentSessionId,
|
|
||||||
`<system-reminder>
|
|
||||||
[BACKGROUND TASK RETRYING]
|
[BACKGROUND TASK RETRYING]
|
||||||
**ID:** \`${task.id}\`
|
**ID:** \`${task.id}\`
|
||||||
**Description:** ${task.description}${sourceText}${failedSessionLine}${failedModelLine}${failedErrorLine}${nextModel ? `\n- Next model: \`${nextModel}\`` : ""}
|
**Description:** ${task.description}${sourceText}${failedSessionLine}${failedModelLine}${failedErrorLine}${nextModel ? `\n- Next model: \`${nextModel}\`` : ""}
|
||||||
|
|
||||||
The task was re-queued on a fallback model after a retryable failure.
|
The task was re-queued on a fallback model after a retryable failure.
|
||||||
</system-reminder>`,
|
</system-reminder>`
|
||||||
{},
|
|
||||||
false,
|
|
||||||
PENDING_PARENT_WAKE_DEBOUNCE_MS,
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const retried = await result
|
const retried = await result
|
||||||
|
if (retried && retryingNotification) {
|
||||||
|
const parentPromptContext = await this.resolveParentWakePromptContext(task)
|
||||||
|
this.queuePendingParentWake(
|
||||||
|
task.parentSessionId,
|
||||||
|
retryingNotification,
|
||||||
|
parentPromptContext,
|
||||||
|
false,
|
||||||
|
PENDING_PARENT_WAKE_DEBOUNCE_MS,
|
||||||
|
)
|
||||||
|
}
|
||||||
if (retried && previousSessionID) {
|
if (retried && previousSessionID) {
|
||||||
this.clearSessionOutputObserved(previousSessionID)
|
this.clearSessionOutputObserved(previousSessionID)
|
||||||
this.clearSessionTodoObservation(previousSessionID)
|
this.clearSessionTodoObservation(previousSessionID)
|
||||||
@@ -2412,76 +2418,18 @@ The task was re-queued on a fallback model after a retryable failure.
|
|||||||
completedTasks,
|
completedTasks,
|
||||||
})
|
})
|
||||||
|
|
||||||
let agent: string | undefined = task.parentAgent
|
|
||||||
let model: { providerID: string; modelID: string } | undefined
|
|
||||||
let tools: Record<string, boolean> | undefined = task.parentTools
|
|
||||||
let promptContext: ReturnType<typeof resolvePromptContextFromSessionMessages> = null
|
|
||||||
|
|
||||||
if (this.enableParentSessionNotifications) {
|
if (this.enableParentSessionNotifications) {
|
||||||
try {
|
const parentPromptContext = await this.resolveParentWakePromptContext(task)
|
||||||
const messagesResp = await messagesInDirectory(this.client, {
|
|
||||||
path: { id: task.parentSessionId },
|
|
||||||
}, this.directory)
|
|
||||||
const messages = normalizeSDKResponse(messagesResp, [] as Array<{
|
|
||||||
info?: {
|
|
||||||
agent?: string
|
|
||||||
model?: { providerID: string; modelID: string }
|
|
||||||
modelID?: string
|
|
||||||
providerID?: string
|
|
||||||
tools?: Record<string, boolean | "allow" | "deny" | "ask">
|
|
||||||
}
|
|
||||||
}>)
|
|
||||||
promptContext = resolvePromptContextFromSessionMessages(
|
|
||||||
messages,
|
|
||||||
task.parentSessionId,
|
|
||||||
)
|
|
||||||
const normalizedTools = isRecord(promptContext?.tools)
|
|
||||||
? normalizePromptTools(promptContext.tools)
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
if (promptContext?.agent || promptContext?.model || normalizedTools) {
|
|
||||||
agent = promptContext?.agent ?? task.parentAgent
|
|
||||||
model = promptContext?.model?.providerID && promptContext.model.modelID
|
|
||||||
? { providerID: promptContext.model.providerID, modelID: promptContext.model.modelID }
|
|
||||||
: undefined
|
|
||||||
tools = normalizedTools ?? tools
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (isAbortedSessionError(error)) {
|
|
||||||
log("[background-agent] Parent session aborted while loading messages; using messageDir fallback:", {
|
|
||||||
taskId: task.id,
|
|
||||||
parentSessionID: task.parentSessionId,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const messageDir = join(MESSAGE_STORAGE, task.parentSessionId)
|
|
||||||
const currentMessage = messageDir
|
|
||||||
? findNearestMessageExcludingCompaction(messageDir, task.parentSessionId)
|
|
||||||
: null
|
|
||||||
agent = currentMessage?.agent ?? task.parentAgent
|
|
||||||
model = currentMessage?.model?.providerID && currentMessage?.model?.modelID
|
|
||||||
? { providerID: currentMessage.model.providerID, modelID: currentMessage.model.modelID }
|
|
||||||
: undefined
|
|
||||||
tools = normalizePromptTools(currentMessage?.tools) ?? tools
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolvedTools = resolveInheritedPromptTools(task.parentSessionId, tools)
|
|
||||||
|
|
||||||
log("[background-agent] notifyParentSession context:", {
|
log("[background-agent] notifyParentSession context:", {
|
||||||
taskId: task.id,
|
taskId: task.id,
|
||||||
resolvedAgent: agent,
|
resolvedAgent: parentPromptContext.agent,
|
||||||
resolvedModel: model,
|
resolvedModel: parentPromptContext.model,
|
||||||
})
|
})
|
||||||
|
|
||||||
const isTaskFailure = task.status === "error" || task.status === "cancelled" || task.status === "interrupt"
|
const isTaskFailure = task.status === "error" || task.status === "cancelled" || task.status === "interrupt"
|
||||||
const shouldReply = allComplete || isTaskFailure
|
const shouldReply = allComplete || isTaskFailure
|
||||||
|
|
||||||
const variant = promptContext?.model?.variant
|
|
||||||
const parentPromptContext: ParentWakePromptContext = {
|
|
||||||
...(agent !== undefined ? { agent } : {}),
|
|
||||||
...(model !== undefined ? { model } : {}),
|
|
||||||
...(variant !== undefined ? { variant } : {}),
|
|
||||||
...(resolvedTools ? { tools: resolvedTools } : {}),
|
|
||||||
}
|
|
||||||
const shouldDeferNotification = await this.isSessionActive(task.parentSessionId)
|
const shouldDeferNotification = await this.isSessionActive(task.parentSessionId)
|
||||||
|
|
||||||
if (shouldDeferNotification) {
|
if (shouldDeferNotification) {
|
||||||
@@ -2519,6 +2467,69 @@ The task was re-queued on a fallback model after a retryable failure.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async resolveParentWakePromptContext(task: BackgroundTask): Promise<ParentWakePromptContext> {
|
||||||
|
let agent: string | undefined = task.parentAgent
|
||||||
|
let model: { providerID: string; modelID: string } | undefined
|
||||||
|
let tools: Record<string, boolean> | undefined = task.parentTools
|
||||||
|
let variant: string | undefined
|
||||||
|
|
||||||
|
try {
|
||||||
|
const messagesResp = await messagesInDirectory(this.client, {
|
||||||
|
path: { id: task.parentSessionId },
|
||||||
|
}, this.directory)
|
||||||
|
const messages = normalizeSDKResponse(messagesResp, [] as Array<{
|
||||||
|
info?: {
|
||||||
|
agent?: string
|
||||||
|
model?: { providerID: string; modelID: string; variant?: string }
|
||||||
|
modelID?: string
|
||||||
|
providerID?: string
|
||||||
|
tools?: Record<string, boolean | "allow" | "deny" | "ask">
|
||||||
|
}
|
||||||
|
}>)
|
||||||
|
const promptContext = resolvePromptContextFromSessionMessages(
|
||||||
|
messages,
|
||||||
|
task.parentSessionId,
|
||||||
|
)
|
||||||
|
const normalizedTools = isRecord(promptContext?.tools)
|
||||||
|
? normalizePromptTools(promptContext.tools)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
if (promptContext?.agent || promptContext?.model || normalizedTools) {
|
||||||
|
agent = promptContext?.agent ?? task.parentAgent
|
||||||
|
model = promptContext?.model?.providerID && promptContext.model.modelID
|
||||||
|
? { providerID: promptContext.model.providerID, modelID: promptContext.model.modelID }
|
||||||
|
: undefined
|
||||||
|
variant = promptContext?.model?.variant
|
||||||
|
tools = normalizedTools ?? tools
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (isAbortedSessionError(error)) {
|
||||||
|
log("[background-agent] Parent session aborted while loading messages; using messageDir fallback:", {
|
||||||
|
taskId: task.id,
|
||||||
|
parentSessionID: task.parentSessionId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const messageDir = join(MESSAGE_STORAGE, task.parentSessionId)
|
||||||
|
const currentMessage = messageDir
|
||||||
|
? findNearestMessageExcludingCompaction(messageDir, task.parentSessionId)
|
||||||
|
: null
|
||||||
|
agent = currentMessage?.agent ?? task.parentAgent
|
||||||
|
model = currentMessage?.model?.providerID && currentMessage?.model?.modelID
|
||||||
|
? { providerID: currentMessage.model.providerID, modelID: currentMessage.model.modelID }
|
||||||
|
: undefined
|
||||||
|
variant = currentMessage?.model?.variant
|
||||||
|
tools = normalizePromptTools(currentMessage?.tools) ?? tools
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedTools = resolveInheritedPromptTools(task.parentSessionId, tools)
|
||||||
|
return {
|
||||||
|
...(agent !== undefined ? { agent } : {}),
|
||||||
|
...(model !== undefined ? { model } : {}),
|
||||||
|
...(variant !== undefined ? { variant } : {}),
|
||||||
|
...(resolvedTools ? { tools: resolvedTools } : {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async isSessionActive(sessionID: string): Promise<boolean> {
|
private async isSessionActive(sessionID: string): Promise<boolean> {
|
||||||
return isOpenCodeSessionActive(this.client, sessionID)
|
return isOpenCodeSessionActive(this.client, sessionID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ type PromptAsyncCall = {
|
|||||||
path: { id: string }
|
path: { id: string }
|
||||||
body: {
|
body: {
|
||||||
noReply?: boolean
|
noReply?: boolean
|
||||||
|
agent?: string
|
||||||
|
model?: { providerID: string; modelID: string }
|
||||||
|
variant?: string
|
||||||
|
tools?: Record<string, boolean>
|
||||||
parts?: unknown[]
|
parts?: unknown[]
|
||||||
}
|
}
|
||||||
query?: {
|
query?: {
|
||||||
@@ -40,9 +44,7 @@ function createNotifier(args: {
|
|||||||
},
|
},
|
||||||
abort: async () => ({ data: {} }),
|
abort: async () => ({ data: {} }),
|
||||||
},
|
},
|
||||||
} as unknown as Parameters<typeof ParentWakeNotifier>[0] extends never
|
} as unknown as ConstructorParameters<typeof ParentWakeNotifier>[0]["client"]
|
||||||
? never
|
|
||||||
: ConstructorParameters<typeof ParentWakeNotifier>[0]["client"]
|
|
||||||
|
|
||||||
const notifier = new ParentWakeNotifier(
|
const notifier = new ParentWakeNotifier(
|
||||||
{
|
{
|
||||||
@@ -139,6 +141,49 @@ describe("ParentWakeNotifier — user message race guard (issue #4120)", () => {
|
|||||||
releaseAllPromptAsyncReservationsForTesting()
|
releaseAllPromptAsyncReservationsForTesting()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("#given pending wake has parent prompt context #when flushing #then promptAsync receives the context", async () => {
|
||||||
|
// given
|
||||||
|
const { notifier, promptAsyncCalls } = createNotifier({
|
||||||
|
sessionMessages: [
|
||||||
|
{
|
||||||
|
info: {
|
||||||
|
role: "assistant",
|
||||||
|
finish: "stop",
|
||||||
|
time: { created: Date.now() - 100 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
notifier.queuePendingParentWake(
|
||||||
|
"parent-context",
|
||||||
|
"task retrying",
|
||||||
|
{
|
||||||
|
agent: "hephaestus",
|
||||||
|
model: { providerID: "openai", modelID: "gpt-5" },
|
||||||
|
variant: "xhigh",
|
||||||
|
tools: { bash: true, edit: false },
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
// when
|
||||||
|
await notifier.flushPendingParentWake("parent-context")
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(promptAsyncCalls).toHaveLength(1)
|
||||||
|
expect(promptAsyncCalls[0]?.body).toMatchObject({
|
||||||
|
noReply: true,
|
||||||
|
agent: "hephaestus",
|
||||||
|
model: { providerID: "openai", modelID: "gpt-5" },
|
||||||
|
variant: "xhigh",
|
||||||
|
tools: { bash: true, edit: false },
|
||||||
|
})
|
||||||
|
expect(promptAsyncCalls[0]?.body.parts).toHaveLength(1)
|
||||||
|
|
||||||
|
notifier.shutdown()
|
||||||
|
releaseAllPromptAsyncReservationsForTesting()
|
||||||
|
})
|
||||||
|
|
||||||
test("#given user message is older than the race window #when flushing pending wake #then dispatch proceeds", async () => {
|
test("#given user message is older than the race window #when flushing pending wake #then dispatch proceeds", async () => {
|
||||||
// given
|
// given
|
||||||
const { notifier, promptAsyncCalls } = createNotifier({
|
const { notifier, promptAsyncCalls } = createNotifier({
|
||||||
|
|||||||
Reference in New Issue
Block a user