fix(background-agent): defer live tool-turn wakes

Require the latest assistant tool-wait turn itself to be stale before a shouldReply parent wake can bypass tool-call deferral. This prevents an all-complete background wake from forking a second parent prompt loop when OpenCode has repaired the tail to a synthetic user message.

Tests:

- bun test src/features/background-agent/parent-wake-user-message-race.test.ts src/features/background-agent/task-completion-cleanup.test.ts src/hooks/shared/prompt-async-gate.test.ts src/shared/prompt-async-route-audit.test.ts --bail

- bun run typecheck

- bun test
This commit is contained in:
YeonGyu-Kim
2026-05-19 10:06:25 +09:00
parent 6915f15299
commit 33b66376d7
3 changed files with 162 additions and 6 deletions
@@ -386,7 +386,10 @@ export class ParentWakeNotifier {
return status === "pending" || status === "running"
}
private latestAssistantTurnIsWaitingOnTools(messages: ParentWakeSessionMessage[]): boolean {
private latestAssistantToolWaitState(messages: ParentWakeSessionMessage[]): {
waiting: boolean
createdAt?: number
} {
for (let index = messages.length - 1; index >= 0; index--) {
const message = messages[index]
if (!message) {
@@ -394,14 +397,17 @@ export class ParentWakeNotifier {
}
const role = this.getParentWakeMessageRole(message)
if (role === "assistant") {
return this.getParentWakeMessageFinish(message) === "tool-calls"
const waiting = this.getParentWakeMessageFinish(message) === "tool-calls"
|| message.parts?.some((part) => this.parentWakePartIsWaitingOnTool(part)) === true
return waiting
? { waiting: true, createdAt: this.getParentWakeMessageCreatedAt(message) }
: { waiting: false }
}
if (role === "user") {
return false
return { waiting: false }
}
}
return false
return { waiting: false }
}
private parentWakeMessageHasOutput(message: ParentWakeSessionMessage): boolean {
@@ -470,13 +476,21 @@ export class ParentWakeNotifier {
private async shouldDeferParentWakeForSessionHistory(sessionID: string, wake: PendingParentWake): Promise<boolean> {
const messages = await this.loadParentWakeSessionMessages(sessionID)
if (!this.latestAssistantTurnIsWaitingOnTools(messages)) {
const toolWaitState = this.latestAssistantToolWaitState(messages)
if (!toolWaitState.waiting) {
delete wake.toolCallDeferralStartedAt
return false
}
const now = Date.now()
wake.toolCallDeferralStartedAt ??= now
if (wake.shouldReply && now - wake.toolCallDeferralStartedAt >= this.options.toolCallDeferMaxMs) {
const latestToolWaitAgeMs = toolWaitState.createdAt === undefined
? 0
: now - toolWaitState.createdAt
if (
wake.shouldReply
&& now - wake.toolCallDeferralStartedAt >= this.options.toolCallDeferMaxMs
&& latestToolWaitAgeMs >= this.options.toolCallDeferMaxMs
) {
log("[background-agent] Sending parent wake after stale tool-call deferral window:", {
sessionID,
})