Skip to content

fix(background-agent): a parent wake retry no longer waits a second promptAsync hold - #8956

Closed
code-yeongyu wants to merge 3 commits into
devfrom
fix/win-ci-parent-wake-empty-turn
Closed

code-yeongyu wants to merge 3 commits into
devfrom
fix/win-ci-parent-wake-empty-turn

Conversation

@code-yeongyu

@code-yeongyu code-yeongyu commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Summary

Fixes the Windows-only red in parent-wake-empty-turn-requeue.test.ts (dev run 36300728869) at its product root, and removes the sleep-polling waitUntil from that test.

Root cause

A parent wake that meets its own promptAsync post-dispatch hold (DEFAULT_PROMPT_ASYNC_POST_DISPATCH_HOLD_MS = 2_000) was requeued with a fixed scheduleFlush(2_000), the full hold length. The hold expires lazily on Date.now() (reservations.ts). A retry timer armed a few ms after the hold started, firing a tick early on coarse Windows timers, still saw the hold and waited a second full hold (about 4 s). That is needless parent-wake latency in production, and it pushed the test past its 4 s polling window (4440 ms on CI).

Fix

  • utils/prompt-async-gate: the reserved result carries the hold's expiresAt. One helper (reservedDispatchResult) builds it for both reserved paths.
  • parent-wake-prompt-dispatch.ts: the first meeting with a hold keeps the existing 2 s back-off, so first-meeting timing is unchanged. Meeting the same hold again (same expiresAt, carried on the wake as gateHoldExpiresAt) waits only the rest of that hold.
  • Test: the empty-turn test asserts the synchronous requeue directly and awaits the second promptAsync call, subscribing before the trigger with a bounded timeout. No sleep loop.
  • A new parent-wake-prompt-dispatch.test.ts pins both halves with a frozen clock. The first meeting backs off 2000 ms. A same-hold retry 5 ms before expiry waits 5 ms; on the base code it waits 2000 (RED).
  • Three existing gate tests updated for the new expiresAt field (expect.any(Number)).

QA & Evidence

What was tested

  • bun test on packages/utils/src/prompt-async-gate* and packages/omo-opencode/src/features/background-agent: 854 pass, 0 fail. tsgo --noEmit (root and utils) and biome are clean.
  • Mutation: base parent-wake-prompt-dispatch.ts plus the new test gives (fail) ... waits only the rest of that hold (2000 instead of 5).
  • Windows: focused soak of both test files on windows-latest, 10 iterations each, 3 dispatches. The first implementation passed 3/3 (runs 36321348865, 36321351161, 36321353299). This final code's runs (36322548103, 36322550277, 36322552645) are listed in a comment below.
  • Real OpenCode: .agents/skills/opencode-qa/scripts/serve-wake-split-probe.sh --expect fixed (opencode serve, fake LLM, plugin loaded from this worktree, isolated XDG), on this branch and on base.

What was observed

  • In all 6 probe runs the gate-hold requeue path (the only dispatch branch this PR changes) ran 0 times. terminal_stops=1 and child_task_sessions=1 held on every run, and the real opencode DB session count was unchanged (isolation receipt).
  • The probe's wake branch count varies run to run on base and branch alike. A wake turn appears exactly when the dispatched-tracker "no assistant output" requeue fires, which happens when the harness keeps the parent alive more than about 5 s after the noReply admission (see probe-attribution.txt). That is a pre-existing sensitivity in the probe's --expect fixed topology check, unrelated to this change. It is tracked separately.

Why it is enough: the only behavior change is limited to re-meeting the same gate hold. It is pinned deterministically RED/GREEN, soaked on Windows, and its absence from the live topology runs is shown by log attribution.

What was omitted: raw logs stay local (.omo/evidence/20260927-parent-wake-gate-hold-retry/).

25ece089cbf854b784fc890a16733f8bc4326fda989ff06d6fba3a83297cfa1f probe-attribution.txt
343d5170be9e30ad2dc0198d1ae08f1e6cc758488fd05d0cb1f77429473c66dd v2/run1/probe.log
63027354b893338278a6309bc6ec1bb0ae76e8e63bbbc02f9f7c8cb1b5d4fd46 v2/run2/probe.log
8a261393d2505a77a9046f99b1adbc3b181cdd028a020bd03f841c4443ba8536 baseline/probe.log
65896085460433b07e7d523007b4a3c9198f4fa219692f5f77be7ed478f42941 baseline-loaded/run1/probe.log
f38b72516f527cdda70872302738da067b24466fdd3503031d3d5a740a5ede47 baseline-loaded/run2/probe.log

Fixes #8951
Refs #8324


Summary by cubic

Fixes parent wake retries waiting a second full promptAsync hold, reducing production latency and eliminating a Windows-only test flake.

The promptAsync gate's reserved result now carries the hold's expiresAt. A wake first meeting a hold keeps the full 2 s back-off; meeting the same hold again waits only the remaining time instead of a second full hold.

  • Carries the hold expiry on the wake across requeues and dedupe merges so retries can distinguish the same hold from a new one.
  • Replaces the sleep-polling waitUntil in the empty-turn requeue test with a direct synchronous assertion and a bounded await on the second promptAsync call.
  • Adds a frozen-clock test pinning both behaviors and updates five existing gate test assertions for the new expiresAt field.

Fixes #8951.

Written for commit 5751232. Summary will update on new commits.

Review in cubic

@github-actions github-actions Bot added utils Changes under packages/utils opencode OpenCode edition: packages/omo-opencode labels Sep 27, 2026
@code-yeongyu

Copy link
Copy Markdown
Owner Author

Probe timing sensitivity noted in QA is tracked as #8958. Final-code Windows soaks: 36322548103, 36322550277, 36322552645 (results to follow).

@code-yeongyu

Copy link
Copy Markdown
Owner Author

Final-code focused Windows soaks (windows-latest, both test files, 10 iterations each): 36322548103 success, 36322550277 success, 36322552645 success (soak head 89e458c = this PR's tree + the throwaway soak workflow commit).

…romptAsync hold (#8951)

A parent wake that met its own promptAsync post-dispatch hold always retried after a fixed 2 s, the whole hold length. The hold expires lazily on Date.now(), so a retry timer that fired a tick early (coarse Windows timers) still saw the hold and waited a second full hold.

The gate's reserved result now carries the hold's expiresAt. The first meeting with a hold keeps the full 2 s back-off; meeting the same hold again (same expiresAt) waits only what is left of it.
@code-yeongyu
code-yeongyu force-pushed the fix/win-ci-parent-wake-empty-turn branch from 69c14b9 to 7d73a7f Compare September 27, 2026 17:42
@code-yeongyu

Copy link
Copy Markdown
Owner Author

Superseded by #8961, which lands this fix together with the other win-ci fixes as one merge (all commits of this branch kept, bundles regenerated once over the merged sources). The evidence in this PR (RED/GREEN, focused Windows soaks) still applies; the issue is closed by #8961's Fixes line.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

omo-senpi Changes under packages/omo-senpi opencode OpenCode edition: packages/omo-opencode utils Changes under packages/utils

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Parent wake that meets the promptAsync hold waits a whole extra hold (Windows CI: parent-wake empty-turn recovery test)

1 participant