Skip to content

Event feed connector: the run loop and tier-2 driver (2/3) - #705

Open
jeremy wants to merge 36 commits into
event-feed-foundationsfrom
event-feed-go-connector
Open

Event feed connector: the run loop and tier-2 driver (2/3)#705
jeremy wants to merge 36 commits into
event-feed-foundationsfrom
event-feed-go-connector

Conversation

@jeremy

@jeremy jeremy commented Aug 12, 2026

Copy link
Copy Markdown
Member

This PR has been split and force-pushed. It now carries the state machine
only, and is stacked on #777 (foundations). #778 (conformance corrections)
stacks on this. The pre-split head is preserved at tag pre-split/705-head;
the exact commit this PR pointed at before the force-push is
pre-split/705-remote-head (d379f2e11). All 63 review threads are intact,
but line anchors on foundation files now resolve against #777.

Why: eight bot rounds here did not converge (12→3→5→2→2→1→3 threads, with late
findings in files no earlier round had touched), and a review pass then found a
P1 credential defect all eight missed because it composes two files across a
package boundary. That defect is fixed in #777.

The Go reference implementation of the SPEC.md §23 Event Feed connector — BC3's
account-wide event feed over Action Cable push plus polling catch-up — and the
tier-2 conformance driver. All 22 fixtures pass (23 with #778's addition).

Layer 2 only: the connector reaches the wire through TicketMinter /
PollSource seams and one sanctioned cable dial (AGENTS.md Hard Rule 2). The
Layer-1 adapters are deferred to G1b, so the package is experimental and a
consumer supplies the seams today.

What is here

connector.go (New, the options, Events, Close), loop.go (states,
transitions, timers, live buffer), catchup.go (the poll walk, its page
boundary, the drain), recovery.go (the 400/409/410 matrix), and the tier-2
driver with its fixture model, harness and self-tests.

The foundations — seams, wire types, transports, filters, checkpoint identity,
the file store, dedupe, backoff, clock, cable codec, feedtest/ — are #777.

Rebase note

The 29 commits were re-cut, not rebased. Rebasing onto #777 was attempted
and abandoned: 8 of them touch only foundation files and would replay empty, and
21 of the remaining 22 mix both halves, so every one conflicts against the four
fixes #777 applies. Resolving 22 interleaved conflicts by hand is a worse
guarantee than re-cutting, which is byte-exact by construction — the foundation
paths come from #777's tip, these 17 from the pre-split head, verified with an
empty git diff against both.

The five fixes on top

Blocker 2 — a poll page carrying no position is malformed

The walk took page.Position on trust, and an empty one silently skipped
history in two different ways.

Position-resume: acceptPosition("") sets l.position = "", and
entryCursor selects on l.position != "" — so it does not preserve the old
cursor, it falls through to a bare present entry. The feed resumes at the
server's head with everything between skipped, reporting nothing.

Present-class is worse. held uses "" as its sentinel for "the final entry
was not present-class", so an empty position is not saved as empty — it
collapses into the sentinel, the held != "" guard skips acceptPosition and
saveCheckpoint outright, and caught_up announces anyway. The position is
discarded, with the drain's deliveries already handed to the consumer.

Refused before delivery, counter resets, and every mutation. That placement is
what the mutation check exercises: moving the guard after the delivery loop
fails three of four subtests on delivered ids = [101], want [].

Blocker 6 — narrow the promise, and make Wait the quiescence point

Close's doc claimed "no seam call and no delivery can begin after Close has
returned". That is not true and cannot be made true: the run goroutine checks
its context at each dispatch point and then acts, so a Close landing in between
cannot stop the call from starting — only from starting on a live context.
Closing that window means holding a lock across arbitrary host code, trading a
benign race for a deadlock reachable from any callback.

The one effect that outlives the process is the checkpoint save, and this PR
deliberately does not gate it. An accepted page's position saves even when
Close lands first — its events were already delivered, and dropping the write
would silently re-deliver them — and the save runs under a context detached
from the run's cancellation (context.WithoutCancel: values kept, cancellation
dropped), because a store that honors its ctx is compliant and would
otherwise lose the position Close raced. A save decided just before Close can
land just after it; that is intended, not residual.

Ordering a second connector over the same store is therefore the consumer's
to do, and Wait is the tool: it blocks until the run goroutine has exited, so
no save can be in flight — by construction, not by a narrow window. Await the
iterator's termination, or Wait, before opening a second connector over the
same checkpoint store. (An earlier revision ordered saves through a
durableGate claimed inside Close and tracked the unclosable [claim, write]
residual in #784; the gate is gone, Wait replaces it, and #784 is closed with
it.)

Also: a cancelled checkpoint load is no longer Terminal(checkpoint_load).

Blocker 7 — observers see origins only

#777's redactor applied to Observer.Gap and both CatchUpStarted sites. An
accepted 410 latches the server's resume URL as reconnect state, so the
reconnect announces its walk carrying it — redacting Gap alone would have left
the identical URL leaving through a different callback one reconnect later.

Observer.Disconnected is redacted, and this paragraph used to say the
opposite. The original reasoning — an error is opaque text, and stripping a
credential out of arbitrary text means modelling the credential, which §23's
"opaque bearer" contract forbids — argued for leaving it alone. A later round
found a ticket reaching the callback through the raw seam read error, which no
seam obligation can repair from the connector's side, so both arguments now go
through closed vocabularies: observableDisconnectReason maps every
unrecognized peer reason to "other", and observableSocketError reduces every
cause to one the connector owns, degrading anything unrecognized to
errSocketFailed.

"Reduces" is load-bearing and was the last hole: matching a sentinel is not the
same as being one, so a seam returning fmt.Errorf("read %s: %w", cableURL, context.Canceled) matched a recognized arm while its text carried the ticket.
Every arm now returns the connector's own value rather than its argument;
errors.Is still matches, and the wrapper does not survive.

The seam obligation remains on Dial, ReadFrame and WriteFrame — it is what
keeps a preserved typed error safe — but the connector no longer depends on it
being honored.

#763 — staleness arms at socket open

Observer.Connected fired between the socket opening and the window that
measures silence on it. Whatever a host's callback spent was time the window
never counted. Observed from inside the callback, which is the only place the
ordering is visible.

#760 — an occupied deferral slot no longer blinds the drain's fatal scan

drainScan returned the moment it found the slot occupied, on the reasoning
that everything queued "arrived behind this one". That is a claim about arrival
order, where §23's carve-out is a claim about which verdict governs. With
any non-fatal outcome parked ahead of it the scan looked at nothing — and the
budget it runs under is pumpDepth+1, sized in drain's own comment to reach
every frame the pump had already read. An occupied slot spent none of it.

The planned fix was a bounded deferral queue carved out of pumpDepth; that
turned out to be unnecessary.
Only one deferral is ever dispatched —
pumpExited, dispatchDisconnect and the invalid-frame teardown all end the
cycle — so a queue would be a buffer sized for a delivery that cannot happen.
The scan keeps the first outcome and discards the rest as it passes them, which
removes the capacity question entirely: no share of pumpDepth, no channel
resize, no change to the published memory bound, no fixture sweep.

The new subtest needs both halves of the two around it: deferring during the
entry poll occupies the slot, and queuing the fatal frame mid-drain puts it
where only the scan can find it. Queued earlier, the ownership cut consumes it
first — which is how the first draft passed against un-fixed code.

On #758/#759: I could not reproduce the missing-wake-source hang. Every path
leaves a wake. The overshoot is real (close to two staleness windows) and
provably cannot exceed two, so the bound is documented in place rather than
patched, and carried to bc3 as an open §23 contract question.

Verification

Pristine worktree, one pass, clean tree before and after:

  • go build / go vet / -race -count=1 / -count=5 — pass
  • make go-lint 0 issues; gosec on the CI-pinned v2.23.0 (hash-verified) 0 issues
  • full make checkexit 0
  • 22/22 fixtures
  • TestWalkFailureBetweenPages both subtests — the invariant that killed the
    reviewers' one-liner survives
  • staleness soak 8 × 500 under -race: 0 failures, 0 data races, re-earned
    because this PR rewrites the files that wait

Kill-matrix correction

Row 15's claim was inherited from the family README and is wrong; #778
corrects it. Tier 2 cannot prove zero egress to a foreign redirect target,
because the driver is the seam and manufactures the verdict. That is a
Layer-1 property, tracked for G1b.

Copilot AI balanced review requested due to automatic review settings August 12, 2026 02:40
@github-actions github-actions Bot added the go label Aug 12, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds the experimental Go Event Feed reference connector, deterministic conformance infrastructure, WebSocket transport, and checkpoint persistence.

Changes:

  • Implements the push/poll state machine, recovery, deduplication, and checkpointing.
  • Adds real and fake transports plus tier-2/tier-3 conformance tests.
  • Documents the experimental API and architecture exception.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Reviewed changes

Copilot reviewed 54 out of 55 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
AGENTS.md Registers Event Feed infrastructure.
CONTRIBUTING.md Documents conformance verification.
go/README.md Adds Event Feed usage guidance.
go/go.mod Adds WebSocket dependency.
go/go.sum Locks WebSocket dependency.
eventfeed/backoff.go Implements retry timing.
eventfeed/backoff_test.go Tests retry timing.
eventfeed/cable.go Implements cable framing.
eventfeed/cable_test.go Tests cable framing.
eventfeed/catchup.go Implements catch-up and streaming.
eventfeed/catchup_test.go Tests catch-up behavior.
eventfeed/checkpoint.go Defines checkpoint contracts.
eventfeed/clock.go Implements production timers.
eventfeed/clock_test.go Tests production timers.
eventfeed/connector.go Defines the public connector.
eventfeed/connector_test.go Tests connector construction.
eventfeed/continuation.go Validates continuation URLs.
eventfeed/dedupe.go Implements delivered-ID deduplication.
eventfeed/dedupe_test.go Tests deduplication.
eventfeed/digest.go Implements filter digests.
eventfeed/digest_test.go Tests shared digest vectors.
eventfeed/doc.go Documents the package contract.
eventfeed/errors.go Defines terminal errors.
eventfeed/errors_test.go Tests error taxonomy.
eventfeed/event.go Defines feed events.
eventfeed/event_test.go Tests event decoding.
eventfeed/export_test.go Exposes test-only hooks.
eventfeed/filestore.go Implements file checkpoints.
eventfeed/filestore_test.go Tests file checkpoints.
eventfeed/filters.go Defines filter validation.
eventfeed/filters_test.go Tests filter validation.
eventfeed/loop.go Implements connector lifecycle.
eventfeed/loop_test.go Tests lifecycle behavior.
eventfeed/reconnect_test.go Tests reconnect and staleness.
eventfeed/recovery.go Implements poll recovery.
eventfeed/recovery_test.go Tests recovery paths.
eventfeed/scenario_conformance_test.go Replays conformance fixtures.
eventfeed/scenario_fixture_test.go Decodes fixture contracts.
eventfeed/scenario_harness_test.go Provides scenario harnessing.
eventfeed/scenario_selftest_test.go Tests driver strictness.
eventfeed/seams.go Defines connector seams.
eventfeed/transport.go Enforces cable URL policy.
eventfeed/transport_contract_test.go Defines transport contract tests.
eventfeed/transport_test.go Tests cable URL policy.
eventfeed/websocket_transport.go Implements WebSocket transport.
eventfeed/websocket_transport_test.go Tests real WebSocket behavior.
eventfeed/feedtest/clock.go Adds deterministic virtual time.
eventfeed/feedtest/clock_test.go Tests virtual time.
eventfeed/feedtest/minter.go Adds scripted ticket minting.
eventfeed/feedtest/minter_test.go Tests scripted minting.
eventfeed/feedtest/polls.go Adds scripted polling.
eventfeed/feedtest/polls_test.go Tests scripted polling.
eventfeed/feedtest/store.go Adds scripted checkpoints.
eventfeed/feedtest/transport.go Adds scripted cable transport.
eventfeed/feedtest/transport_test.go Tests scripted transport.
Suppressed comments (1)

go/pkg/basecamp/eventfeed/filestore.go:230

  • The rename is atomic but not durable without syncing the staged file and parent directory. After a system crash, the first checkpoint file can disappear; the next Load then reports Missing and starts at the present, which can skip history rather than merely replay from an older position. Since Save and the package advertise durable checkpointing, sync the file before rename and the directory after rename, or stop claiming crash durability and avoid treating disappearance as a safe present entry.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread go/pkg/basecamp/eventfeed/catchup.go Outdated
Comment thread go/pkg/basecamp/eventfeed/recovery.go
Comment thread go/pkg/basecamp/eventfeed/loop.go
Comment thread go/pkg/basecamp/eventfeed/feedtest/clock.go
Comment thread go/pkg/basecamp/eventfeed/filestore.go Outdated
Comment thread go/pkg/basecamp/eventfeed/connector.go Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ceb8398f4c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread go/pkg/basecamp/eventfeed/catchup.go Outdated
Comment thread go/pkg/basecamp/eventfeed/catchup.go
Comment thread go/pkg/basecamp/eventfeed/cable.go Outdated
Comment thread go/pkg/basecamp/eventfeed/catchup.go Outdated
Comment thread go/pkg/basecamp/eventfeed/loop.go
Comment thread go/pkg/basecamp/eventfeed/connector.go
Comment thread go/pkg/basecamp/eventfeed/connector.go Outdated
Comment thread go/pkg/basecamp/eventfeed/cable.go Outdated
Comment thread go/pkg/basecamp/eventfeed/recovery.go
Comment thread go/pkg/basecamp/eventfeed/checkpoint.go
Copilot AI review requested due to automatic review settings August 12, 2026 05:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 54 out of 55 changed files in this pull request and generated no new comments.

Suppressed comments (4)

go/pkg/basecamp/eventfeed/websocket_transport.go:73

  • The offered subprotocol is not verified after the handshake. coder/websocket v1.8.15 accepts a 101 response with an empty Sec-WebSocket-Protocol, so this can return a connection even though actioncable-v1-json was never negotiated, contrary to the CableTransport contract. Check conn.Subprotocol() and reject/close a missing selection; add a server case that intentionally selects none.
    go/pkg/basecamp/eventfeed/catchup.go:302
  • This parks an already-observed overflow until the poll returns. SPEC §23 requires semantic signals at the first consumer-context opportunity after the condition arises (with “before the next save” only as the outer bound), so a stalled poll can postpone the handler forever even though this goroutine has received the dropping frame. Dispatch the overflow immediately here; an Accept disposition can continue awaiting the poll, while Terminate should cancel the attempt/poll.
			} else if over {
				// The buffer, not the socket: the call is unaffected and is
				// awaited to completion, and the drop's disposition runs
				// before this page's position moves anything durable.
				l.deferred = &deferredFrame{item: item, overflow: true}
				if l.hooks.frameDeferred != nil {
					l.hooks.frameDeferred(true)
				}
				r := <-done
				return r.page, false, r.err

go/pkg/basecamp/eventfeed/websocket_transport.go:73

  • checkCableURL explicitly accepts case-insensitive schemes (the new unit test includes WSS://), but this passes that original spelling to coder/websocket. In v1.8.15 its handshake switch recognizes only lowercase ws/wss, so a URL accepted by policy fails as a transient dial and is retried indefinitely. Normalize only the scheme before dialing, while leaving the ticket-bearing remainder unchanged.

This issue also appears on line 70 of the same file.
go/pkg/basecamp/eventfeed/websocket_transport.go:183

  • This synchronous graceful close can block teardown for several seconds: coder/websocket v1.8.15 waits up to 5 seconds to write the close frame and another 5 seconds for the peer response. Because dispose calls this before cancelling the attempt, caller cancellation, Connector.Close, terminal outcomes, and reconnects can all stall on a live peer that ignores the handshake. Use a bounded teardown strategy that preserves the required close frame without letting the library's full handshake timeout delay the universal Closed edge.

Copilot AI review requested due to automatic review settings August 12, 2026 05:29

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 54 out of 55 changed files in this pull request and generated 3 comments.

Suppressed comments (1)

go/pkg/basecamp/eventfeed/catchup.go:504

  • A live event admitted by drainScan is stranded when the buffer was empty at the start of this iteration: batch stays empty, so this returns even though the scan just repopulated l.buffer. Streaming never drains that buffer, delaying the event until a later repair walk and allowing caught_up (and a held save) to happen first. Continue the loop when the scan added events.
		batch := l.buffer.take()
		if out, done := l.drainScan(at, &budget); done {
			return out, true
		}
		if len(batch) == 0 {
			return cycleOutcome{}, false

Comment thread go/pkg/basecamp/eventfeed/catchup.go Outdated
Comment thread go/pkg/basecamp/eventfeed/filestore.go
Comment thread go/pkg/basecamp/eventfeed/catchup.go

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6eb58e6010

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread go/pkg/basecamp/eventfeed/catchup.go Outdated
Comment thread go/pkg/basecamp/eventfeed/catchup.go Outdated
Comment thread go/pkg/basecamp/eventfeed/cable.go Outdated
Comment thread go/pkg/basecamp/eventfeed/websocket_transport.go Outdated
Comment thread go/pkg/basecamp/eventfeed/loop.go Outdated
Comment thread go/pkg/basecamp/eventfeed/transport.go Outdated
Comment thread go/pkg/basecamp/eventfeed/catchup.go Outdated
@jeremy

jeremy commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

Round-2 note on the suppressed comments, since three of them are load-bearing and one changed the design.

catchup.go:302 (overflow parked until the poll returns) — taken, and it reframed the visible finding above it. Rather than add a third dispatch site I deleted the overflow deferral outright; admitDuringPoll now dispatches at the drop's instant. Details in the thread. Fixture 22's own description already called drop-time dispatch normative and its post-poll rendezvous "scheduling slack only", so the implementation now matches what the merged fixture says it should do.

catchup.go:504 (live event stranded by drainScan) — real, and a defect introduced by my own round-1 change: drain returned on len(batch) == 0 even when the scan had just repopulated the buffer, so the event waited for a later repair walk while caught_up and the held save went first. Now gated on l.buffer.empty() too; red proof showed [event 41 save pos-1] where [event 41 event 42 save pos-1] was required.

websocket_transport.go:73 ×2 and :183 — all three taken and in progress: the negotiated subprotocol is never verified after the handshake, a WSS://-spelled URL passes policy but the library's dial switch is lowercase-only (so it fails transient and retries forever), and the graceful close can block teardown on a peer that ignores the handshake. That last one has to keep the close frame that nine fixtures pin — an earlier round caught the opposite bug — so it is getting a bound rather than a reordering.

Thanks for putting the sharp ones in the suppressed block; they've been the most useful part of both rounds.

Copilot AI review requested due to automatic review settings August 12, 2026 06:11
@jeremy

jeremy commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

Closing out the remaining suppressed comments from round 2 (02fec5f). Two fixed, one declined with evidence.

Unverified subprotocol — real, fixed. Confirmed in the library rather than assumed: coder/websocket@v1.8.15/dial.go:270-283, verifySubprotocol returns nil when Sec-WebSocket-Protocol is absent, and the server side (accept.go, selectSubprotocol) sets no header at all when it selects nothing — so a 101 that negotiated nothing yielded a live connection. A mismatched selection the library does reject, leaving the empty case as the only reachable one, exactly as you said. Now conn.Subprotocol() must match actioncable-v1-json after the handshake; on mismatch the socket is torn down and the dial fails DialPolicy. Policy rather than transient is a deliberate call: a fresh mint returns a URL pointing at the same server, which will select the same nothing, so retrying forever against a server that cannot speak the protocol is the wrong shape — the redirect refusal already lands there for the same structural reason. Test includes the server-selects-none case you asked for.

Unbounded graceful close — real, fixed. close.go:99-128,157-228: Close writes the close frame under a hardcoded 5s context, then waits another 5s for the peer, then waitGoroutines. The phases can't be bounded separately (closeHandshake is unexported, no exported close-frame writer), and CloseNow is not an escape hatch once Close is in flight — casClosing has already flipped, so it just waits too. Bound is 1s, run off-caller: only the write is contractual (§23 needs the peer to see the frame, which is why dispose closes before cancelling — this is a bound, not the reordering an earlier round rejected), and that write is a control frame to an open socket bounded by the kernel send buffer, not by the peer. All 12 fixtures carrying expectClientClose still pass. Red proof: Close blocked past 3s against a peer that never answers; green returns at exactly the 1s budget, so it passes via the timeout path rather than a lucky response.

Case-insensitive scheme — not a defect, declined. net/url.Parse lowercases the scheme before anything downstream sees it ($(go env GOROOT)/src/net/url/url.go:454), and coder/websocket's dial switch runs on u.Scheme from its own url.Parse, so a WSS:// spelling arrives there already wss. Verified empirically, not just by reading: the new test dials a WS://-spelled loopback URL and passed against un-fixed code, with the ticket-bearing remainder byte-identical. No normalization added; the test stays as a regression pin binding checkCableURL's deliberate case-insensitivity to what actually dials.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 54 out of 55 changed files in this pull request and generated no new comments.

Suppressed comments (3)

go/pkg/basecamp/eventfeed/catchup.go:158

  • A socket outcome deferred while this poll was in flight is skipped when the poll itself fails. For terminal poll branches, recoverPoll calls disposeAttempt, which clears l.deferred; for retryable failures, the deferred frame can remain undispatched through arbitrarily many retries. In particular, an invalid_event_stream_command observed during CatchingUp can be replaced by poll_failed/authorization_failed or delayed indefinitely, despite SPEC §23 requiring protocol-fatal to terminate from every socket-open state. Dispatch the already-observed socket outcome here before applying poll recovery; a failed poll has no successful page boundary left to finish.
		if p.err != nil {
			step, out, done := l.recoverPoll(at, cursor, p.err)

go/pkg/basecamp/eventfeed/transport.go:40

  • Checking u.Host does not ensure that the URL has a hostname. For example, wss://:443/cable has Host == ":443" but an empty Hostname(), so it passes the policy check and is classified as a transient dial failure instead of terminal invalid_cable_url, causing repeated re-mints/dials for a structurally unusable URL.
    go/pkg/basecamp/eventfeed/cable.go:250
  • The tier-2 push-event schema requires all nine keys, including presence-bearing visible_to_clients, and SPEC §23 treats a correlated message missing a required event key as an invalid frame. Omitting this field from the presence checks accepts both an absent value and JSON null, exposing a nil value on a push event instead of taking the socket-failure recovery edge.
		{"creator_id", p.CreatorID != nil},
		{"recording_id", p.RecordingID != nil},

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 02fec5ff1f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread go/pkg/basecamp/eventfeed/connector.go Outdated
Comment thread go/pkg/basecamp/eventfeed/catchup.go Outdated
Comment thread go/pkg/basecamp/eventfeed/cable.go Outdated
Comment thread go/pkg/basecamp/eventfeed/catchup.go Outdated
Copilot AI review requested due to automatic review settings August 12, 2026 06:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 54 out of 55 changed files in this pull request and generated no new comments.

Suppressed comments (1)

go/pkg/basecamp/eventfeed/catchup.go:160

  • A deferred socket outcome is skipped when the in-flight poll returns an error. recoverPoll may retry, increment authorization failures, or terminate, and disposal then clears l.deferred; this can even swallow an already-observed invalid_event_stream_command instead of producing protocol_fatal. Since no page succeeded, dispatch the deferred outcome before classifying the poll error (the finish-page ordering only applies to successful pages).
		if p.err != nil {
			step, out, done := l.recoverPoll(at, cursor, p.err)
			if done {
				return out, "", true

@jeremy

jeremy commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

Holding two findings open deliberately: the in-flight-poll mechanism has drawn four rounds

Two round-4 findings — the suspendable bound in awaitSupersededPoll (thread-adjacent) and the deferred socket outcome swallowed on the poll-error path (suppressed, catchup.go:160) — are not being fixed in this round. Both are correct. I am not writing the next patch on that mechanism until its shape is settled, because the pattern is now the finding.

The ledger on one mechanism, in order:

  1. Round 1 (Codex P1): a stalled PollSource holds the consumer's goroutine when the socket dies → added awaitSupersededPoll, bounding the wait by the staleness window.
  2. Round 2 (Copilot + Codex, P1): a deferred overflow dispatches after the save, and is dropped entirely on failed-poll paths → removed the overflow deferral; admitDuringPoll dispatches at the drop's instant.
  3. Round 3 (Codex): the fatal-frame scan's budget was sized by the live-buffer capacity, so a fatal could hide behind one ping → rebounded on the pump queue's own depth.
  4. Round 4 (Codex, this round): the staleness bound from step 1 is suspendable by the very pump backpressure that creates the problem, so a misbehaving peer can keep a compliant stalled poll alive indefinitely. Plus (suppressed): the socket deferral is cleared by disposal when the poll returns an error, which can swallow an already-observed invalid_event_stream_command instead of producing protocol_fatal.

Each fix was principled and each was verified, but four rounds of edge-findings on one structure is evidence about the structure. What they all orbit: the state machine both awaits a poll seam call and services the frame queue during it, parking one out-of-band frame in a single l.deferred slot, with a bound borrowed from a timer whose evaluation the queue itself can suspend.

The concurrency is not optional — fixtures 01 and 19 require a live frame admitted after confirm but before the entry page is served, and transition 21 requires the in-flight page to be finished before the dying socket is observed. So this is not a mechanism that can simply be deleted, which is precisely why it wants a decision rather than a fifth patch. The candidate shapes:

  • (a) Patch in place — give the superseded-poll wait its own deadline from the injected clock (unsuspendable), and dispatch a deferred socket outcome before recoverPoll classifies a poll error, since the finish-page ordering only applies to pages that succeeded. Small, local, and both findings close.
  • (b) One event loop — stop blocking on the poll at all: make the poll result just another case in the same select that reads frames, so there is no deferral slot, no superseded wait, and no borrowed bound. Larger, restructures the core of catchup.go, and dissolves this whole class.

I lean (a) plus a note, because the deferral that remains is the one SPEC explicitly sanctions (transition 21's deferred consumption) and (b) risks the delivery-ordering guarantees that nine fixtures and the save-ordering invariant pin. But it is a judgment call about this PR's core, so it goes to a human rather than to me.

The other three round-4 findings (synchronous cancellation on Close, visible_to_clients required on push frames, and the drain batch escaping the live-buffer ceiling) are independent of this and are being fixed now.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a9103dd83a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread go/pkg/basecamp/eventfeed/websocket_transport.go
Comment thread go/pkg/basecamp/eventfeed/transport.go
Comment thread go/pkg/basecamp/eventfeed/loop.go Outdated
Comment thread go/pkg/basecamp/eventfeed/loop.go Outdated
Comment thread go/pkg/basecamp/eventfeed/catchup.go
Comment thread go/pkg/basecamp/eventfeed/loop.go
Copilot AI review requested due to automatic review settings August 12, 2026 07:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 54 out of 55 changed files in this pull request and generated 1 comment.

Suppressed comments (3)

go/pkg/basecamp/eventfeed/loop.go:152

  • ctx is the attempt context, which is a child of runCtx. Connector.Close (or caller cancellation) therefore cancels the pump immediately, before the state machine can reach dispose and call conn.Close. The default WebSocket read may abort the socket on that cancellation, so normal shutdown can still produce the abrupt disconnect that the close-before-cancel ordering is meant to prevent. Give the pump a cancellation scope that is canceled only by disposal after the socket close; HTTP seam calls can continue using the attempt context.
    go/README.md:615
  • This says the connector performs no wire I/O, but the package directly performs the sanctioned WebSocket cable dial. That contradicts both the implementation and the architecture rule. Limit the claim to HTTP requests and explicitly name the cable dial exception.
**Experimental: the Layer-1 seam adapters have not landed yet.** The connector performs
no wire I/O of its own — every HTTP exchange reaches the wire through a seam backed by
a generated operation — and the adapters that build those seams over the generated
`CreateStreamTicket` and `PollEvents` operations are still to come. Until they do, a
consumer must supply the `TicketMinter` and `PollSource` implementations itself, and the
exported surface may still change as they land.

go/pkg/basecamp/eventfeed/loop.go:638

  • Cancellation is not re-checked after Load. A custom store that honors the supplied context can return context.Canceled when the caller cancels or calls Close, and this path then yields checkpoint_load even though cancellation is documented to end iteration cleanly. Match the mint/poll paths by giving runCtx cancellation precedence over the load error.

Comment thread go/pkg/basecamp/eventfeed/catchup.go Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 93a3d5a457

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread go/pkg/basecamp/eventfeed/catchup.go Outdated
Comment thread go/pkg/basecamp/eventfeed/cable.go Outdated
Comment thread go/pkg/basecamp/eventfeed/loop.go
Comment thread go/pkg/basecamp/eventfeed/loop.go
jeremy added 2 commits August 22, 2026 01:58
…as the bug

Five of this round's eight findings taken; the notable decline is recorded
at the end.

The continuation validator had the defect validateConfig was cured of two
commits ago, on its other consumer: checkContinuation canonicalized the
server-supplied URL first, and CanonicalOrigin's ToLower rewrites every
invalid byte to U+FFFD — so a continuation host carrying a raw 0xff
collapsed to the same canonical form as a configured origin that
legitimately contains the replacement character (valid UTF-8, so
construction accepts it), and §8 same-origin validation equated two
distinct byte strings on the authenticated path. The raw bytes are refused
before the lossy step; red proof: the collapsed URL was accepted against a
U+FFFD base.

Two waits treated a staleness firing as a verdict where §23 says it is a
wake. In awaitSupersededPoll, the stale=false arm returned superseded on an
authoritative expiry — but a read error does not reset staleness, so the
PRE-EXISTING window, mostly spent when the failure was deferred, abandoned
the in-flight poll almost immediately: a failure landing 7s into a 7.5s
window granted the poll 0.5s of the grace phase §23 measures from the
deferral ("wakes may be early or late, and the deadline is what decides").
The arm now latches through graceWake exactly as the stale caller does, and
the deadline alone ends the wait — red proof: teardown on the old firing,
green: a wake, then teardown only when virtual time reaches
deferral + EVENT_FEED_STALE_AFTER. In waitPollRetry, a select with both
poll-retry and staleness ready could take the retry and start another Poll
on a socket whose expired window was already evidence — and a blocking call
then defers the stale verdict and buys a fresh grace phase, overrunning
§23's published bound ("detection window + grace phase... nothing waits
longer than their sum"). The retry arm now re-checks staleness before
polling; the red proof parks the run goroutine in an overflow handler while
both timers fire, and 17/40 rounds issued the second poll.

Transition 6 was announced late: newLiveConn opens the socket, arms
staleness and starts the pump — the transition's own definition — but the
state stayed Connecting until awaitConfirmation, so Observer.Connected ran
with AwaitingWelcome's timer set {handshake-deadline, staleness} against an
announced state whose set is {handshake-deadline} alone. The announcement
now precedes the callback; red proof: Connected observed "connecting".

Wait could report quiescence for a run that had started: the single-shot
claim was an atomic CompareAndSwap taken BEFORE the mutex section that
publishes runDone, and the claim is observable in that window — a second
consumption yields the usage terminal — while Wait still read nil and
returned. The claim now lives in the same critical section as the
publication, so Wait runs wholly before it (no run to await) or wholly
after runDone exists. No red proof discriminates this one honestly: the
window was two adjacent statements with no seam between them, and a hook
would exist for nothing else — the same account the terminal-claim fix gave
in an earlier round. The existing single-shot and Wait suites hold the
contract on both sides of the change.

AGENTS.md's architecture row still called this package foundations-only
with the run loop pending; this branch is where that stops being true, so
the row now names foundations, the run loop, and the tier-2 driver, with
the Layer-1 adapters and the other SDKs still pending.

Declined: terminating on a post-confirmation reject_subscription. The state
machine draws transition 12 solely from AwaitingConfirmation; §23's "always
terminal — first attempt or reconnect" pins which ATTEMPT a rejection lands
on, not a state-independent verdict. Action Cable rejects only from the
subscribe callback and the connector subscribes exactly once per socket, so
a post-confirmation rejection is an unsolicited frame — and honoring it
would hand one such frame the connector's most severe verdict, ZERO
reconnects, which is precisely what the pre-subscribe gate was added to
prevent in an earlier round. The default arm's liveness-only comment says
this on purpose.
…nt-feed-go-connector

* origin/event-feed-foundations:
  Make the fake transport honour the oversize sentinel it mirrors
  Police the endpoints a discovered issuer names, not only the issuer (#810)
  Refuse redirects on the signed download hop in every SDK (#809)
  Quiet known-noise CodeQL alerts without losing coverage (#807)
  Judge the advertised OAuth issuer's address, not just its spelling (#804)
  Let Go's raw GET retry loop see the Retry-After it already parses (#796)
  Deflake three tests that raced a wall clock, and gate the class that produced two of them (#794)
  SPEC §6: decide which statuses honour Retry-After, and how each loop composes it (#793)
  Pin the conformance runners' fixture reads, and give CI a leg that can see them break (#791)
  Report an anonymous embed the timestamp walk cannot resolve, instead of skipping it (#790)
@jeremy

jeremy commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

@codex review

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 24 out of 25 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

go/pkg/basecamp/eventfeed/scenario_conformance_test.go:730

  • This boundary is captured too late. For example, expectPoll advances the pointer and then releases its response; the connector can reach and leave the scripted buffer occupancy before expectPoll returns, after which from excludes that transition and this rendezvous times out despite compliant behavior. Capture the occupancy-history index atomically when the pointer advances onto the expectBuffered step, not when its executor is eventually scheduled.
	from := len(d.h.occupancyHistory)

Comment thread go/pkg/basecamp/eventfeed/scenario_conformance_test.go Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a466f3896f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread go/pkg/basecamp/eventfeed/loop.go
Comment thread go/pkg/basecamp/eventfeed/loop.go Outdated
Comment thread go/pkg/basecamp/eventfeed/catchup.go
Comment thread go/pkg/basecamp/eventfeed/catchup.go
jeremy added 2 commits August 22, 2026 02:38
All five of this round's findings taken — four expired-verdict races in the
run loop's family, and a conformance-driver fidelity hole.

The dial result could outrun the handshake deadline: both ready, the select
random, and accepting the dial installed the pump, armed staleness, and
announced Connected for an attempt whose window had already closed —
transition 7 bypassed by a coin flip. The success path now drains the
deadline before transition 6, as a drain rather than a Stop-probe because
an unfired deadline must keep running to welcome. The red proof fires the
deadline from inside Dial, sequencing it strictly before every dial result,
so any Connected at all is a violation: 1/100 rounds pre-fix, 0 after.

The subscribe write's bounded wait selected only {written, close, phase
deadline}, on the recorded premise that the phase deadline is always the
tighter bound. It is not, twice: staleness (7.5s) undercuts even the
default 10s handshake deadline on an immediate first welcome, and a
duplicate welcome resends under the confirmation deadline, which is
configurable to anything — a dead socket sat blocked in the write
arbitrarily past the expiry rows 9/15 say tears it down. The wait now
carries the staleness case like every other socket-open wait; red proof:
a stalled write plus a fired window produced no StaleConnection within the
watchdog.

Streaming could deliver past a latched expiry: a frame received AFTER the
window fired does not reset it — staleHolder.arm latches, which is §23's
authoritative-firing rule working — but the fired timer and that frame were
then both ready, and the frame arm delivered first. The arm now re-checks
the verdict before delivering, with evaluate as the arbiter: a frame the
pump received first moved the generation and delivers as ever, a latched
expiry takes transition 25 with the frame discarded to the reconnect walk.
Red proof, rounds-driven off a collector parked mid-delivery: 17/40 rounds
delivered event 102 past the latched expiry.

A throttled Retry-After of zero was read as absence. SPEC §6 fixes the
adapter mapping — a retryable outcome whose last response carried a parsed
Retry-After maps to throttled(retry_after) whatever its status, one without
maps to transient — so PollThrottled always carries a server-directed wait,
zero included, waited exactly and cap-exempt. pollRetryDelay is now
kind-keyed; a parsed zero stays zero instead of drawing up to 60s of local
jitter (the red run drew 315ms), and a negative value clamps rather than
arming a negative timer.

The tier-2 driver's arrival-strict lookahead judged expectBuffered against
GLOBAL occupancy history, so occupancy reached and left in a long-departed
era satisfied a pending expectBuffered and let the scan read through to a
following expectCheckpoint — an out-of-order save accepted by the very
instrument that exists to reject it. History is now scoped to an era
boundary captured whenever the step pointer moves (enterStep and the
await advance), and expectBuffered's own rendezvous scans from the same
boundary, which also closes the reached-and-left-before-runStep flake its
run-time capture had. Red proof: the scan judged an arriving save against
step 3 (expectCheckpoint) on stale history. Every committed fixture still
passes under the scoped scan.
…nt-feed-go-connector

* origin/event-feed-foundations:
  Fix the filters-clone comment that stated the opposite of the code
  Event feed: a policy reason never echoes what the server wrote
@jeremy

jeremy commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

@codex review

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 27 changed files in this pull request and generated 4 comments.

Suppressed comments (1)

go/pkg/basecamp/eventfeed/scenario_conformance_test.go:1077

  • The poll lane has the same status-keyed mapping error: a 429/503 without a parsed header is emitted as PollThrottled with zero. With the new pollRetryDelay branch, that becomes an immediate retry loop instead of transient jitter. SPEC.md:682-687 requires PollThrottled only when a usable parsed Retry-After is present, regardless of status.
		kind := eventfeed.PollTransient
		if status == 429 || status == 503 {
			kind = eventfeed.PollThrottled
		}

Comment thread go/pkg/basecamp/eventfeed/backoff.go Outdated
Comment thread go/pkg/basecamp/eventfeed/scenario_conformance_test.go Outdated
Comment thread go/pkg/basecamp/eventfeed/scenario_conformance_test.go Outdated
Comment thread go/pkg/basecamp/eventfeed/catchup.go Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 86ec2cd547

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread go/pkg/basecamp/eventfeed/loop.go
Comment thread go/pkg/basecamp/eventfeed/loop.go
Comment thread go/pkg/basecamp/eventfeed/loop.go
jeremy added 2 commits August 22, 2026 03:00
…o ask it

Four follow-ups taken, one of them a correction to this branch's own
previous round — stated plainly: the round-4 throttled-zero change overshot,
and Copilot caught it.

The two §6 texts compose; they do not conflict. The seam mapping is
presence-keyed — throttled iff the last response carried a PARSED
Retry-After — but "parsed" is defined by §6's parsing algorithm, and that
algorithm cannot yield zero: step 1 requires an integer > 0, step 2 returns
max(0, date − now) only when > 0, and the rounding rationale says why —
"zero is read as 'no usable value' and drops the request onto the local
backoff curve". So a conformant adapter's throttled ALWAYS carries a
positive value, the original value gate honored every one of them exactly,
and keying the delay on the kind bought nothing while turning a
nonconforming adapter's throttled-with-zero into a zero-delay poll-retry —
a tight loop against a server that is throttling the caller. The red run
shows it literally: "poll-retry armed for 0s". pollRetryDelay is reverted
to the value gate with the reconciliation written above it; the round-4
test asserting the wrong contract is replaced by one asserting the jitter
fallthrough. What WAS wrong is the seam doc, which still described both
throttled kinds by status ("429/503") — the exact status-keyed mapping the
§6 sentence exists to forbid — and both docs now state the presence-keyed
definition with the always-positive consequence.

The tier-2 driver had the same status-keying in the flesh, on both lanes: a
schema-valid 500/502/504 carrying Retry-After became transient and its
delay never reached classifyMintFailure's floor, while a bare 429 became a
throttled zero. Both outcome mappers now classify retryable statuses by
presence of the parsed value. And the driver's Retry-After parse was
integer-only — a valid RFC 7231 HTTP-date failed the scenario outright
(red: strconv.Atoi rejecting the date), zero and negatives became
durations. retryAfterFrom now implements §6 against the harness's virtual
clock — integer > 0; else HTTP-date with the sub-second remainder rounded
UP; else undefined — returning presence separately from the value.

The round-3 grace-phase fix also overshot, in the other direction: an
authoritative firing late in the phase re-armed graceWake for a FULL
window, so a firing 1ms short of the deadline pushed the only remaining
wake almost a whole window past it — the wait ended nearly two windows
after the deferral instead of one. graceWake now takes its wake distance
and every caller passes the remainder to its deadline (the full window at
the deferral itself, where they coincide). The new red proof pins the upper
bound the round-3 test could not see: advance to 1ms short, fire, advance
the last 1ms, and demand the teardown there — red sat parked until the
watchdog; green ends at the deadline exactly.
…nt-feed-go-connector

* origin/event-feed-foundations:
  Event feed: the redactor renders configured text or a placeholder, never the server's
@jeremy

jeremy commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 67e908aa19

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread go/pkg/basecamp/eventfeed/catchup.go

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 27 changed files in this pull request and generated 3 comments.

Comment thread go/README.md Outdated
Comment thread go/README.md Outdated
Comment thread go/README.md
… lost to an announcement

All seven of this round's findings taken — four in the loop's established
families, three in the README, each verified against the code first.

A superseded poll is now cancelled AT the grace deadline, in time and not
merely in control flow. Disposal closes the socket gracefully before it
cancels — deliberately, so the peer sees a close frame — but a peer that
never acknowledges holds that close for the transport's full grace budget,
and the abandoned seam call (carrying the caller's bearer) stayed live
through it, stalling reconnection past §23's detection-plus-grace bound.
The deadline branch cancels the attempt context itself; the disposal that
follows finds its own cancel a no-op. The red proof captures the pump's
read context and parks the teardown's close: pre-fix the context was still
live at the park.

AwaitingConfirmation's frame arm gains the latched-expiry drain the
Streaming arm got last round — the same shape for a worse consequence: a
frame received after the window fired could win the select and either admit
a correlated message to the loop-wide buffer, which survives the teardown
and resurfaces after reconnect, or hand a confirmation a live catch-up on a
socket whose verdict was already in. Rounds red: 11/40 announced Confirmed
past a latched expiry.

Two Close-outranks-the-announcement gaps, both on the far side of observer
callbacks that are documented Close sites. outcomeFailed announced Backoff
and asked the product clock for a reconnect timer with runCtx already
cancelled by a Close inside Disconnected or StaleConnection; finishDrain
continued past CaughtUp into dispatchDeferred and stream, which re-arms the
repair cadence through the same product seam and announces Streaming. Both
now take the Closed edge first — a host clock that blocks in NewTimer would
otherwise keep the iteration and Wait from ever reaching Closed. Red
proofs: the state ledger carried "backoff" (and "streaming") after Close
returned; green goes straight to closed.

Three README corrections, each checked against the code. The terminal-vs-
continuable section implied every continuable outcome waits a timer;
rejected positions do not — reenterWalk re-polls a replacement cursor
immediately on the same socket, and the text now says which failure
continues how. The unwrap claim promised a generated error behind every
poll_failed; the connector's own verdicts carry none (a 200 whose page has
no position), so the claim is now conditional. And the consumer-surface
section stated "Close stops the feed" without the lifecycle this branch
actually ships: Close only cancels and can return with a save still in
flight, Wait is the quiescence point, and a replacement connector opened
straight after Close races the prior run's last save — the README now says
so where the store guidance lives.
@jeremy

jeremy commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

@codex review

…nt-feed-go-connector

* origin/event-feed-foundations:
  Fix three comments still stating rules the code has since outgrown

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 27 changed files in this pull request and generated no new comments.

Suppressed comments (2)

go/pkg/basecamp/eventfeed/catchup.go:373

  • Dispatch an already-parked outcome before scanning the socket again. When the in-flight poll returns a successful page with Next, pollPage may already have stored a protocol-fatal disconnect in l.deferred; socketCheck can then consume the pump's later read error/closed channel and end the cycle as a retryable failure before the next iteration reaches probeFatal. That turns the required Terminal(protocol_fatal) into Backoff and loses the first outcome's ordering.
		if out, done := l.socketCheck(at); done {
			return out, "", true
		}
		cursor = Cursor{PageURL: page.Next}

go/pkg/basecamp/eventfeed/catchup.go:530

  • The fixed grace deadline is not actually enforced for ordinary deferred frames/read errors. graceWake is armed only for a deferred staleness verdict, so a frame reset just before deadline can move the next staleness firing almost a full window later; awaitSupersededPoll checks the deadline only after a wake and therefore remains blocked past it. This contradicts the new SPEC.md:3235-3243 requirement that the grace phase be measured from deferral and be immune to frame resets. Please provide a deadline wake for every deferral (without letting it be mistaken for a new staleness verdict) and cover a reset immediately before the deadline.
		if stale {
			// The firing this branch consumed was the wait's own wake source,
			// and an authoritative expiry latches — so without this there is
			// nothing left to wake the grace phase at all, and the bounded wait
			// becomes unbounded again. Armed BEFORE the hook, so a test that
			// rendezvouses on the deferral cannot advance past a wake that is
			// not yet armed. The distance is the remainder to the deadline —
			// here, at the deferral itself, the full window.
			at.lc.stale.graceWake(l.cfg.staleAfter)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 86dd27a4af

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread go/pkg/basecamp/eventfeed/catchup.go
Comment thread go/pkg/basecamp/eventfeed/connector.go
Comment thread go/pkg/basecamp/eventfeed/scenario_conformance_test.go
jeremy added 3 commits August 22, 2026 12:44
…nt-feed-go-connector

* origin/event-feed-foundations:
  Event feed: save through a symlinked store path instead of replacing it
  Event feed: an invalid frame limit fails closed, not open into no limit

# Conflicts:
#	go/pkg/basecamp/eventfeed/seams.go
resolveStorePath's Lstat-error branch intentionally returns the unresolved
path with no error — the comment above it says why: the open or rename that
follows reports the same failure with better context, where a resolve-time
error would name a step the caller never asked about. nilerr cannot read
the comment; the annotation repeats it. Foundations carries the same line
un-annotated and will want the same fix.
…nt-feed-go-connector

* origin/event-feed-foundations:
  Annotate resolveStorePath's deliberate nilerr fall-through
@jeremy

jeremy commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 93c714ca1f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread go/pkg/basecamp/eventfeed/catchup.go

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 27 changed files in this pull request and generated 2 comments.

Suppressed comments (1)

go/pkg/basecamp/eventfeed/scenario_conformance_test.go:1031

  • The driver attaches the parsed header before classifying the status, so scripted 401/403 (and unrecoverable 404/422) mint errors carry RetryAfter. This contradicts the §23 mapping that only retryable outcomes carry that value and makes tier 2 encode the same unauthorized-backoff bug as the connector. Assign RetryAfter only in the retryable default branch.
	retryAfter, present := d.retryAfterFrom(respond.Headers)
	mintErr := &eventfeed.MintError{RetryAfter: retryAfter, Err: fmt.Errorf("mint responded %d", *respond.Status)}
	switch *respond.Status {
	case 401, 403:
		mintErr.Kind = eventfeed.MintUnauthorized

Comment thread go/pkg/basecamp/eventfeed/loop.go Outdated
Comment thread go/pkg/basecamp/eventfeed/catchup.go Outdated
…dy race

All six of this round's findings taken.

The three dispatch frame arms — AwaitingConfirmation, Streaming, and the
poll-retry wait — now let the Closed edge win the both-ready race Close
itself creates: cancellation makes the conforming ReadFrame return its
cancellation error, the pump hands it off, and the frame case dispatching
it reported the consumer's own Close to Observer.Disconnected as a socket
failure, after Close had returned. Rounds red: 17/40 fired
Disconnected(context canceled) post-Close.

A panic in host code — an observer, the signal handler, a checkpoint
store, the consumer's range body, all of which run on the run goroutine —
unwound with only the run context cancelled: no deferred path closed the
CableConn, and a compliant conn is only required to unblock reads on
CLOSE, so under an outer recovery the socket stayed open with its pump
parked and a later Connector.Close could not reach it (cancelRun cleared
on unwind). runCycle now re-disposes the live attempt inside a recover and
re-panics; disposal is idempotent, so paths that already disposed are
unharmed, and phase timers held as locals are deliberately not chased —
unfired, they fire into nothing. Red: the socket stayed open and the
staleness timer stayed armed through a recovered panic.

The repair-poll arm gains the family's expired-verdict drain: entering the
walk made an already-rendered stale verdict look in-flight — pollPage
deferred it into a grace phase, and the poll's page could be accepted
first — where transition 25 fires directly from every socket-open state.
Rounds red: repair polls issued on an expired socket.

Both unauthorized paths forwarded a seam-supplied RetryAfter into the
reconnect floor. The SPEC's loop table names `unauthorized` "a kind
carrying no retry_after" and row 4 says the rest: "the backoff draw alone
governs". Both arms drop the field; red on each showed the 5-minute floor
verbatim.

The tier-2 driver's redirect leg now runs the SHIPPED per-hop predicate.
Fixture 30's description promised a sentinel listener the harness never
binds, and the driver classified the scripted 302 by an ad-hoc origin
comparison — synthesizing the refusal rather than exercising the decision,
and turning an unreducible hostile Location into a SCENARIO error instead
of a refusal. redirectRefusalFrom now classifies through checkContinuation
(exported to the driver as a test shim), so the decision tier 2 pins is
the one that ships. What tier 2 cannot see — an adapter's HTTP client
auto-following inside one seam call — is below the poll seam by
construction, and pretending otherwise was the finding: the fixture's
description now claims exactly what the tier verifies (the refusal
decision, the continuation terminal, the closed socket, no further seam
calls) and names Layer-1 adapter conformance as the owner of the zero-
egress property, where the pending adapter PR's sentinel can actually
observe a request. The description edit is the only fixture change, one
line, and the fixture gate stays green.
@jeremy

jeremy commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 16811f1200

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 28 changed files in this pull request and generated no new comments.

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

go/pkg/basecamp/eventfeed/catchup.go:530

  • The fixed grace deadline is only given an immutable wake when the deferred outcome is staleness. For a deferred disconnect/invalid frame, a later frame can reset the ordinary staleness timer near deadline; the rearmed case wakes before the deadline, then the next wake is delayed until frameTime + staleAfter, so the abandoned poll can outlive the one-window grace phase. This contradicts SPEC.md:3235-3243 and #758’s accepted “immune to frame resets” criterion. The existing TestGracePhaseIsImmuneToFrameResets uses staleDeferralHarness, so it cannot exercise this branch. Please add equivalent coverage for a frame-based deferral and provide a fixed deadline wake for that path without allowing socketCheck to replace the parked disconnect reason with staleness.
		if stale {
			// The firing this branch consumed was the wait's own wake source,
			// and an authoritative expiry latches — so without this there is
			// nothing left to wake the grace phase at all, and the bounded wait
			// becomes unbounded again. Armed BEFORE the hook, so a test that
			// rendezvouses on the deferral cannot advance past a wake that is
			// not yet armed. The distance is the remainder to the deadline —
			// here, at the deferral itself, the full window.
			at.lc.stale.graceWake(l.cfg.staleAfter)

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

Labels

conformance Conformance test suite go

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants