fix(opencode-plugin): close TOCTOU race in deliverPendingToIdle#23
fix(opencode-plugin): close TOCTOU race in deliverPendingToIdle#23mmkzer0 wants to merge 2 commits intoaannoo:mainfrom
Conversation
Prevent duplicate promptAsync injections from concurrent wake paths by adding a synchronous deliveryInFlight guard before the first await and clearing it in finally. Also include the follow-up indentation correction in deliverPendingToIdle.
There was a problem hiding this comment.
Pull request overview
This PR addresses duplicate hcom message injections in the OpenCode plugin by preventing concurrent executions of deliverPendingToIdle() (a TOCTOU race between TCP notify wake and session.status idle wake).
Changes:
- Introduces a synchronous
deliveryInFlightguard to reject overlapping delivery attempts before the firstawait. - Wraps the delivery path in
try/finallyto ensure the guard resets even on early returns/errors. - Expands inline documentation to describe the two-layer serialization strategy (
deliveryInFlight+pendingAckId).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| client.session.promptAsync({ | ||
| path: { id: sid }, | ||
| body: { parts: [{ type: "text", text: formatted }] }, | ||
| } as any) |
There was a problem hiding this comment.
pendingAckId is set before calling client.session.promptAsync(...), but if promptAsync throws synchronously (e.g., invalid session id / client state), pendingAckId will remain non-null and permanently block future deliveries (since the transform hook won’t fire to clear it). Consider wrapping the promptAsync call in a try/catch that logs the error and resets pendingAckId (and possibly triggers a retry on the next wake).
| client.session.promptAsync({ | |
| path: { id: sid }, | |
| body: { parts: [{ type: "text", text: formatted }] }, | |
| } as any) | |
| try { | |
| client.session.promptAsync({ | |
| path: { id: sid }, | |
| body: { parts: [{ type: "text", text: formatted }] }, | |
| } as any) | |
| } catch (e) { | |
| pendingAckId = null | |
| log("ERROR", "plugin.delivery_prompt_failed", instanceName, { | |
| error: String(e), | |
| pending_ack: maxId, | |
| }) | |
| return false | |
| } |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
This fixes duplicate injected hcom turns in the OpenCode plugin by closing a TOCTOU race in
deliverPendingToIdle().The issue became reproducible under multi-agent orchestration, where wake events happen frequently and near-simultaneously.
Root cause
deliverPendingToIdle()can be entered from two independent wake paths:session.status->idle)Both paths could pass the
pendingAckId === nullcheck before either caller set it (firstawaitboundary), causing duplicateopencode-read/promptAsyncdelivery of the same payload.Fix
deliveryInFlightguard flag checked/set before the firstawaitfinallypendingAckIdremains in place as the second serialization layer between injection and transform-hook ack.Validation
FIRST_OK/SECOND_OK) now show single delivery per messagecargo test -q)Notes
This PR is intentionally scoped to the delivery race fix only. The agent/model persistence follow-up is prepared as a separate stacked branch/PR.