Today every sync.run produces TWO GlobalBus events:
1. From bus.publish itself — the projection view:
{ payload: { id, type: "session.created", properties } }
2. From sync/index.ts — the source-of-truth envelope:
{ payload: { type: "sync", syncEvent: { type: "session.created.1", id, seq, aggregateID, data } } }
These two emits live in different files and discriminate via
`payload.type` — either a real event type or the sentinel "sync".
That collision is the source of confusion in the codebase:
consumers have to know which shape they're looking at.
The duality dates back to:
- d88912abf0 (Dec 2025): Dax added GlobalBus.emit inside bus.publish.
- b22add292c (#22347, Apr 2026): James consolidated sync events onto
GlobalBus by ADDING a second emit with the source envelope.
#22347's stated intent was "consolidate events into a single stream".
Two emits with different shapes on one channel isn't quite that. One
emit per event with a unified shape is.
This PR collapses the two emits into one:
{ payload: { id, type: "session.created", properties, sync?: { name, seq, aggregateID, data } } }
- bus.publish accepts an optional `sync?: SyncMetadata` option.
- sync/index.ts passes it; its own GlobalBus.emit is removed.
- BusEvent.effectPayloads() adds an optional `sync` field to every
event schema.
- SyncEvent.effectPayloads() is no longer included in the wire schema
(it's now subsumed by the optional field).
- Consumers migrate from `payload.type === "sync"` filtering to
`payload.sync != null` filtering. control-plane/workspace.ts
reconstructs SerializedEvent from { id, sync.* } for replay.
Note: this is a BREAKING SDK wire-format change. External consumers
that read `payload.type === "sync"` or `payload.syncEvent` need to
migrate. Internal opencode consumers all migrated in this PR.
Side observation: the runtime emit was using `payload.syncEvent`
(nested) while the SDK schema declared the fields at top level under
`type: "sync"` — a silent schema drift. Collapsing to one emit also
fixes that drift by definition.
Verified:
- bun typecheck — clean
- bun run test test/sync/index.test.ts test/bus/bus-effect.test.ts
test/server/httpapi-event.test.ts
test/server/httpapi-event-diagnostics.test.ts — 29/29 green
- bun run test test/server/httpapi-sdk.test.ts -t "streams sync-backed" — green
Reduce avoidable setup costs in slow opencode tests while preserving reviewed coverage and recording the benchmark evidence for follow-up test-suite work.
Pasting into the prompt textarea left its layout stale until the next edit, so the visible content did not reflect the pasted text. Mark the layout dirty on paste and notify the content-change handler once the renderer is idle so the prompt updates immediately.