refactor(background-agent): wire ParentWakeNotifier into BackgroundManager
Replace the inlined parent-wake coalescing logic in manager.ts with delegation to the ParentWakeNotifier extracted in c1ccf8d09. The four timer Maps and the related methods now live in their own module with a narrow public API, while BackgroundManager retains the wiring point and the enqueue-callback bridge.
Closes HIGH-9 (step 2: integration)
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -233,11 +233,15 @@ function getPendingNotifications(manager: BackgroundManager): Map<string, string
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getPendingParentWakes(manager: BackgroundManager): Map<string, PendingParentWakeForTest> {
|
function getPendingParentWakes(manager: BackgroundManager): Map<string, PendingParentWakeForTest> {
|
||||||
return (cast<{ pendingParentWakes: Map<string, PendingParentWakeForTest> }>(manager)).pendingParentWakes
|
return (cast<{
|
||||||
|
parentWakeNotifier: { getPendingParentWakes: () => Map<string, PendingParentWakeForTest> }
|
||||||
|
}>(manager)).parentWakeNotifier.getPendingParentWakes()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDispatchedParentWakes(manager: BackgroundManager): Map<string, PendingParentWakeForTest> {
|
function getDispatchedParentWakes(manager: BackgroundManager): Map<string, PendingParentWakeForTest> {
|
||||||
return (cast<{ dispatchedParentWakes: Map<string, PendingParentWakeForTest> }>(manager)).dispatchedParentWakes
|
return (cast<{
|
||||||
|
parentWakeNotifier: { getDispatchedParentWakes: () => Map<string, PendingParentWakeForTest> }
|
||||||
|
}>(manager)).parentWakeNotifier.getDispatchedParentWakes()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCompletionTimers(manager: BackgroundManager): Map<string, ReturnType<typeof setTimeout>> {
|
function getCompletionTimers(manager: BackgroundManager): Map<string, ReturnType<typeof setTimeout>> {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import {
|
|||||||
type QueueItem,
|
type QueueItem,
|
||||||
} from "./constants"
|
} from "./constants"
|
||||||
|
|
||||||
import { resolveRegisteredAgentName, subagentSessions } from "../claude-code-session-state"
|
import { subagentSessions } from "../claude-code-session-state"
|
||||||
import { getTaskToastManager } from "../task-toast-manager"
|
import { getTaskToastManager } from "../task-toast-manager"
|
||||||
import { formatDuration } from "./duration-formatter"
|
import { formatDuration } from "./duration-formatter"
|
||||||
import {
|
import {
|
||||||
@@ -63,10 +63,7 @@ import {
|
|||||||
} from "./attempt-lifecycle"
|
} from "./attempt-lifecycle"
|
||||||
import { registerManagerForCleanup, unregisterManagerForCleanup } from "./process-cleanup"
|
import { registerManagerForCleanup, unregisterManagerForCleanup } from "./process-cleanup"
|
||||||
import { setContinuationMarkerSource } from "../../features/run-continuation-state"
|
import { setContinuationMarkerSource } from "../../features/run-continuation-state"
|
||||||
import {
|
import { isSessionActive as isOpenCodeSessionActive } from "../../hooks/shared/session-idle-settle"
|
||||||
isSessionActive as isOpenCodeSessionActive,
|
|
||||||
settleAfterSessionIdle,
|
|
||||||
} from "../../hooks/shared/session-idle-settle"
|
|
||||||
import { promptAsyncAfterSessionIdle, type PromptAsyncGateResult } from "../../hooks/shared/prompt-async-gate"
|
import { promptAsyncAfterSessionIdle, type PromptAsyncGateResult } from "../../hooks/shared/prompt-async-gate"
|
||||||
import {
|
import {
|
||||||
findNearestMessageExcludingCompaction,
|
findNearestMessageExcludingCompaction,
|
||||||
@@ -96,39 +93,9 @@ import {
|
|||||||
resolveSubagentSpawnContext,
|
resolveSubagentSpawnContext,
|
||||||
type SubagentSpawnContext,
|
type SubagentSpawnContext,
|
||||||
} from "./subagent-spawn-limits"
|
} from "./subagent-spawn-limits"
|
||||||
|
import { ParentWakeNotifier, type ParentWakePromptContext } from "./parent-wake-notifier"
|
||||||
type OpencodeClient = PluginInput["client"]
|
type OpencodeClient = PluginInput["client"]
|
||||||
|
|
||||||
type ParentWakePromptContext = {
|
|
||||||
agent?: string
|
|
||||||
model?: { providerID: string; modelID: string }
|
|
||||||
variant?: string
|
|
||||||
tools?: Record<string, boolean>
|
|
||||||
}
|
|
||||||
|
|
||||||
type PendingParentWake = {
|
|
||||||
promptContext: ParentWakePromptContext
|
|
||||||
notifications: string[]
|
|
||||||
shouldReply: boolean
|
|
||||||
dispatchedAt?: number
|
|
||||||
toolCallDeferralStartedAt?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type ParentWakeSessionMessage = {
|
|
||||||
info?: {
|
|
||||||
role?: string
|
|
||||||
finish?: string
|
|
||||||
time?: { created?: unknown }
|
|
||||||
}
|
|
||||||
role?: string
|
|
||||||
finish?: string
|
|
||||||
time?: { created?: unknown }
|
|
||||||
parts?: Array<{
|
|
||||||
type?: string
|
|
||||||
text?: string
|
|
||||||
content?: unknown
|
|
||||||
}>
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResumeTaskSnapshot = {
|
type ResumeTaskSnapshot = {
|
||||||
status: BackgroundTask["status"]
|
status: BackgroundTask["status"]
|
||||||
completedAt?: Date
|
completedAt?: Date
|
||||||
@@ -272,10 +239,7 @@ export class BackgroundManager {
|
|||||||
private completedTaskSummaries: Map<string, BackgroundTaskNotificationTask[]> = new Map()
|
private completedTaskSummaries: Map<string, BackgroundTaskNotificationTask[]> = new Map()
|
||||||
private idleDeferralTimers: Map<string, ReturnType<typeof setTimeout>> = new Map()
|
private idleDeferralTimers: Map<string, ReturnType<typeof setTimeout>> = new Map()
|
||||||
private notificationQueueByParent: Map<string, Promise<void>> = new Map()
|
private notificationQueueByParent: Map<string, Promise<void>> = new Map()
|
||||||
private pendingParentWakes: Map<string, PendingParentWake> = new Map()
|
private readonly parentWakeNotifier: ParentWakeNotifier
|
||||||
private pendingParentWakeTimers: Map<string, ReturnType<typeof setTimeout>> = new Map()
|
|
||||||
private dispatchedParentWakes: Map<string, PendingParentWake> = new Map()
|
|
||||||
private dispatchedParentWakeTimers: Map<string, ReturnType<typeof setTimeout>> = new Map()
|
|
||||||
private observedOutputSessions: Set<string> = new Set()
|
private observedOutputSessions: Set<string> = new Set()
|
||||||
private observedIncompleteTodosBySession: Map<string, boolean> = new Map()
|
private observedIncompleteTodosBySession: Map<string, boolean> = new Map()
|
||||||
private rootDescendantCounts: Map<string, number>
|
private rootDescendantCounts: Map<string, number>
|
||||||
@@ -306,6 +270,19 @@ export class BackgroundManager {
|
|||||||
this.enableParentSessionNotifications = options?.enableParentSessionNotifications ?? true
|
this.enableParentSessionNotifications = options?.enableParentSessionNotifications ?? true
|
||||||
this.modelFallbackControllerAccessor = options?.modelFallbackControllerAccessor
|
this.modelFallbackControllerAccessor = options?.modelFallbackControllerAccessor
|
||||||
this.logger = options?.log ?? log
|
this.logger = options?.log ?? log
|
||||||
|
this.parentWakeNotifier = new ParentWakeNotifier(
|
||||||
|
{
|
||||||
|
client: this.client,
|
||||||
|
directory: this.directory,
|
||||||
|
enqueueNotificationForParent: this.enqueueNotificationForParent.bind(this),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pendingRetryMs: PENDING_PARENT_WAKE_RETRY_MS,
|
||||||
|
acceptedMessageSkewMs: PARENT_WAKE_ACCEPTED_MESSAGE_SKEW_MS,
|
||||||
|
toolCallDeferMaxMs: PARENT_WAKE_TOOL_CALL_DEFER_MAX_MS,
|
||||||
|
failureRequeueWindowMs: PARENT_WAKE_FAILURE_REQUEUE_WINDOW_MS,
|
||||||
|
},
|
||||||
|
)
|
||||||
this.registerProcessCleanup()
|
this.registerProcessCleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1385,222 +1362,12 @@ The fallback retry session is now created and can be inspected directly.
|
|||||||
this.observedOutputSessions.add(sessionID)
|
this.observedOutputSessions.add(sessionID)
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolveParentWakePromptContext(promptContext: ParentWakePromptContext): ParentWakePromptContext {
|
|
||||||
const resolvedAgent = resolveRegisteredAgentName(promptContext.agent)
|
|
||||||
return {
|
|
||||||
...promptContext,
|
|
||||||
...(resolvedAgent ? { agent: resolvedAgent } : {}),
|
|
||||||
...(promptContext.model ? { model: { ...promptContext.model } } : {}),
|
|
||||||
...(promptContext.tools ? { tools: { ...promptContext.tools } } : {}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private cloneParentWake(wake: PendingParentWake): PendingParentWake {
|
|
||||||
const promptContext = this.resolveParentWakePromptContext(wake.promptContext)
|
|
||||||
return {
|
|
||||||
promptContext,
|
|
||||||
notifications: [...wake.notifications],
|
|
||||||
shouldReply: wake.shouldReply,
|
|
||||||
...(wake.dispatchedAt !== undefined ? { dispatchedAt: wake.dispatchedAt } : {}),
|
|
||||||
...(wake.toolCallDeferralStartedAt !== undefined
|
|
||||||
? { toolCallDeferralStartedAt: wake.toolCallDeferralStartedAt }
|
|
||||||
: {}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearDispatchedParentWake(sessionID: string): void {
|
private clearDispatchedParentWake(sessionID: string): void {
|
||||||
const timer = this.dispatchedParentWakeTimers.get(sessionID)
|
this.parentWakeNotifier.clearDispatchedParentWake(sessionID)
|
||||||
if (timer) {
|
|
||||||
clearTimeout(timer)
|
|
||||||
this.dispatchedParentWakeTimers.delete(sessionID)
|
|
||||||
}
|
|
||||||
this.dispatchedParentWakes.delete(sessionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
private trackDispatchedParentWake(sessionID: string, wake: PendingParentWake): void {
|
|
||||||
this.clearDispatchedParentWake(sessionID)
|
|
||||||
const dispatchedWake = this.cloneParentWake(wake)
|
|
||||||
dispatchedWake.dispatchedAt = Date.now()
|
|
||||||
this.dispatchedParentWakes.set(sessionID, dispatchedWake)
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
this.dispatchedParentWakeTimers.delete(sessionID)
|
|
||||||
this.dispatchedParentWakes.delete(sessionID)
|
|
||||||
}, PARENT_WAKE_FAILURE_REQUEUE_WINDOW_MS)
|
|
||||||
this.dispatchedParentWakeTimers.set(sessionID, timer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async requeueDispatchedParentWake(sessionID: string, reason: string): Promise<boolean> {
|
private async requeueDispatchedParentWake(sessionID: string, reason: string): Promise<boolean> {
|
||||||
const wake = this.dispatchedParentWakes.get(sessionID)
|
return this.parentWakeNotifier.requeueDispatchedParentWake(sessionID, reason)
|
||||||
if (!wake) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
await settleAfterSessionIdle()
|
|
||||||
|
|
||||||
if (await this.hasAcceptedMessageAfterDispatchedParentWake(sessionID, wake)) {
|
|
||||||
this.clearDispatchedParentWake(sessionID)
|
|
||||||
log("[background-agent] Ignored late parent wake failure after assistant output:", {
|
|
||||||
sessionID,
|
|
||||||
reason,
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
this.clearDispatchedParentWake(sessionID)
|
|
||||||
const pendingWake = this.pendingParentWakes.get(sessionID)
|
|
||||||
if (pendingWake) {
|
|
||||||
pendingWake.notifications.unshift(...wake.notifications)
|
|
||||||
pendingWake.shouldReply = pendingWake.shouldReply || wake.shouldReply
|
|
||||||
pendingWake.promptContext = wake.promptContext
|
|
||||||
pendingWake.toolCallDeferralStartedAt ??= wake.toolCallDeferralStartedAt
|
|
||||||
} else {
|
|
||||||
this.pendingParentWakes.set(sessionID, this.cloneParentWake(wake))
|
|
||||||
}
|
|
||||||
this.schedulePendingParentWakeFlush(sessionID)
|
|
||||||
log("[background-agent] Requeued dispatched parent wake after prompt failure:", {
|
|
||||||
sessionID,
|
|
||||||
reason,
|
|
||||||
})
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private async loadParentWakeSessionMessages(sessionID: string): Promise<ParentWakeSessionMessage[]> {
|
|
||||||
try {
|
|
||||||
const messagesResp = await messagesInDirectory(this.client, {
|
|
||||||
path: { id: sessionID },
|
|
||||||
}, this.directory)
|
|
||||||
return normalizeSDKResponse(messagesResp, [] as ParentWakeSessionMessage[])
|
|
||||||
} catch (error) {
|
|
||||||
log("[background-agent] Failed to inspect parent session messages for wake safety:", {
|
|
||||||
sessionID,
|
|
||||||
error,
|
|
||||||
})
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getParentWakeMessageRole(message: ParentWakeSessionMessage): string | undefined {
|
|
||||||
return message.info?.role ?? message.role
|
|
||||||
}
|
|
||||||
|
|
||||||
private getParentWakeMessageFinish(message: ParentWakeSessionMessage): string | undefined {
|
|
||||||
return message.info?.finish ?? message.finish
|
|
||||||
}
|
|
||||||
|
|
||||||
private getParentWakeMessageCreatedAt(message: ParentWakeSessionMessage): number | undefined {
|
|
||||||
const value = message.info?.time?.created ?? message.time?.created
|
|
||||||
if (typeof value === "number" && Number.isFinite(value)) {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
if (typeof value === "string") {
|
|
||||||
const parsed = Date.parse(value)
|
|
||||||
return Number.isFinite(parsed) ? parsed : undefined
|
|
||||||
}
|
|
||||||
if (value instanceof Date) {
|
|
||||||
return value.getTime()
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
private latestAssistantTurnIsWaitingOnTools(messages: ParentWakeSessionMessage[]): boolean {
|
|
||||||
for (let index = messages.length - 1; index >= 0; index--) {
|
|
||||||
const message = messages[index]
|
|
||||||
if (!message) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const role = this.getParentWakeMessageRole(message)
|
|
||||||
if (role === "assistant") {
|
|
||||||
return this.getParentWakeMessageFinish(message) === "tool-calls"
|
|
||||||
}
|
|
||||||
if (role === "user") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private parentWakeMessageHasOutput(message: ParentWakeSessionMessage): boolean {
|
|
||||||
const role = this.getParentWakeMessageRole(message)
|
|
||||||
if (role !== "assistant" && role !== "tool") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (!message.parts || message.parts.length === 0) {
|
|
||||||
return role === "assistant"
|
|
||||||
}
|
|
||||||
return message.parts.some((part) => {
|
|
||||||
if (part.type === "text" || part.type === "reasoning") {
|
|
||||||
return typeof part.text === "string" && part.text.trim().length > 0
|
|
||||||
}
|
|
||||||
if (part.type === "tool" || part.type === "tool_result") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (part.content !== undefined) {
|
|
||||||
if (typeof part.content === "string") {
|
|
||||||
return part.content.trim().length > 0
|
|
||||||
}
|
|
||||||
if (Array.isArray(part.content)) {
|
|
||||||
return part.content.length > 0
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private parentWakeMessageContainsNotification(
|
|
||||||
message: ParentWakeSessionMessage,
|
|
||||||
wake: PendingParentWake,
|
|
||||||
): boolean {
|
|
||||||
if (this.getParentWakeMessageRole(message) !== "user") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return message.parts?.some((part) =>
|
|
||||||
typeof part.text === "string" && wake.notifications.some((notification) => part.text?.includes(notification))
|
|
||||||
) ?? false
|
|
||||||
}
|
|
||||||
|
|
||||||
private async shouldDeferParentWakeForSessionHistory(sessionID: string, wake: PendingParentWake): Promise<boolean> {
|
|
||||||
const messages = await this.loadParentWakeSessionMessages(sessionID)
|
|
||||||
if (!this.latestAssistantTurnIsWaitingOnTools(messages)) {
|
|
||||||
delete wake.toolCallDeferralStartedAt
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const now = Date.now()
|
|
||||||
wake.toolCallDeferralStartedAt ??= now
|
|
||||||
if (wake.shouldReply && now - wake.toolCallDeferralStartedAt >= PARENT_WAKE_TOOL_CALL_DEFER_MAX_MS) {
|
|
||||||
log("[background-agent] Sending parent wake after stale tool-call deferral window:", {
|
|
||||||
sessionID,
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
log("[background-agent] Deferred parent wake because latest assistant turn is waiting on tool results:", {
|
|
||||||
sessionID,
|
|
||||||
})
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private async hasAcceptedMessageAfterDispatchedParentWake(
|
|
||||||
sessionID: string,
|
|
||||||
wake: PendingParentWake,
|
|
||||||
): Promise<boolean> {
|
|
||||||
if (wake.dispatchedAt === undefined) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const dispatchedAt = wake.dispatchedAt
|
|
||||||
const messages = await this.loadParentWakeSessionMessages(sessionID)
|
|
||||||
return messages.some((message) => {
|
|
||||||
const createdAt = this.getParentWakeMessageCreatedAt(message)
|
|
||||||
if (createdAt === undefined) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
createdAt >= dispatchedAt - PARENT_WAKE_ACCEPTED_MESSAGE_SKEW_MS
|
|
||||||
&& this.parentWakeMessageContainsNotification(message, wake)
|
|
||||||
) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return createdAt >= dispatchedAt && this.parentWakeMessageHasOutput(message)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearSessionOutputObserved(sessionID: string): void {
|
private clearSessionOutputObserved(sessionID: string): void {
|
||||||
@@ -2699,132 +2466,11 @@ The task was re-queued on a fallback model after a retryable failure.
|
|||||||
shouldReply: boolean,
|
shouldReply: boolean,
|
||||||
delayMs?: number,
|
delayMs?: number,
|
||||||
): void {
|
): void {
|
||||||
const resolvedPromptContext = this.resolveParentWakePromptContext(promptContext)
|
this.parentWakeNotifier.queuePendingParentWake(sessionID, notification, promptContext, shouldReply, delayMs)
|
||||||
const pendingWake = this.pendingParentWakes.get(sessionID)
|
|
||||||
if (pendingWake) {
|
|
||||||
pendingWake.notifications.push(notification)
|
|
||||||
pendingWake.promptContext = resolvedPromptContext
|
|
||||||
pendingWake.shouldReply = pendingWake.shouldReply || shouldReply
|
|
||||||
} else {
|
|
||||||
this.pendingParentWakes.set(sessionID, {
|
|
||||||
promptContext: resolvedPromptContext,
|
|
||||||
notifications: [notification],
|
|
||||||
shouldReply,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this.schedulePendingParentWakeFlush(sessionID, delayMs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async flushPendingParentWake(sessionID: string): Promise<void> {
|
private async flushPendingParentWake(sessionID: string): Promise<void> {
|
||||||
if (!this.pendingParentWakes.has(sessionID)) {
|
await this.parentWakeNotifier.flushPendingParentWake(sessionID)
|
||||||
this.clearPendingParentWakeTimer(sessionID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await this.isSessionActive(sessionID)) {
|
|
||||||
this.schedulePendingParentWakeFlush(sessionID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.clearPendingParentWakeTimer(sessionID)
|
|
||||||
await settleAfterSessionIdle()
|
|
||||||
|
|
||||||
if (await this.isSessionActive(sessionID)) {
|
|
||||||
this.schedulePendingParentWakeFlush(sessionID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const latestWake = this.pendingParentWakes.get(sessionID)
|
|
||||||
if (!latestWake) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await this.shouldDeferParentWakeForSessionHistory(sessionID, latestWake)) {
|
|
||||||
this.schedulePendingParentWakeFlush(sessionID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pendingParentWakes.delete(sessionID)
|
|
||||||
|
|
||||||
const notificationContent = latestWake.notifications.join("\n\n")
|
|
||||||
|
|
||||||
try {
|
|
||||||
const promptResult = await promptAsyncAfterSessionIdle({
|
|
||||||
client: this.client,
|
|
||||||
sessionID,
|
|
||||||
source: "background-agent-parent-wake",
|
|
||||||
settleMs: 0,
|
|
||||||
postDispatchHoldMs: 250,
|
|
||||||
input: {
|
|
||||||
path: { id: sessionID },
|
|
||||||
body: {
|
|
||||||
noReply: !latestWake.shouldReply,
|
|
||||||
...latestWake.promptContext,
|
|
||||||
parts: [createInternalAgentTextPart(notificationContent)],
|
|
||||||
},
|
|
||||||
query: { directory: this.directory },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if (promptResult.status === "failed") {
|
|
||||||
throw promptResult.error
|
|
||||||
}
|
|
||||||
if (promptResult.status !== "dispatched") {
|
|
||||||
const pendingWake = this.pendingParentWakes.get(sessionID)
|
|
||||||
if (pendingWake) {
|
|
||||||
pendingWake.notifications.unshift(...latestWake.notifications)
|
|
||||||
pendingWake.shouldReply = pendingWake.shouldReply || latestWake.shouldReply
|
|
||||||
pendingWake.promptContext = latestWake.promptContext
|
|
||||||
pendingWake.toolCallDeferralStartedAt ??= latestWake.toolCallDeferralStartedAt
|
|
||||||
} else {
|
|
||||||
this.pendingParentWakes.set(sessionID, latestWake)
|
|
||||||
}
|
|
||||||
this.schedulePendingParentWakeFlush(sessionID)
|
|
||||||
log("[background-agent] Deferred parent wake skipped by promptAsync gate:", {
|
|
||||||
sessionID,
|
|
||||||
status: promptResult.status,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log("[background-agent] Sent deferred parent wake:", { sessionID })
|
|
||||||
this.trackDispatchedParentWake(sessionID, latestWake)
|
|
||||||
} catch (error) {
|
|
||||||
const pendingWake = this.pendingParentWakes.get(sessionID)
|
|
||||||
if (pendingWake) {
|
|
||||||
pendingWake.notifications.unshift(...latestWake.notifications)
|
|
||||||
pendingWake.shouldReply = pendingWake.shouldReply || latestWake.shouldReply
|
|
||||||
pendingWake.promptContext = latestWake.promptContext
|
|
||||||
pendingWake.toolCallDeferralStartedAt ??= latestWake.toolCallDeferralStartedAt
|
|
||||||
} else {
|
|
||||||
this.pendingParentWakes.set(sessionID, latestWake)
|
|
||||||
}
|
|
||||||
this.schedulePendingParentWakeFlush(sessionID)
|
|
||||||
log("[background-agent] Failed to send deferred parent wake:", { sessionID, error })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private schedulePendingParentWakeFlush(sessionID: string, delayMs?: number): void {
|
|
||||||
if (this.pendingParentWakeTimers.has(sessionID)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
this.pendingParentWakeTimers.delete(sessionID)
|
|
||||||
void this.enqueueNotificationForParent(sessionID, () => this.flushPendingParentWake(sessionID)).catch((error) => {
|
|
||||||
log("[background-agent] Failed to retry pending parent wake:", { sessionID, error })
|
|
||||||
})
|
|
||||||
}, delayMs ?? PENDING_PARENT_WAKE_RETRY_MS)
|
|
||||||
|
|
||||||
this.pendingParentWakeTimers.set(sessionID, timer)
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearPendingParentWakeTimer(sessionID: string): void {
|
|
||||||
const timer = this.pendingParentWakeTimers.get(sessionID)
|
|
||||||
if (!timer) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
clearTimeout(timer)
|
|
||||||
this.pendingParentWakeTimers.delete(sessionID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private hasRunningTasks(): boolean {
|
private hasRunningTasks(): boolean {
|
||||||
@@ -3147,15 +2793,7 @@ The task was re-queued on a fallback model after a retryable failure.
|
|||||||
}
|
}
|
||||||
this.idleDeferralTimers.clear()
|
this.idleDeferralTimers.clear()
|
||||||
|
|
||||||
for (const timer of this.pendingParentWakeTimers.values()) {
|
this.parentWakeNotifier.shutdown()
|
||||||
clearTimeout(timer)
|
|
||||||
}
|
|
||||||
this.pendingParentWakeTimers.clear()
|
|
||||||
|
|
||||||
for (const timer of this.dispatchedParentWakeTimers.values()) {
|
|
||||||
clearTimeout(timer)
|
|
||||||
}
|
|
||||||
this.dispatchedParentWakeTimers.clear()
|
|
||||||
|
|
||||||
for (const sessionID of trackedSessionIDs) {
|
for (const sessionID of trackedSessionIDs) {
|
||||||
subagentSessions.delete(sessionID)
|
subagentSessions.delete(sessionID)
|
||||||
@@ -3168,8 +2806,6 @@ The task was re-queued on a fallback model after a retryable failure.
|
|||||||
this.notifications.clear()
|
this.notifications.clear()
|
||||||
this.pendingNotifications.clear()
|
this.pendingNotifications.clear()
|
||||||
this.pendingByParent.clear()
|
this.pendingByParent.clear()
|
||||||
this.pendingParentWakes.clear()
|
|
||||||
this.dispatchedParentWakes.clear()
|
|
||||||
this.notificationQueueByParent.clear()
|
this.notificationQueueByParent.clear()
|
||||||
this.rootDescendantCounts.clear()
|
this.rootDescendantCounts.clear()
|
||||||
this.queuesByKey.clear()
|
this.queuesByKey.clear()
|
||||||
|
|||||||
@@ -98,9 +98,8 @@ function createManager(
|
|||||||
abort: async () => ({}),
|
abort: async () => ({}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const placeholderClient = {} as PluginInput["client"]
|
|
||||||
const ctx: PluginInput = {
|
const ctx: PluginInput = {
|
||||||
client: placeholderClient,
|
client: client as PluginInput["client"],
|
||||||
project: {} as PluginInput["project"],
|
project: {} as PluginInput["project"],
|
||||||
directory: tmpdir(),
|
directory: tmpdir(),
|
||||||
worktree: tmpdir(),
|
worktree: tmpdir(),
|
||||||
@@ -111,7 +110,6 @@ function createManager(
|
|||||||
const manager = new BackgroundManager(
|
const manager = new BackgroundManager(
|
||||||
{ pluginContext: ctx, config: undefined, enableParentSessionNotifications }
|
{ pluginContext: ctx, config: undefined, enableParentSessionNotifications }
|
||||||
)
|
)
|
||||||
Reflect.set(manager, "client", client)
|
|
||||||
|
|
||||||
return { manager, promptAsyncCalls }
|
return { manager, promptAsyncCalls }
|
||||||
}
|
}
|
||||||
@@ -174,7 +172,10 @@ function getPendingNotifications(manager: BackgroundManager): Map<string, string
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getPendingParentWakes(manager: BackgroundManager): Map<string, PendingParentWakeForTest> {
|
function getPendingParentWakes(manager: BackgroundManager): Map<string, PendingParentWakeForTest> {
|
||||||
return Reflect.get(manager, "pendingParentWakes") as Map<string, PendingParentWakeForTest>
|
const parentWakeNotifier = Reflect.get(manager, "parentWakeNotifier") as {
|
||||||
|
getPendingParentWakes: () => Map<string, PendingParentWakeForTest>
|
||||||
|
}
|
||||||
|
return parentWakeNotifier.getPendingParentWakes()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCompletionTimers(manager: BackgroundManager): Map<string, ReturnType<typeof setTimeout>> {
|
function getCompletionTimers(manager: BackgroundManager): Map<string, ReturnType<typeof setTimeout>> {
|
||||||
|
|||||||
Reference in New Issue
Block a user