Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
5853859
Conformance: row 15's kill claim is partial, and the sentinel obligat…
jeremy Aug 18, 2026
528a037
Conformance: an advance that arms a timer is rejected, in every driver
jeremy Aug 18, 2026
c36aaa9
Conformance: a straggler below the entry page's served id (fixture 31)
jeremy Aug 18, 2026
a1f5369
Conformance: the advance guard measured firings, not arming
jeremy Aug 19, 2026
5063508
Conformance: both corrections reach the schema, which is the contract
jeremy Aug 19, 2026
cd19804
Conformance: the kill matrix counted a partial kill as a kill
jeremy Aug 19, 2026
1812eee
Conformance: row 15 is not a partial kill, and one of its claims cann…
jeremy Aug 19, 2026
950591d
Conformance: a proof that has not landed is an obligation, not a proof
jeremy Aug 19, 2026
de50ac2
Conformance: the zero-egress obligation was scoped to what a harness …
jeremy Aug 19, 2026
524376d
Conformance: the spec still said the two things this branch stopped d…
jeremy Aug 19, 2026
9f8567d
Conformance: the corrected ceiling was still one frame short
jeremy Aug 19, 2026
636338f
Conformance: a prose "Tracked in #N" was a promise nothing verified
jeremy Aug 19, 2026
ed94105
Conformance: the registry gate was fail-open, in a gate that exists t…
jeremy Aug 19, 2026
5b4d175
Conformance: the arrival-strict gap closed, so its registry entry goe…
jeremy Aug 20, 2026
f97625a
Conformance: the advance guard was a heuristic wearing a MUST
jeremy Aug 20, 2026
b2b1110
Conformance: both tracked promises came due, so the registry empties
jeremy Aug 20, 2026
0e6ebf2
Take five review findings on the tier-2 corrections, and decline one
jeremy Aug 22, 2026
d8beb4d
Back the tracking registry with a detector for its canonical grammar
jeremy Aug 22, 2026
7f07755
Close the third round on the registry: leader by class, ceiling by lane
jeremy Aug 22, 2026
afd32f4
Absorb the merged base's tracking promises into the registry
jeremy Aug 22, 2026
e74b487
Say what DueWithin reads, type the registry's strings, split the egre…
jeremy Aug 22, 2026
2367dab
Weigh the decode copy in the ceiling, and retire the no-op rationale
jeremy Aug 22, 2026
42ff059
Close the ceiling at the decode chain's length, and diagnose bad YAML…
jeremy Aug 22, 2026
4d5382f
Give unparseable YAML the gate's own diagnosis
jeremy Aug 24, 2026
b042239
Cite #819 where the prose said G1b, and let the gate hold it open
jeremy Aug 24, 2026
cff23ae
Teach the sweep blockquotes, and give SPEC's Layer-1 assignment its c…
jeremy Aug 24, 2026
a4a66a5
Bound every ms field at 10 virtual years, in the schema and at load
jeremy Aug 25, 2026
d5e22d1
Commit the mention counts, so a covered file cannot absorb new promises
jeremy Aug 25, 2026
b9e61bf
Model scenario ms values as int64, end to end
jeremy Aug 25, 2026
53271c7
Scope the ceiling to Go, describe the registry's real unit, refuse du…
jeremy Aug 25, 2026
36da938
Drop a stray possessive in the ceiling's holder sentence
jeremy Aug 25, 2026
44ec527
The advance rendezvous is authored: expectTimers precedes it, at load
jeremy Aug 25, 2026
2ff4de3
A rendezvous must be able to mean settled, and a registry is one docu…
jeremy Aug 25, 2026
ee4975e
Presence is meaning for config durations, and the rendezvous is two s…
jeremy Aug 25, 2026
9598aea
Null is the third state, and the decode chain grew a link when it was…
jeremy Aug 25, 2026
0fa21f3
A registered file must be sweepable, and the rearm residue gets its name
jeremy Aug 25, 2026
d58d38b
Give the envelope its three states, and split the ceiling into what i…
jeremy Aug 25, 2026
001ce96
Judge ms numbers by mathematical value, as the schema does
jeremy Aug 25, 2026
27b50e8
Judge integrality on the literal, refuse quoted numbers, appease unco…
jeremy Aug 25, 2026
0987833
Judge a number's magnitude by its value, not its exponent's spelling
jeremy Aug 25, 2026
4377b37
Scope the peak to payload, and sanction the literal cap in the schema
jeremy Aug 25, 2026
66e7d7a
Bound the exponent before the place arithmetic can wrap
jeremy Aug 25, 2026
3a8fae8
Short-circuit zero before its exponent reaches a rational
jeremy Aug 25, 2026
b7d2839
Decide zero before the exponent is even parsed
jeremy Aug 25, 2026
6aab6e7
State the rendezvous guarantee at its true strength: flaky, never green
jeremy Aug 25, 2026
72ef91f
Point the row-15 redaction prose at the test that exists
jeremy Aug 25, 2026
4442bd1
Absorb the exact-head round's three suppressed notes
jeremy Aug 25, 2026
a220b04
Register main's two #818 tracking claims with the registry the rebase…
jeremy Sep 10, 2026
90a528d
Judge every fixture number literal once, at load, so the schema's all…
jeremy Sep 10, 2026
e553d74
Conformance: carry the shipped event shape into fixture 31, and bound…
jeremy Sep 16, 2026
67f317f
Conformance: retire the tracking entries that pointed at a closed issue
jeremy Sep 16, 2026
e85766a
Conformance: leave an event's details untouched by the integer walk, …
jeremy Sep 16, 2026
6a04137
Conformance: one rule for what the integer walk leaves alone, a close…
jeremy Sep 16, 2026
656dcab
Conformance: the opaque rule judges the status by value, capacities g…
jeremy Sep 16, 2026
358b89e
Conformance: the sendable close codes are the IANA registry's, 1012–1…
jeremy Sep 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,9 @@ jobs:
with:
ruby-version: "3.3"

# Offline and first: the gate below is a no-op whenever the allowlist
# references no issues, so this is what proves it rejects anything.
# Offline and first: a green run of the live gate below proves only that
# today's referenced issues are open, so this is what proves it REJECTS
# anything — closed issues, malformed rows, the fail-closed paths.
- name: Self-test the known-defect gate
run: make test-check-known-defect-issues-open

Expand Down
15 changes: 7 additions & 8 deletions MIGRATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -818,13 +818,12 @@ constructed. They now sleep the server's `Retry-After` — both wire forms,
delta-seconds and HTTP-date — in place of the backoff, with no jitter and no
ceiling beyond what the host can represent: a value the parser holds but the
host cannot schedule saturates at 2147483647 seconds (~68 years) rather than
wrapping negative, and that figure does not vary by architecture. A value too
large for the parser's own `int64` is treated as malformed instead and falls
through to the backoff curve, as it always did. That split is Go's: SPEC §6's
parsing algorithm says only to parse a positive integer, #793 states the
two-tier rule (unrepresentable → malformed, unschedulable → saturate) in §6
"Retry-After Honouring", and #799 tracks the cross-SDK convergence on
over-range values, which the six SDKs still answer differently.
wrapping negative, and that figure does not vary by architecture. Every
over-ceiling digit string saturates there, including one too large for the
parser's own `int64` — the earlier two-tier rule (unrepresentable → malformed,
unschedulable → saturate) is gone from every Go parser — and #799 tracks the
cross-SDK convergence on over-range values, which the six SDKs still answer
differently.

**Two behaviours changed for `DownloadURL` and the rate-limiter hook as well**,
because all three paths share `parseRetryAfter`: an HTTP-date's sub-second
Expand All @@ -834,7 +833,7 @@ the backoff curve; and a delta-seconds above the schedulable ceiling saturates
instead of wrapping. The wire operations those paths perform are unchanged, and they
already honoured the header on 429 — it is what the header parses to that
moved. Typed service methods run the generated retry loop, which has its own
copy of the parse and is untouched (#798).
copy of the parse; its clamping landed with #855 (the defect was #798).
Comment thread
jeremy marked this conversation as resolved.

**Wrong behaviour you get if you ignore it:** none, but the wait between
attempts on a throttled account can now be seconds or minutes where it used to
Expand Down
7 changes: 4 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,10 @@ check-known-defect-issues-open:
@echo "==> Checking known-defect tracking issues are open..."
@./scripts/check-known-defect-issues-open

# Drive that gate from outside. Its live run is a no-op today — the allowlist
# references no issues — so without this NOTHING exercises the closed-issue
# rejection, the fail-closed path, or the second reference shape. Offline: PATH
# Drive that gate from outside. Its live run verifies whatever the allowlist
# and registry currently reference, and a green run proves only that those
# issues are open — so without this NOTHING exercises the closed-issue
# rejection, the fail-closed paths, or the second reference shape. Offline: PATH
# is stripped to a stub `gh` answering from a canned table, because a self-test
# that asked GitHub would assert against whatever is true this morning.
#
Expand Down
155 changes: 144 additions & 11 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -3615,13 +3615,93 @@ Two dispatch clarifications, pinned:
(implementation-chosen; the Go reference uses 256). At capacity the pump **blocks** —
back-pressure propagates to the socket and TCP — rather than dropping: the
state-machine-owned live buffer is the only place a frame can ever be dropped, and its
overflow signal is the only drop signal. Worst-case connector memory is therefore
bounded multiplicatively — every queued or buffered item is itself bounded by
`EVENT_FEED_MAX_FRAME_BYTES`, so the ceiling is
(pump depth + `EVENT_FEED_LIVE_BUFFER_CAPACITY`) × `EVENT_FEED_MAX_FRAME_BYTES`
overflow signal is the only drop signal. **The retention ceiling below is the GO
REFERENCE IMPLEMENTATION'S**, stated in its own terms — two goroutines, a
`json.RawMessage` copy, a copying decoder — **and it presumes a transport with
bounded reads**. Every SDK's cable lane inherits the shape (bounded queue, blocking
hand-off, single deferral slot, the buffer as the only drop point) but re-derives its
own weights, and one recorded divergence already breaks the per-item premise
elsewhere: TypeScript's default global-`WebSocket` lane cannot pre-bound a read, so a
single oversized message is allocated whole at receipt, before the
`EVENT_FEED_MAX_FRAME_BYTES` check drops it — the scenario-lane table in Appendix F
records that as an accepted divergence, and no universal cross-SDK byte ceiling is
published here. The ceiling also counts FRAMES, not errors: exactly one non-frame item
can ride the queue or the deferral slot — the read error that ends the pump, at most
one per attempt because the pump exits by sending it — and its SIZE is
transport-authored, unbounded by the seam contract. The built-in transport's errors
are bounded by construction (fixed shapes whose renderings are configured text or
placeholders, never server bytes); a custom transport's error is its author's to
bound. In the Go reference the accounting SPLITS, because two kinds of memory answer
different questions.

**RETAINED storage is what a consumer sizes against** — raw frames and buffered
events held across blocking points, an enumeration by HOLDER, which is what closes
the count: the hand-off queue (≤ pump depth), the live buffer
(≤ `EVENT_FEED_LIVE_BUFFER_CAPACITY`), the single deferral slot (≤ 1), and one
in-hand frame for each of the exactly two goroutines that touch frames. The retained
worst case is
(pump depth + 3 + `EVENT_FEED_LIVE_BUFFER_CAPACITY`) × `EVENT_FEED_MAX_FRAME_BYTES`
Comment thread
jeremy marked this conversation as resolved.
(≈ 10 GiB at the defaults' extreme, reached only if every slot holds a maximum-size
frame) — even under a slow consumer. Implementations MAY additionally impose a total
byte cap on the live buffer; if they do, eviction routes through the same overflow
frame) — even under a slow consumer. The **+ 3** is three frame-sized retentions the
queue's depth does not count, held by different parties at the same time:
- the **pump's own in-flight frame** — the pump is a single reader, so it may hold exactly
one frame it has already READ and not yet handed off. One rather than an unbounded
number for that reason: one reader holds at most one frame outside the queue.
- the **state machine's in-hand frame** — the protocol-fatal scan's dequeue is the very
receive that lets a blocked pump refill the queue, so while the scan still holds that
frame — examining, admitting, or parking it — the queue is full again and the pump may
already hold its next read. A single consumer, so one frame in hand, for the pump's
own reason.
- the **deferred socket outcome** — the single slot the in-flight-poll servicing and the
drain's scan park one receive in. It is retained while the queue behind it refills, so it
is concurrent with a full queue and with both in-hand frames, not an alternative to
any of them.

**TRANSIENT decode-time allocation rides on top, per frame, in the state machine's
hands alone** (the pump never parses), bounded by a small implementation-topology
multiple of the frame being decoded rather than by a published constant. The multiple
covers the decode chain's representations — the wire bytes, `parseFrame`'s envelope
`json.RawMessage` copy, `decodeMessageEvent`'s per-field
`map[string]json.RawMessage`, the decoded `Event`'s strings — plus decoder overhead
proportional to member count: map bucket storage and copied keys, which a frame of
many tiny members inflates past any per-representation count. That is exactly why
the multiplier is NOT published: it is implementation topology, not contract — it
moved when the exact-spelling per-field decode was coded in, and pinning a number
would turn every decoder refactor into a spec change. What IS contract: transients
exist only between a frame's dequeue and its decode returning, one frame at a time,
so **peak frame-payload retention is the retained formula plus ONE frame's
transient allocation** — never a per-slot or per-queue multiplier. Payload, not
process memory: the equality deliberately excludes the two things it cannot
bound — the single transport-authored error item, whose size is its author's
(stated above), and runtime metadata (channel and `pumpItem` storage, slice and
map headers), which scales with the configured capacities, not with frame
bytes.

The retained enumeration cannot grow by a further party being noticed: every
retained frame is in one of the three counted structures or in the hands of the pump
or the state machine. The live buffer's weight is one per slot: a buffered `Event`
retains only the chain's LAST representation — its strings are copies, since Go's
decoder never aliases its input buffer, and its `details` bytes are the decoder's own
clone of that member (a bounded slice of the frame, retained per slot alongside the
strings; never the frame itself) — so no transient survives admission, and a slot's
weight is the decoded event, details included.

The formula is the cable lane's retention, and only that — every counted item is a
raw socket frame or a buffered live event. The poll lane sits outside it on purpose:
`PollSource.Poll` returns one page decoded whole, and the walk retains that page
until its rows are delivered. What bounds it is shape, not size: pages are fetched
sequentially, so a walk holds at most one live page (a superseded attempt's in-flight
poll may briefly hold another before its result is discarded), but the page's SIZE is
the server's pagination decision — `EVENT_FEED_MAX_FRAME_BYTES` governs socket
frames and says nothing about an HTTP body the generated layer decodes. A
total-connector memory bound would need a poll-page cap this contract deliberately
does not impose.

The drain's protocol-fatal scan is budgeted at `pump depth + 1` and not at this figure,
which is not an inconsistency: the budget counts what the scan may DEQUEUE — the queue plus
the pump's held frame — while the ceiling counts what may be RETAINED, and the deferral slot
is retained without being dequeued by that scan. Implementations MAY additionally impose a
total byte cap on the live buffer; if they do, eviction routes through the same overflow
signal, never a silent drop.
- The transport negotiates subprotocol `actioncable-v1-json`, sends no `Origin` header
(non-browser clients), and passes the mint URL through untouched, query string included.
Expand Down Expand Up @@ -3991,9 +4071,26 @@ logged (Security Invariants below).

Required tier-2 coverage: a hostile cross-origin `next` mid-walk, a hostile 410
`resume` URL, and a validated same-origin `next` answering 302 with a cross-origin
`Location` each terminate with `invalid_continuation` and zero requests to the foreign
origin; store-failure coverage proves Failed(load) terminates with zero wire attempts and
Failed(save) continues with the observer signal and a subsequent save attempt.
`Location` each terminate with `invalid_continuation`, are not retried, and issue no
further poll; store-failure coverage proves Failed(load) terminates with zero wire
attempts and Failed(save) continues with the observer signal and a subsequent save
attempt.

**Zero egress to the foreign origin splits at the seam.** For the hostile `next` and
`resume` cases the target is connector-visible and tier 2 owns the coverage: a
connector that follows one hands the URL to the poll seam, which the driver observes
and fails — fixtures 26/27 assert zero requests to those hosts, structurally (no step
ever serves them, and the harness's servers own only their own origins). For the
redirect the obligation is Layer-1's, and this paragraph used to require it at tier 2:
the poll lane IS the seam, so the driver reduces the `Location` to its origin and
hands the connector a refusal verdict. The connector never sees a `Location` and never
decides whether to follow one, which makes the foreign origin unreachable by
construction of the harness — a harness that asserted no request reached it would be
asserting something about itself. That obligation belongs to the Layer-1 seam
adapter's own 302 test, where a real generated `PollEvents` call meets a real redirect
against an adapter with automatic redirect-following disabled.
`conformance/event-feed/README.md`'s row-15 note records it as a pending obligation
rather than a proof the repository contains; the adapters are tracked in #819.

### Clock, Timers, and Virtual Time `[conformance]`

Expand Down Expand Up @@ -4048,8 +4145,44 @@ the advance whose deadlines land inside the window also fire; ties break by crea
order.* A harness may additionally fire a named timer without advancing the clock,
asserting its scheduled delay against a `{min, max}` envelope — that is how jitter is
asserted without a cross-language RNG seam. Each language's test clock passes a shared
semantics checklist (deadline order, reentrant scheduling within an advance, creation-order
tie-break) before its tier-2 results count.
semantics checklist (deadline order, creation-order tie-break) before its tier-2 results
count.

**The reentrant clause is normative for the algorithm and forbidden as a fixture
dependency.** It stays in the algorithm because a clock that ignored it would fire the
wrong set. But it is UNSCRIPTABLE wherever the connector runs concurrently with the
driver: whether a timer armed during the window lands inside it depends on when the
connector's goroutine, thread, or task got scheduled, which no fixture can pin. So **no
fixture may rely on it, and every driver MUST REJECT an `advance` whose window would fire
any timer**, naming `fireTimer` as the deterministic alternative.

The test is what would FIRE, decided from the clock's state before time moves — not what
gets ARMED. Arming happens on the connector's schedule, so a driver can only look for it
by waiting and then assuming nothing further is coming, which is a heuristic wearing a
MUST and passes a late arm in silence. Firing is one atomic read under the same lock the
advance selects under. The inversion is sound because a test clock releases that lock only
across a firing's aftermath — so an advance that fires nothing never wakes anything and
cannot cause an arm, leaving nothing to detect. It is stricter than an arming rule (a
firing that replaces nothing is rejected too) and that is the trade: a script wanting that
firing writes `fireTimer` and names the timer. The due-set read also needs a settled set
to read — an action's completion can precede the timer arms its transition causes — so
every `advance` must be the scenario's first step or immediately follow the two-step
rendezvous `expectState` then `expectTimers`, enforced at fixture load (an empty
rendezvous set is rejected with it — it orders nothing). Neither step alone settles: a
set match can coincide with a transient mid-surgery set (timer surgery spans clock
acquisitions), and an announcement can precede a tail arm. Together they do — the
announcement bounds the surgery, and any timer still unarmed at the announcement is
exactly what the exact-set match then waits for, both blocking under the watchdog so
wrong authorship fails loudly on every schedule where the stale pair no longer holds —
a pre-action pair can pass on the schedule where the action is not yet processed, so a
wrong script is at worst flaky, never stably green; the settled guarantee is for
correctly authored pairs. A transition that announces no state change, or only
rearms a timer of the same kind and count, is invisible to this rendezvous — a served
live frame's pump-side `staleness` rearm is the concrete case. Such a script overrides
`stalenessMs` large so no deadline, old or new, sits inside a window it advances (the
schema's own guidance, and what the suite's one advance does), or uses `fireTimer` for
the firing it actually wants. `conformance/event-feed/schema.json`'s
`$defs.advance` states both, and the driver obligation is enforced there.

Teardown discipline: disposing a connection attempt — deadline lapse, staleness, socket
death, terminal — cancels the frame pump, **cancels any in-flight seam call belonging to
Expand Down
Loading
Loading