Summary
Automated audit found 19 race condition and concurrency bugs across packages/app/src/ and packages/opencode/src/. These span optimistic updates without rollback, concurrent write races, missing cancellation, stale data dependencies, timer cleanup issues, async generator error boundaries, and WebSocket/SSE event ordering.
High Severity
| File |
Lines |
Issue |
packages/app/src/context/sync.tsx |
254–261 |
Module-level optimistic Map has no concurrency control — rapid sequential submits or multi-tab usage can cause optimistic messages to disappear |
packages/opencode/src/session/turn-change.ts |
726–818 |
Undo/redo mutate() performs read-check-modify without transaction — double-click or multi-client usage can apply changes twice |
packages/app/src/context/global-sdk.tsx |
123–156 |
replayCursor.update(event.id) happens in raw SSE stream loop before events are flushed to UI store — connection drop between receive and flush causes event loss on reconnect |
Medium Severity
| Category |
File |
Lines |
Issue |
| Missing rollback |
packages/app/src/components/prompt-input/submit.ts |
141–186, 573–681 |
Command failures don't rollback server state; pending map uses stale sessionID for cleanup |
| Missing cancellation |
packages/app/src/pages/session/message-timeline.tsx |
427–585 |
turnChangeFetch continues after unmount — may show wrong dialog/toast for new session |
| Missing cancellation |
packages/app/src/context/global-sync/bootstrap.ts |
280–467 |
Bootstrap requests not cancellable on dispose — callbacks may write to disposed store |
| Event ordering |
packages/app/src/context/global-sync/event-reducer.ts |
208–300 |
part.delta silently dropped if part.updated not yet received (network reordering) |
| Timer cleanup |
packages/app/src/components/terminal.tsx |
203–298, 504 |
sizeTimer may fire after unmount; reconn dedup may mask real failures |
| Effect errors |
packages/opencode/src/session/llm.ts |
432–530 |
Inner Effect.runPromise timeout may cause unhandled defect if blockers service fails |
| Effect errors |
packages/opencode/src/control-plane/workspace.ts |
312–358 |
parseSSE callback errors may kill sync loop (defensive wrapping needed) |
Low Severity
| File |
Lines |
Issue |
packages/app/src/context/sync.tsx |
342–417 |
getOptimistic may read mutated state during load |
packages/app/src/pages/session/use-session-timeline-data.ts |
197–212 |
lastGoodMessages mutable variable capture race |
packages/app/src/context/file/tree-store.ts |
42–127 |
TOCTOU in options.scope() check |
packages/app/src/pages/layout.tsx |
265–315 |
Interval recreation race on rapid toggle |
packages/app/src/pages/session/session-running-state.ts |
42–65 |
Extra setTick from stale timer |
packages/app/src/context/global-sync.tsx |
89–98, 403–418 |
Nested rAF + setTimeout may leak inner timer |
packages/opencode/src/session/processor.ts |
591–680 |
fnUntraced limits debuggability on event processing errors |
packages/app/src/components/terminal.tsx |
527–608 |
Async output.push() may cause out-of-order display |
packages/opencode/src/control-plane/sse.ts |
18–65 |
onEvent called synchronously in forEach — async handlers may process out of order |
Recommendations
- Add atomic operations for the
optimistic Map in sync.tsx (use a queue or mutex)
- Move cursor update in
global-sdk.tsx to happen inside flush() alongside event emission
- Add sequence numbers to
message.part.delta events or buffer deltas until part exists
- Wrap
parseSSE call in workspace.ts with try/catch so malformed events don't terminate sync
- Use
AbortSignal consistently in bootstrapDirectory to cancel in-flight requests on dispose
- Guard async callbacks in
message-timeline.tsx with a disposed flag before calling dialog.show() or showToast()
Summary
Automated audit found 19 race condition and concurrency bugs across
packages/app/src/andpackages/opencode/src/. These span optimistic updates without rollback, concurrent write races, missing cancellation, stale data dependencies, timer cleanup issues, async generator error boundaries, and WebSocket/SSE event ordering.High Severity
packages/app/src/context/sync.tsxoptimisticMap has no concurrency control — rapid sequential submits or multi-tab usage can cause optimistic messages to disappearpackages/opencode/src/session/turn-change.tsmutate()performs read-check-modify without transaction — double-click or multi-client usage can apply changes twicepackages/app/src/context/global-sdk.tsxreplayCursor.update(event.id)happens in raw SSE stream loop before events are flushed to UI store — connection drop between receive and flush causes event loss on reconnectMedium Severity
packages/app/src/components/prompt-input/submit.tspendingmap uses stale sessionID for cleanuppackages/app/src/pages/session/message-timeline.tsxturnChangeFetchcontinues after unmount — may show wrong dialog/toast for new sessionpackages/app/src/context/global-sync/bootstrap.tspackages/app/src/context/global-sync/event-reducer.tspart.deltasilently dropped ifpart.updatednot yet received (network reordering)packages/app/src/components/terminal.tsxsizeTimermay fire after unmount;reconndedup may mask real failurespackages/opencode/src/session/llm.tsEffect.runPromisetimeout may cause unhandled defect ifblockersservice failspackages/opencode/src/control-plane/workspace.tsparseSSEcallback errors may kill sync loop (defensive wrapping needed)Low Severity
packages/app/src/context/sync.tsxgetOptimisticmay read mutated state during loadpackages/app/src/pages/session/use-session-timeline-data.tslastGoodMessagesmutable variable capture racepackages/app/src/context/file/tree-store.tsoptions.scope()checkpackages/app/src/pages/layout.tsxpackages/app/src/pages/session/session-running-state.tssetTickfrom stale timerpackages/app/src/context/global-sync.tsxpackages/opencode/src/session/processor.tsfnUntracedlimits debuggability on event processing errorspackages/app/src/components/terminal.tsxoutput.push()may cause out-of-order displaypackages/opencode/src/control-plane/sse.tsonEventcalled synchronously inforEach— async handlers may process out of orderRecommendations
optimisticMap insync.tsx(use a queue or mutex)global-sdk.tsxto happen insideflush()alongside event emissionmessage.part.deltaevents or buffer deltas until part existsparseSSEcall inworkspace.tswith try/catch so malformed events don't terminate syncAbortSignalconsistently inbootstrapDirectoryto cancel in-flight requests on disposemessage-timeline.tsxwith a disposed flag before callingdialog.show()orshowToast()