Skip to content

mcpx: /v1/events replay drops everything past 256 events without a gap marker, though reconnection is documented as lossless #1914

Closed

Description

@dezren39

Relocated from dezren39/darwin-nix#193 on 2026-10-28 (placement audit): mcpx's single source lives in developing-today/code (pkgs/mcpx); re-home tracked at #309. Original body below; issue references inside it are historical (dezren39/nix numbering).

Parent: #177

Summary

GET /v1/events promises lossless reconnection: "Reconnect with Last-Event-ID ... or ?since=N and everything
after N is replayed", and a reconnect older than the retained history "gets an in-band gap event"
(docs/story.md:2203-2214). A replay of more than 256 matching events keeps the oldest 256 and silently drops the
rest, with no gap event. The retained history is 1024 by default (events.history), so a ?since=0 against a busy
daemon can lose up to three quarters of what it asked for, and the client is told nothing.

Found by reading the code in the 2026-09-30 issues pass (worker "ext", confirmed by the architect). Not yet
reproduced against a running daemon
; the first step is the reproduction.

Current state (05c78b2)

Bus.SubscribeFrom (internal/events/events.go:176-199):

  • makes the subscription channel with a buffer of 256 (:177);
  • holds the bus lock for the whole replay (:178-179), and the subscription has not been returned yet, so nothing
    can drain the channel while the replay fills it;
  • replays each retained event into the channel with a non-blocking send; when the buffer is full it increments
    sub.Dropped and moves on (:190-194);
  • sets gap only when since predates the retained history (:185-187), never for drops.

Dropped has no reader outside internal/events (:147, :193, :230), and the daemon's handler
(internal/daemon/server.go:826) sees only gap. The same counter is raised for a slow live reader (:230), which
is the documented trade-off ("one slow reader cannot stall the daemon"); the replay case is different because it
happens before the reader has had any chance to read.

Approaches

A -- enlarge the buffer to events.history. One line; replays of up to the whole history fit. Costs memory per
subscriber (1024 events each) and still loses silently if a live burst arrives before the reader drains; the promise
remains unguarded.

B -- replay outside the lock, from a snapshot, with blocking sends. Copy the matching history under the lock,
register the subscription, then feed the snapshot to the reader at its own pace before live events. Lossless for the
replay; live-burst loss still silent.

C -- recommended: B, plus make every loss visible with the gap contract #6 owns. When Dropped rises -- during
replay or live -- emit the in-band gap {since, latest} marker #6 defines (#181 collision 13), so a consumer always
knows it missed something and can resynchronise with ?since=. Add the test that would have caught this: replay more
than the buffer and assert either every event or a gap. Recommended because "lossless" is a documented promise and
the only honest versions of it are "you got everything" or "you were told you did not".

Open questions

  • Should /v1/events report Dropped in its stream or only as the gap marker?
  • Does the MCP notification path (internal/pool/pool.go:411) share the bus and the same failure?

Ease · Impact · Related

Ease S (A, B) / M (C, with #6's gap contract) · Impact med -- silent loss on the one path documented as
lossless; #81's hooks and #86's browser surface consume this stream.
Related: #177 (class), #6 (owns gap semantics, collision 13), #81 (hooks over the bus), #86 (browser reconnect,
Last-Event-ID), #87 (lifetime), #129 (guard mechanics).

Security/auth deferred; see tracker.

Acceptance criteria

  • Reproduction first: a replay through Bus.SubscribeFrom with more than 256 matching retained events demonstrates the silent drop on current code (the issue is code-read, not yet reproduced against a running daemon).
  • After the fix, a replay larger than the channel buffer loses nothing silently: either all retained events after since are delivered, or the client receives an in-band gap event naming the loss — matching the docs/story.md reconnection promise.
  • The Dropped counter has a reader that surfaces drops (gap event, metric, or log), including drops after the subscription is live.
  • A ?since=0 replay against a full 1024-event history is covered by a test; tests pass in CI.

Activity

  1. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  2. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    History carried over from dezren39/darwin-nix#193 (verbatim, oldest first):


    @dezren39 · 2026-10-19T11:49:30Z

    Partial fix landed — reopening for the remainder (the auto-close came from the PR body saying "does not close" this issue; GitHub's keyword parser matched the verb anyway).

    Done (replay half, approach B's losslessness): PR #460 merged here (a047f67d) + the identical mcpx port in developing-today/code PR #351 merged (0111161), as one cross-repo set.

    • Reproduced on pre-fix main in both copies: publish 300 events, SubscribeFrom(since=0, replay=true) → received 256, Dropped=44, gap=false — silent loss, exactly as filed.
    • Fix: SubscribeFrom now collects the matching replay under the lock, sizes the subscription channel for the whole replay plus the usual live buffer (bounded by the retained history), and sets gap if a replay send ever still cannot land.
    • Regression test TestAReplayLargerThanTheSubscriberBufferMissesNothing (300-event replay, all in order, Dropped=0): fails pre-fix, passes post-fix; events/daemon/pool suites green locally in both repos; both PRs' CI fully green incl. all conformance legs and server-all.

    Remaining (approach C's second half): drops of live events for a slow reader are still only counted on Subscription.Dropped, not emitted as an in-band mid-stream gap {since, latest} marker. That visibility depends on the gap contract owned by #6, so this issue stays open for it.


    @dezren39 · 2026-10-28T05:41:53Z

    Intake review — fleet monitor

    Liveness: Verified still live at intake: the issue is open and no merged PR closes it. Evidence caveat (same as the rest of this register batch): the filer's evidence was verified against 05c78b2 (2026-09-30) — re-verify against current main before fixing; if a row has since been fixed, say so on the issue and narrow the scope instead of redoing it.

    Related: Parent tracker named in the issue body; sibling register-batch issues in dezren39/nix (the #209–#220 batch was enriched in the previous monitor run). mcpx also lives in developing-today/code — check for a code-side twin before assuming a nix-only fix.

    Notes / risks: Flagged honestly in the issue: found by code reading, not yet reproduced against a running daemon. The first acceptance criterion is the reproduction; if the drop cannot be reproduced, close with that evidence instead of fixing blind.

    Suggested approach: Re-verify the cited code paths on current main, land failing tests for the acceptance criteria first, then the smallest fix that satisfies them.

    🤖 Agent note · state: filed

    • Agent: fleet monitor
    • Session / ID: repo-fleet-monitor run 2026-10-28 00:20 CDT
    • Found via: monitor reconciliation
    • Task: Intake enrichment (§5 pass): acceptance criteria appended, labels normalized, this review posted.
    • Expectation: effort/s as labelled; done = the acceptance criteria on the issue are met.
  3. added
    bugSomething isn't working
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    source/humanFiled directly by a human
    priority/normalOrdinary priority (the default)
    effort/sSmall: expected under 2 hours of work
    meta/enrichedIntake review done: acceptance criteria, links, and normalized labels added
    app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 28, 2026
  4. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    Cross-reference from triage of #1850 (queue task 2740, 2026-10-29): #1850's 'Gap semantics' section describes this same defect (replay past the 256-slot channel silently drops events, contradicting the lossless-reconnection promise) as the design question its event store would inherit. The defect itself is tracked here (#1914, and #1679 for the since=0/malformed-cursor face) — #1850 remains open only for the event-store design/implementation, which should consume whatever gap semantics is decided here rather than inventing its own.

  5. removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 29, 2026
  6. 89 remaining items

  7. removed
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Oct 30, 2026
  8. added a commit that references this issue on Oct 30, 2026
  9. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    Agent note — done (queue task 3192)

  10. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Oct 30, 2026
  11. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  12. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)bugSomething isn't workingeffort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)meta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedpriority/normalOrdinary priority (the default)source/humanFiled directly by a human

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions