fix(plugin): await session.idle + server.instance.disposed handlers#74
Merged
Conversation
PR #50 added a top-level forceFlush race to recover the final span on exit, but forceFlush only exports ended spans. The plugin's session.idle and server.instance.disposed handlers (which end the open turn span and drain the SDK) were fire-and-forget via 'void hook[event]?.(...)', so the final turn span was still open when forceFlush ran. Narrow await: only those two disposal-class events. All other events stay fire-and-forget.
There was a problem hiding this comment.
1 issue found across 1 file
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/opencode/src/plugin/index.ts">
<violation number="1" location="packages/opencode/src/plugin/index.ts:257">
P2: Synchronous exceptions from `hook["event"]` are not caught, so a throwing plugin can fail the bus subscription fiber instead of being logged and isolated.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Fix all with cubic | Re-trigger cubic
Per PR #74 review: the previous patch only wrapped 'await ret' in try/catch, so a synchronous throw from hook[event] would escape the Effect.promise callback as a defect and kill the Stream.runForEach fiber — silently disabling every plugin's event handler for the rest of the process. Wrap the whole invocation.
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/opencode/src/plugin/index.ts">
<violation number="1" location="packages/opencode/src/plugin/index.ts:259">
P2: Non-awaited plugin event Promises can reject without being caught, so async handler failures on non-disposal events are still unhandled and not logged.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Fix all with cubic | Re-trigger cubic
… them Per PR #74 review: for non-disposal events the handler's promise is not awaited, so an async rejection becomes an unhandledRejection — Node logs it generically but our log.error never fires, hiding which plugin/event broke. Attach .catch(log.error) on the discard path. Sync throws and the awaited path still go through the outer try/catch.
Alezander9
pushed a commit
that referenced
this pull request
May 17, 2026
Three fixes: 1. (src/index.ts) Wrap the dynamic 'await import(./plugin)' in the top-level finally with try/catch so a module-load failure cannot strand the process before forceFlush + process.exit(). 2. (plugin/index.ts) Re-add per-hook error isolation on the bus event dispatch loop. Reverting PR #74's await also accidentally removed this. Catches sync throws and observes async rejections via .catch(log.error) so one bad plugin can't terminate the subscription fiber for the rest of the process. 3. (plugin/index.ts) Deregister this layer's shutdown hooks from the module-level Set via Effect.addFinalizer so multi-instance TUI mode doesn't accumulate stale closures across project reopens.
Alezander9
added a commit
that referenced
this pull request
May 17, 2026
fix(plugin): synchronous shutdown hook for OTel span drain (revert PR #74)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Await plugin event handlers for
session.idleandserver.instance.disposedso lifecycle plugins (e.g.bcode-laminar) can finish draining beforeprocess.exit().Why
packages/opencode/src/index.ts:252-266(PR #50) added a top-levelprovider.forceFlush()race beforeprocess.exit()to recover the final agent LLM span. That helped, butforceFlush()only exports ended spans. The deeper bug — already called out in PR #50's own comment — is that bus dispatch atpackages/opencode/src/plugin/index.ts:249isvoid hook["event"]?.(...), so thebcode-laminarplugin'ssession.idleandserver.instance.disposedhandlers (whichspan.end()the open turn span andawait processor.forceFlush()/await sdk.shutdown()) are fire-and-forget. The turn span is therefore still open whenforceFlush()runs, so it never reaches the wire beforeprocess.exit()kills the in-flight export.Verified empirically in V4 cloud staging: long-task runs land every turn span except the last one in BrowserCode-CLOUD Laminar. Exact symptom of the existing comment.
Change
Narrow scope by design:
session.message,session.created, etc. keep their existing fire-and-forget semantics — no latency increase, no new deadlock surface, no ordering changes for the hot paths.log.errorpattern theconfighook already uses, so one bad plugin can't stall the bus.Verification plan
main.cloud/backend/sandboxes/v4-worker/Dockerfileto--version 0.1.7; CP auto-deploys to AgentCore.Yellow-zone note
Documented in
memory/browsercode/EXCEPTIONS.mdunder "Phase F (cont.) — await disposal/idle plugin events". This is a real upstream-grade fix (PR #50's comment already identifies the underlying bug). Upstreamable toanomalyco/opencodeonce we've validated it in production for a week.Summary by cubic
Await plugin handlers for
session.idleandserver.instance.disposedso lifecycle plugins (e.g.,bcode-laminar) can end the final turn span and flush OTel before process exit. Also log errors from fire‑and‑forget handlers to avoid unhandled rejections.session.idleandserver.instance.disposed; all other events stay fire-and-forget..catch(log.error)to handler promises to surface async failures instead of unhandledRejection.provider.forceFlush()as a fallback.Written for commit e008c31. Summary will update on new commits. Review in cubic