Skip to content

fix(opencode-plugin): close TOCTOU race in deliverPendingToIdle#23

Open
mmkzer0 wants to merge 2 commits intoaannoo:mainfrom
mmkzer0:fix/toctou-delivery-mutex
Open

fix(opencode-plugin): close TOCTOU race in deliverPendingToIdle#23
mmkzer0 wants to merge 2 commits intoaannoo:mainfrom
mmkzer0:fix/toctou-delivery-mutex

Conversation

@mmkzer0
Copy link
Copy Markdown

@mmkzer0 mmkzer0 commented Apr 19, 2026

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:

  • TCP notify wake
  • idle-status wake (session.status -> idle)

Both paths could pass the pendingAckId === null check before either caller set it (first await boundary), causing duplicate opencode-read/promptAsync delivery of the same payload.

Fix

  • Add synchronous deliveryInFlight guard flag checked/set before the first await
  • Early-return concurrent callers when a delivery is already running
  • Reset guard in finally

pendingAckId remains in place as the second serialization layer between injection and transform-hook ack.

Validation

  • Reproduced prior duplicate-delivery behavior under frequent wake conditions
  • Two-turn injection tests (FIRST_OK / SECOND_OK) now show single delivery per message
  • Real OpenCode usage confirms duplicate turn injection is no longer present
  • Repository test suite passes (cargo 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.

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.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 deliveryInFlight guard to reject overlapping delivery attempts before the first await.
  • Wraps the delivery path in try/finally to 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.

Comment thread src/opencode_plugin/hcom.ts Outdated
Comment on lines +128 to +131
client.session.promptAsync({
path: { id: sid },
body: { parts: [{ type: "text", text: formatted }] },
} as any)
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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
}

Copilot uses AI. Check for mistakes.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants