Repository navigation
feat(server): durable log reads, changes feed, and watermarked snapshots - #34962
Merged
Merged
Conversation
kitlangton
force-pushed
the
event-api-contracts
branch
from
July 2, 2026 19:07
74b401f to
d7d0b6b
Compare
This was referenced Jul 19, 2026
jinhuang712
pushed a commit
to jinhuang712/opencode
that referenced
this pull request
Sep 26, 2026
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.
Reshapes the V2 event surface into three explicit contracts, replacing the "one stream, three jobs" shape. The old durable-read endpoints are deleted outright:
session.historyandsession.eventsare subsumed by the log read, and V2 is experimental with one known external consumer (being migrated next).RFC:
anomalyco/experiments—agents/docs/rfc-opencode-event-api-and-storage-seam.md(§3 is normative for the contracts here).Why
Two production consumers were audited against the current API:
message.refreshthat drops concurrent events, and overflow disconnects that leave open transcripts silently stale. The web app built hundreds of lines of bespoke fetch-vs-event reconciliation.All of these reduce to the same two gaps: durable data riding a lossy channel, and snapshots that share no coordinate system with the stream.
The three contracts
1. The Log —
GET /api/session/:id/log?after&followDurable, ordered, gap-free read of public session events after an exclusive
aftercursor — the only event API allowed to promise reliability.follow=falsereplays to the end of the log and completes (replacessession.history);follow=truetransitions to live (replacessession.events). Both old endpoints are deleted in this PR.log.caught_upmarker at the replay boundary carrying the aggregate watermark, so even a bare read yields an attach point. The marker reports the true log position even when trailing events were filtered.EventV2.logsubscribes the aggregate wake before the historical read, so events committed during replay either appear in the replay or arrive after the marker (RFC §6 Q2). Reads skip event types missing from the durable manifest instead of failing, preservinghistory's filtered-gap tolerance; raw tail seqs keep cursors advancing across the gaps.2. The Changes feed —
GET /api/event/changesPayload-free hint channel:
{ type: "log.hint", aggregateID, seq } | { type: "log.sweep_required" }.SubscriberOverflowErrorfailure mode does not exist on this channel by design.LayerOptions.changesKeyCapacity); exceeding it abandons the buffer and emitssweep_required— the honest escape hatch (Kubernetes410 Gonelesson).sweep_requiredis also emitted first on every (re)subscribe, making the "hints missed while disconnected were never buffered" contract self-enforcing.3. Watermarked snapshots
session.listandsession.activeresponses gainwatermarks: Record<Session.ID, seq>;session.messagesgains a response-levelwatermark.resourceVersionprotocol: UIs do fetch →log(after: watermark, follow: true)with no race window, and guard snapshot application withwatermark >= appliedSeq.session.listpairs rows and seqs in one transaction (exact),session.messagesreads the watermark before the snapshot (understate-safe), andsession.activeis documented advisory since activity is process state, not log-derived.Branded coordinates
Event.Seq(log position) andEvent.Version(envelope schema version) are branded ints across the durable envelope, markers, hints, watermarks, andaftercursors.afteronly accepts values that came from the log/watermarks — and seq can't be confused with the message-row seq space, nor transposed withversionin envelope literals. Authoring stays plain (durable: { version: 1 }); the brand applies at the envelope boundary. Core service inputs accept plainnumber; discipline is enforced at the protocol layer, and the effect client inherits the brands.Deleted
GET /api/session/:id/historyandGET /api/session/:id/event, their handlers,SessionV2.history/SessionV2.events,EventV2.durable, andEventV2.readAggregate. All their test coverage migrated tolog-based equivalents (filtered gaps, exclusive cursors, replay/live handoff, wake coalescing).event.subscribesurvives unchanged as the volatile live channel, now documented as such.Ephemeral inventory (RFC §6 Q5/Q6)
Already resolved in the codebase: all
*.deltaevents,ExecutionSettled, andserver.connectedare live-only by definition (session-event.ts). No durable consumer replays deltas.Out of scope (deliberate)
EventStorestorage seam / Postgres driver (RFC §4) — separate PR after this surface settles.session.active; the downstream agent migrates tochanges+login a follow-up).Notes
promise.test.tsAPI-group pin that was already failing onv2— separable.schema/test/event-manifest.test.tspublic-surface count drift (63 → 66 on base).filesystem/watchertimeouts under full-suite load (identical on base; pass in isolation).bun run generate; the generator stopped unwrapping.dataforsession.activenow that the response carrieswatermarks. The legacypackages/sdkOpenAPI artifact regenerates through V1 tooling and was not touched, matching upstream practice.Verification
core/test/event.test.ts(caught-up boundary both modes, empty-log marker, coalescing keeps max seq per key, sweep on retention exhaustion, sweep-first on subscribe,sequences, replay/live handoff and wake coalescing throughlog),core/test/session-log.test.ts(public filtering vs aggregate watermark, live follow, NotFound, undecodable gaps, migrated empty sessions, list watermark transactional pairing).