Repository navigation
fix(background-agent): defer parent-wake when a user message just arrived (fixes #4120) - #4122
Conversation
…ived (fixes #4120) When a background subagent emits [ALL BACKGROUND TASKS COMPLETE], the plugin queues a parent-wake that ultimately calls dispatchInternalPrompt against the parent session. If the user submits a new prompt inside the ~250 ms post-dispatch hold window, both writes land on the same OpenCode session-storage file at the same instant. OpenCode's @parcel/watcher (which the plugin itself does not depend on, but does indirectly trigger) batches those events into a TSFN callback and dispatches them into a JS env that the renderer has just torn down because the session view re-mounted around the user's new message -> napi_fatal_error / SIGABRT on macOS arm64. Removing the plugin removes the parent-wake, which is why removing OmO eliminates the crash. Mitigation: - Before flushPendingParentWake calls dispatchInternalPrompt, inspect the parent session's message tail. If the most recent message is a user message added inside PARENT_WAKE_USER_MESSAGE_IN_PROGRESS_WINDOW_MS (default 2_000 ms), reschedule instead of dispatching. The user's own prompt will drive the model; queued notifications will be re-flushed on the next idle. - Best-effort unref() of the long-lived pending-retry and dispatched- wake bookkeeping setTimeouts. They previously pinned the host event loop and prolonged the teardown window during which the watcher race can fire. The new option userMessageInProgressWindowMs is wired through BackgroundManager via a module-level constant and is independently testable. Regression test parent-wake-user-message-race.test.ts covers: - fresh user message -> dispatch deferred - latest message is assistant -> dispatch proceeds - user message older than window -> dispatch proceeds - window=0 disables the guard This is a surface-level mitigation of the most-likely root cause from the audit; a deeper fix (singleton guard against plugin double-instantiation under @opencode-ai/plugin@local reload, dispose lifecycle for OpenCode plugin reload) is out of scope here.
There was a problem hiding this comment.
No issues found across 3 files
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Auto-approved: The change adds a minimal, well-tested guard that defers parent-wake dispatch when a fresh user message is detected, without altering existing logic for the common case, and the timer .unref() improvements are unambiguously safe; the overall regression risk is near zero.
Re-trigger cubic
|
For context, also see the discussion threads:
|
Closing — superseded by upstream fixesClosing this PR. Issue #4120 was closed by the maintainer's own series of parent-wake fixes shipped between this PR's submission and v4.2.2:
The maintainer's approach is content-classification-based (filter synthetic/internal user messages, fix dispatch timestamping). My PR's approach was timing-based (2s "user message just arrived" window). Both close the same race, but the upstream version is semantically cleaner — it distinguishes real user input from OmO-injected synthetic tails by content, not by clock, and therefore can't be defeated by a slow-arriving real user message. Live-test honestyDuring post-PR validation on macOS arm64 (v4.2.0 local build with this fix applied), my new defer-guard log line fired 0 times across multiple parallel background-task completions plus immediate follow-up user messages. The pre-existing Defense-in-depth — not worth keepingI considered re-positioning this PR as an additive belt-and-suspenders layer above b53a8b5+33b66376, but the 2s timing window can in principle defer real fast user prompts that legitimately arrive right after a background completion. That's a false-positive surface area I don't want to add when the upstream solution is already correct. Thanks @code-yeongyu for the much cleaner fix. PR #4121 (#4119) is still in flight; the Oracle review blockers on that one are now addressed. |
Fixes #4120 (related: #3997, #4061).
Problem
When a background subagent emits
[ALL BACKGROUND TASKS COMPLETE], the plugin queues a parent-wake that ultimately callsdispatchInternalPromptagainst the parent session. If the user submits a new prompt inside the ~250 ms post-dispatch hold window, both writes land on the same OpenCode session-storage file at the same instant. OpenCode's@parcel/watcher(which the plugin doesn't depend on directly but does indirectly trigger via session writes) batches those events into a TSFN callback and dispatches them into a JS env that the renderer has just torn down because the session view re-mounted around the user's new message →napi_fatal_error/SIGABRTon macOS arm64.Removing the plugin removes the parent-wake, which is why removing OmO eliminates the crash (clean A/B in #4120).
Mitigation
flushPendingParentWakecallsdispatchInternalPrompt, inspect the parent session's message tail. If the most recent message is a user message added insidePARENT_WAKE_USER_MESSAGE_IN_PROGRESS_WINDOW_MS(default 2_000 ms), reschedule instead of dispatching. The user's own prompt will drive the model; queued notifications will be re-flushed on the next idle.unref()of the long-lived pending-retry and dispatched-wake bookkeepingsetTimeouts. They previously pinned the host event loop and prolonged the teardown window during which the watcher race can fire.The new option
userMessageInProgressWindowMsis wired throughBackgroundManagervia a module-level constant and is independently testable.Tests
parent-wake-user-message-race.test.tscovers:window=0disables the guardHonest disclaimer on confidence
During live testing on macOS arm64 with 6 parallel background tasks, the sidecar stayed stable across multiple completion cycles — but the new
Deferred parent wake because user message just arrivedlog line did not fire (grep -c = 0). The existing first-layer defer (isSessionActive(parent)check) andsettleAfterSessionIdle(150ms)appear to absorb most realistic timing windows on this hardware. I cannot prove this specific guard fired on the original crash path.Submitting this anyway because:
napi_fatal_error) is no longer reproducible with this build loaded.setTimeout.unref()change is unambiguously correct — those timers were preventing clean teardown.A deeper fix (singleton guard against plugin double-instantiation under
@opencode-ai/plugin@localreload, dispose lifecycle for OpenCode plugin reload) is out of scope here and called out in the commit message.Happy to gather more evidence with
snapshot: trueand tighter timing if requested before merge.Summary by cubic
Defers parent-wake dispatch when a fresh user message just arrived to prevent the macOS Electron sidecar crash caused by a prompt race. Fixes #4120.
usermessage (default 2000 ms), defer instead of dispatching.userMessageInProgressWindowMs(wired through BackgroundManager) to tune or disable this guard.Written for commit 3294128. Summary will update on new commits. Review in cubic