Skip to content

mcpx(events): the /v1/events resume contract silently breaks at since=0 and on a malformed since — a trimmed history reports no gap, and a bad cursor is coerced to 0 or MaxUint64 #1679

Closed

Description

@dezren39

Summary

The /v1/events resume contract — "a subscriber that missed events is told so, rather than silently given a stream with a hole in it" — breaks in two shapes on the same code path, so a resumable stream can be silently incomplete or silently mispositioned:

  • BUG-A (bus): an explicit since=0 resume never reports a gap, even after the retained history has been trimmed. The gap check in SubscribeFrom is gated on since > 0, so the one resume position that asks for everything from the start is the one position that can lose events 1..N with no gap signal.
  • BUG-B (daemon): a malformed since / Last-Event-ID is silently coerced to one of two opposite positions. handleEvents discards the strconv.ParseUint error, so a syntax error behaves exactly like since=0 (full retained replay) and an out-of-range value behaves like since=MaxUint64 (no replay, no gap, live-only) — neither is rejected and neither is signalled.

Evidence (verified on main d39e6111)

BUG-A

pkgs/mcpx/internal/events/events.go (sha256 370a935a…):

  • The package's promise, New (:~155): the history "is bounded… a subscriber gone longer than the history covers is told so rather than silently given a gap."
  • SubscribeFrom's own doc (:206-208): "gap is true when since predates the retained history, so the subscriber knows it missed something and can resynchronise instead of trusting a stream with a hole in it."
  • The implementation (:218) is narrower than both docs:
if since > 0 && len(h) > 0 && h[0].Seq > since+1 {
    gap = true
}
  • The daemon deliberately distinguishes position zero from absent (internal/daemon/server.go:1264-1265): "Present-but-zero replays everything retained; absent is live only", and emits the in-band event: gap frame (:1277-1280) only when SubscribeFrom returns gap=true.
  • So: daemon has published more than eventHistory (default 1024) events, a client connects with ?since=0 (or an SSE reconnect sends Last-Event-ID: 0), events 1..(first retained − 1) are gone, the replay starts mid-history, and no gap frame is emitted. The existing tests pin the neighbouring cases but not this one: TestAGapBeyondHistoryIsReportedRatherThanHidden uses since=1; TestAReplayLargerThanTheSubscriberBufferMissesNothing uses since=0 with everything still retained (h[0].Seq == 1, where the uniform formula h[0].Seq > since+1 also yields no gap).

Premise exercised this run (faithful mirror of the gap condition + retained()/trimming, probe-2026-10-28-laneA-eventsgap/premise.py in the fleet's hidden files): keep=3, seq 1..10 → retained [8,9,10]; CTRL since=1 → gap=true; CTRL since=0 with nothing trimmed → gap=false, all replayed; BUG since=0 after trim → replay [8,9,10], gap=false — events 1..7 silently missing.

BUG-B

pkgs/mcpx/internal/daemon/server.go:1262-1265:

raw := firstNonEmptyStr(r.Header.Get("Last-Event-ID"), q.Get("since"))
since, _ := strconv.ParseUint(raw, 10, 64)
// Present-but-zero replays everything retained; absent is live only.
sub, gap := s.Events.SubscribeFrom(f, since, raw != "")

The error is discarded. Per the installed Go stdlib source (strconv/atoi.go, ParseUint: a syntax error returns 0, a range error returns maxVal):

since value effective position result
banana, -1 0 (syntax error, discarded) full retained replay — as if the client asked from the start
99999999999999999999999999 MaxUint64 (range error, discarded) no replay, no gap — live-only forever, client believes it is caught up at a position that can never exist

Neither outcome is a 400, a gap frame, or a log line. Last-Event-ID is normally server-generated, but since is a documented query parameter of this endpoint (handler comment, server.go:~1243), so hand-written clients — the ones most likely to send a malformed value — get one of two silently opposite streams.

Disclosure

A Go probe covering BUG-A (probe_lanea_test.go, same hidden-files folder) was prepared but not executed: the laptop was unreachable this run (SSH tunnel closes immediately — the ongoing outage, cf. #1667) and verification probes are never compiled on the sandbox. Triage should run the probe first and narrow this issue if either traced verdict differs. BUG-A is a line-trace + premise-mirror verification; BUG-B additionally rests on the Go stdlib source read this run.

Why it matters

The gap signal is the only mechanism a /v1/events consumer has to know it must resynchronise instead of trusting the stream. BUG-A removes it for exactly the fresh-start resume (new client, reset cursor, Last-Event-ID: 0) once a busy daemon has rolled its 1024-event history — elicitation and server-lifecycle consumers then act on a partial history believing it complete. BUG-B makes a client bug (a bad cursor value) undetectable from either side: the daemon cannot log it and the client cannot distinguish the response from a legitimate one.

Acceptance criteria

  • SubscribeFrom(f, 0, true) on a bus whose retained history starts at seq > 1 returns gap=true; with nothing trimmed it still returns gap=false (existing tests keep passing, plus a new regression test for the trimmed zero case).
  • GET /v1/events?since=0 after the daemon's history has rolled emits the in-band event: gap frame.
  • A malformed since / Last-Event-ID (non-numeric, negative, or out of uint64 range) is rejected (e.g. 400) or otherwise explicitly signalled — never silently treated as 0 or MaxUint64; a test pins each malformed class.
  • The SubscribeFrom doc comment and the implementation agree on when gap is set, for since = 0, since inside the retained window, and since beyond it.

Filed by the fleet issue creator (lane A) with file:line evidence, premise log, and a prepared-but-unexecuted Go probe; see the agent note for the packet/state details.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/normalOrdinary priority (the default)
    app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Oct 28, 2026
  2. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator-code-a (issue creator, lane A)
    • Session / ID: cron job new-issue-creator-code-a, run 2026-10-28 08:35 CDT
    • Found via: filed this run
    • Task: Filed this issue after verifying both shapes this run on main d39e611 — line trace of pkgs/mcpx/internal/events/events.go:218 (gap check gated on since > 0) and pkgs/mcpx/internal/daemon/server.go:1263 (ParseUint error discarded), premise mirror exercised (CTRL since=1 gap=true; BUG since=0 after trim gap=false), Go stdlib ParseUint semantics read from the installed source. The prepared Go probe was NOT executed (laptop unreachable; probes are never compiled on the sandbox) — triage should run it first.
    • Expectation: effort/s — triage pins the gap formula for since=0 and the malformed-since response (400 vs signal), then the fix + regression tests.
    • Links: queue packet packet-1679.json (repo-health-code-pilot, triage)
  3. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 31, 2026
  4. 32 remaining items

  5. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Oct 31, 2026
  6. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

  7. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Oct 31, 2026
  8. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  9. dezren39 commented on Oct 31, 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)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions