feat(firehose): self-test wedge defence + alive sentinel + decode-error robustness - #42
Merged
Merged
Conversation
…or robustness Day-8 housekeeping for the two firehose observability findings flagged in PR #40's bench artifact (and the Day-7 close memory): 1. **postgres-js LISTEN dispatch wedge on bad-JSON callback (Finding #1).** On Day-7 the listener received exactly ONE notification with a malformed (non-JSON) payload, the decode_error logged, and every subsequent NOTIFY (valid or not) was silently dropped despite the underlying TCP connection staying alive. Verified at the time via pg_stat_activity: connection idle, application_name still 'foxbook-firehose-listener', dispatch wedged at the postgres-js library layer. Production triggers always emit valid row_to_json JSON, so this isn't a runtime risk today — but a future migration or future event type that emits something the listener can't parse would silently kill the firehose with no automatic recovery. **Defence:** opt-in self-test heartbeat. If `selfTestIntervalMs` is set (default 60s; 0 disables), the listener periodically fires its own `pg_notify` carrying a `__listener_self_test__` event_type with a fresh nonce. The listener's own onnotify handler recognizes the event_type, filters it from the public EventEmitter (SSE clients never see it), and updates `lastSelfTestEchoNonce`. On the next heartbeat tick, if the prior ping's nonce never echoed within `selfTestIntervalMs * 1.5` tolerance, the listener throws → forces the reconnect-backoff path → resubscribes. Sentinel log: firehose_listener_self_test_failed observed_lag=Nms expected_within=Mms Tested with 3 new cases: - Self-test ping fires + echoes cleanly → no reconnect, public emitter never sees the self-test event. - Self-test ping fires but never echoes → tolerance window elapses → `firehose_listener_self_test_failed` + reconnect + resubscribe (factory invoked twice). - aliveLogEveryHeartbeats=0 disables the periodic alive sentinel (clean-log surface for low-noise environments). 2. **Decode-error robustness.** The decode-error path already caught `JSON.parse` failures + sentinel-logged. Adding the comment that bytes were received successfully (dispatch is healthy at the postgres-js layer) so future-readers understand the wedge isn't from our parse failure — it was from postgres-js's internal state. No behaviour change; documentation only. 3. **Periodic alive sentinel (Finding #2 prep).** New `firehose_listener_alive heartbeats=N events=M self_tests_ok=K` log line, fired every `aliveLogEveryHeartbeats` heartbeats (default 10 → ~5 min at default heartbeat). Gives operators a positive-confirmation signal that the listener is live + how many events have flowed without needing to grep for the absence of reconnect logs. Counters reset on each (re)subscribe so the value is per-cycle, not lifetime. NOT included today (filed for follow-up if NOTICE-level visibility becomes load-bearing): postgres-js `onnotice` callback wiring. The library exposes this as a construction-time option on `postgres()`; threading it through `createDirectPostgresClient` would require a small factory-options extension. Doable, but `onnotice` only fires for Postgres NOTICE-level messages (e.g. raised by RAISE NOTICE), not connection-state transitions — which is what Finding #2 was actually asking for. The right defence for "silent library-level reconnect" is the self-test mechanism above (it catches the wedge regardless of what postgres-js's library-level state is doing); the `onnotice` debug callback would be a small additional visibility layer rather than a load-bearing fix. Verification (all green): * pnpm check:core-isolation * pnpm -r typecheck * pnpm --filter @foxbook/api test → 70 passed (3 new self-test cases) Implements the postgres-js LISTEN wedge defence + alive sentinel from the Day-7 close memory's "Day-8 queue".
cloakmaster
marked this pull request as ready for review
May 3, 2026 06:39
This was referenced May 3, 2026
cloakmaster
added a commit
that referenced
this pull request
May 3, 2026
* docs: stable-mode Phase 2 — documentation lockdown Stable-mode wrap per ADR 0008 (this PR introduces it). Phase 2 of the plan at /Users/tester/.claude/plans/focus-deeply-and-use-nifty-swan.md. No code changes; no behavior changes; protocol surface unchanged. ## What's in this PR - ADR 0006 (RETROACTIVE, dated 2026-04-26) — protocol-not-marketplace path-ordering rule + co-option-not-forking failure mode. Backdated to original ratification per the 2026-04-26 external review. EXPLICITLY does NOT include the SR 11-7 / EU AI Act / cyber-insurance monetization wedge that was discussed during the same review but REJECTED four days later (2026-04-30); a doc dated 2026-04-26 must not retrofit later-rejected framings. - ADR 0008 (NEW, dated 2026-05-03) — stable mode and maintenance posture. Feature freeze, substrate stays live, brand protection persists, future direction = TBD pending external integration signal. - README.md Status section — replaces "in progress" stragglers (SDK npm publish + MCP server) with stable-mode framing pointing at ADR 0008. Adds harness aggregator entry callout. - TRADEMARK.md — removes "currently DNS-only; landing-page content lands week-2" outdated note (Phase 5 lands the landing page). - CONTRIBUTING.md (NEW) — what's welcome (bug fixes, docs, forks under different names) vs not happening (new features, spec extensions). Security: security@foxbook.dev. - NOTICE (NEW) — Apache 2.0 attribution + canonical impl pointer + cross-impl reference status. - docs/RATIONALE.md (NEW) — narrative summary of all 8 ADRs. Audience: stranger discovering Foxbook in 12 months. - docs/VERIFY-IN-60-SECONDS.md (NEW) — written walkthrough of the live demo, three curls + SDK reconstruction. ## Review gate ADR 0006 is the strategic doc in this PR; the protocol-not-marketplace thesis is historically scoped (what was ratified WHEN). Other docs are factual / structural and flow without per-doc review. Specific scope discipline on ADR 0006: - INCLUDED: protocol-now leaves marketplace-later open (path asymmetry); marketplace-now closes protocol-ever; co-option-not-forking failure mode; defense indicators are cross-impl references that name the canonical impl. - EXPLICITLY NOT INCLUDED: SR 11-7 / EU AI Act Article 12 / cyber- insurance monetization wedge (rejected 2026-04-30, four days after ratification). A future ADR can revisit monetization framing under its own number with its own date. ## Verification - pnpm check:generated — 10 files match - pnpm check:core-isolation — clean - pnpm -r typecheck — clean - No code touched, so no test runs needed ## Refs - Plan: /Users/tester/.claude/plans/focus-deeply-and-use-nifty-swan.md - ADR 0006 sources: external review 2026-04-26 (memory project_protocol_thesis_external_validation.md captures the full discussion; ADR 0006 captures only what was ratified) - ADR 0008 sources: stable-mode plan + Phase 1 close (PRs #42, #60, #61) * docs(adr-0006): minor edits per review (verified-date header + drop 'Wedge 1' tag) --------- Co-authored-by: Ben <ben@inkog.io>
6 tasks
cloakmaster
added a commit
that referenced
this pull request
May 4, 2026
…ng (#64) Phase 4 of the stable-mode plan. Master runbook for keeping transparency.foxbook.dev / api.foxbook.dev / foxbook.dev running unattended at low cost. No code changes; no behavior changes. ## What's in this PR - docs/OPERATIONS.md (NEW) — master runbook covering: - Future-you sanity check (5 commands to run when something breaks) - Monthly cost summary (target <$30/mo) - Re-deploy each service (Fly.io api, Cloudflare Worker, Pages landing) - Key rotation (transparency-log signing key + per-agent keys) - Incident response (log corruption, forged claim, leaked secret) - Database backups (Neon PITR by tier) - Uptime monitoring (this PR's workflow + silencing alerts) - Phase 5 stable-mode landing notes - Cross-references to ADRs + config files - .github/workflows/uptime.yml (NEW) — 15-min cron, three endpoints, opens / comments-on / closes GitHub issues tagged `uptime-incident`. All matrix values passed via env: rather than direct ${{ }} interpolation (defense-in-depth against future user-derived matrix entries; matrix is static config today). - apps/api/fly.toml — added cross-reference comment pointing at OPERATIONS.md for re-deploy / rollback / key-rotation procedures. Existing 72-line first-time-setup runbook preserved. - docs/operations/env-vars.md — rewritten to reflect production reality (Fly + Cloudflare Worker secrets, four actual variables). Removed Vercel / Upstash / KMS / MCP sections that were earlier scaffolding never shipped. Added pointer to OPERATIONS.md for the broader runbook. ## Verification - pnpm check:generated — 10 files match - pnpm check:core-isolation — clean - pnpm -r typecheck — clean - No code touched (docs + workflow only) ## What's NOT in this PR - Cloudflare Pages landing page itself (foxbook.dev) — Phase 5 - Final Discussion #1803 announcement — Phase 5, deferred until Phase 4.5 (eriknewton spec session + ADR 0009) lands first ## Refs - Plan: /Users/tester/.claude/plans/focus-deeply-and-use-nifty-swan.md - ADR 0008 — stable-mode posture (Phase 2) - Phase 1: shipped 2026-05-03 (#42, #60, #61) - Phase 2: docs lockdown (#62, #63) - Phase 3: brand protection (NOTICE + TRADEMARK in #62; identity-bound terminal steps confirmed by maintainer) - Phase 4: this PR - Phase 4.5: ADR 0009 after eriknewton spec session (gates Phase 5) - Phase 5: deferred until 4.5 lands Co-authored-by: Ben <ben@inkog.io>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why (Day-7 → Day-8 follow-up)
Two firehose observability findings surfaced during the Day-7 verification gate (PR #40's bench artifact + Day-7 close memory):
pg_terminate_backend, postgres-js handled the LISTEN reconnect transparently and our wrapper's catch block never fired — events resumed without anyfirehose_listener_reconnect attempt=Nsentinel reaching operator logs.This PR adds defence-in-depth for both, scoped narrowly per the Day-8 directive.
What
1. Self-test wedge defence (Finding #1)
If
selfTestIntervalMsis set (default 60s, 0 disables), the listener periodically fires its ownpg_notifycarrying a__listener_self_test__event_type + fresh nonce. The listener's own onnotify handler:lastSelfTestEchoNonce.On the next heartbeat tick, if the prior nonce never echoed within
selfTestIntervalMs * 1.5tolerance, the listener throws → forces the reconnect-backoff path → resubscribes. New sentinel:This catches the wedge regardless of what postgres-js's library-level state is doing — we don't need to monkey-patch the library, we just verify our own dispatch is healthy from the inside.
2. Periodic alive sentinel (Finding #2 prep)
Fired every
aliveLogEveryHeartbeats(default 10, 0 disables) → ~5 min at default heartbeat. Gives operators a positive-confirmation signal without grepping for the absence of reconnect logs. Counters are per-cycle (reset on resubscribe).3. Decode-error robustness (documentation only)
The existing JSON-parse catch + sentinel log already worked. Adding a comment explaining that received bytes prove dispatch is healthy at the postgres-js layer — the wedge wasn't from our parse failure but from postgres-js internal state. No behaviour change.
What's NOT in this PR
postgres-jsonnoticecallback wiring. The library exposesonnoticeas a construction-time option onpostgres(); threading it throughcreateDirectPostgresClientwould need a small factory-options extension. Doable, butonnoticeonly fires for Postgres NOTICE-level messages (e.g.RAISE NOTICE), not connection-state transitions — which is what Finding ADR 0001: service-agnostic core (accepted, verified) #2 was actually asking for. The self-test mechanism above is the load-bearing defence;onnoticewould be a small additional visibility layer. Filed as a follow-up if NOTICE-level visibility becomes operationally necessary.firehose_listener_aliveabsence. That's an operator-dashboard concern; the sentinel logs are the building block.Tests
3 new cases in
apps/api/__tests__/firehose.listener.test.ts(existing 67 tests still pass; 70 total in apps/api):firehose_listener_self_test_failedlogged; reconnect-backoff fires; factory invoked a second time (resubscribe). Validates the full wedge → reconnect → recovery path.aliveLogEveryHeartbeats=0disables alive sentinel — clean-log surface for low-noise environments.Verification
pnpm check:core-isolationpnpm check:generated(8 files match)pnpm -r typecheckpnpm --filter @foxbook/api test→ 70 passed + 4 skippedRefs
ops/bench-results/2026-04-27-firehose-latency.csvnotes both findings.apps/api/src/firehose/listener.ts.