Skip to content

docs(HT-127): state the event contract's no-ordering consequence - #203

Merged
zaridan merged 4 commits into
mainfrom
docs/ht-127-event-ordering-contract
Aug 10, 2026
Merged

docs(HT-127): state the event contract's no-ordering consequence#203
zaridan merged 4 commits into
mainfrom
docs/ht-127-event-ordering-contract

Conversation

@zaridan

@zaridan zaridan commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

🟢 SAFE TO MERGE

Gates green on c93b19b. Both decisions below are authorized, not inferred. Codex: 2 passes, 2 findings, 2 real and fixed.

One-way door: adds a consumer obligation to the substrate-v1 event contract.

Jira: HT-127.

What changed

substrate-v1.md §4 already granted "no cross-event ordering guarantee." It never said
what an integrator does about it. §4 now names the concrete case — two events emitted in
one transaction share an occurredAt exactly (now() is transaction start time), so
conversation.message_received can reach a consumer before the conversation.created
it belongs to — and states the requirement that follows: treat any event as possibly the
first seen for its conversationId, upsert rather than insert, read state from the API
rather than from arrival order. §8's guarantee table now carries "no ordering" on the
emission row too, so it cannot be read as "the outbox is ordered, delivery is not."

EventOutboxStore.claimBatch's drain order is now deterministic: (occurred_at, event_id) breaks ties that were previously left unordered by design, using event_id
(the table's primary key) rather than adding a column. This makes a single caller's
repeated claims against a fixed eligible set reproducible for debugging and replay — it
does not create a global order across concurrent drainers, and it does not change the
public delivery guarantee, which stays "no cross-event ordering."

Decision provenance

Decision — in plain words Source
Webhook consumers are told in writing that they must cope with events arriving out of order, including a message arriving before the conversation it belongs to You, 2026-08-08 — chose "State it as a MUST" from three offered options
The outbox does NOT get a sequence column; a free tiebreak on the existing primary key (event_id) is used instead You, 2026-08-08: "fold it in"

Verification

  • npm run typecheck / npm run lint — clean.
  • npx vitest run src/store/event-outbox.test.ts — 10/10, the only file this change touches. Confirmed the new tiebreak test discriminates: stashed the source fix, re-ran, watched it fail against the unfixed sort (returned insertion order instead of event_id order); restored and re-ran green.
  • CI Quality (typecheck, lint, test, coverage) (full suite, isolated) — green on c93b19b, the actual merge gate. A local full-suite run under heavy machine contention produced unusable noise (unrelated files failing en masse at 20–80s per simple test) and was discarded in favor of CI's isolated result.

Review, three passes across three heads — none blur into the others:

  • CodeRabbit reviewed 7abd333b (this branch's prior head, doc-comment-only changes) and does not cover this PR's runtime change (ORDER BY occurred_at, event_id and its test). Incremental review is off in this repo, so it never re-ran on a later head despite repeated @coderabbitai full review requests.
  • Codex (adversarial, substituting for CodeRabbit) reviewed fc9b8d3 — the event_id tiebreak and its test. Confirmed: event_id is genuinely the table's primary key (migration 023), so (occurred_at, event_id) is a real total order; the public no-ordering contract in §4 is preserved; the new test discriminates with high (not perfect) probability given random UUIDs, which the test's own comment already discloses honestly. Real finding: the interface doc's "reproducible... every time" wording read as claiming one global order across concurrent drainers, when SKIP LOCKED actually partitions eligible rows between them unpredictably — fixed in c93b19b.
  • Codex re-checked c93b19b alone (the doc-comment fix only): confirmed the corrected comment now accurately describes claimBatch under both single-caller and concurrent-drainer conditions, verified sentence-by-sentence against the implementation. No remaining inaccuracy.

No open findings block this PR.

Events emitted in one transaction share an occurredAt exactly (now() is
transaction start time), so conversation.message_received can reach a
consumer before the conversation.created it belongs to. substrate-v1 §4
already granted no cross-event ordering guarantee; it never said what an
integrator has to do about it.

§4 now names the concrete case and the requirement that follows: treat any
event as possibly the first seen for its conversationId, upsert rather than
insert, read state from the API rather than from arrival order. Sorting by
occurredAt does not recover an order — the ties are exact. §8's guarantee
table carries "no ordering" on the emission row too, so it cannot be read
as "the outbox is ordered, delivery is not".

No tiebreak column is added. event_id is a random v4 uuid and created_at is
the same now(), so a tiebreak would take a new sequence — and drainEventOutbox
fans each event to one queue_jobs row per endpoint, each retrying
independently, so claim order does not survive to the consumer regardless. A
deterministic drain would hold in development and break under production
retry, which is worse than no guarantee at all.

claimBatch's interface doc records the same reasoning, and a test pins the
premise the spec text rests on: two events appended in one transaction give
count(DISTINCT occurred_at) = 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
helpthread Ready Ready Preview Aug 10, 2026 7:12pm
helpthread-inbox Ready Ready Preview Aug 10, 2026 7:12pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR documents that event ordering is not guaranteed. It adds deterministic event_id tie-breaking to claimBatch and tests timestamp equality and repeatable ordering.

Changes

Event Ordering Contract

Layer / File(s) Summary
Ordering contract documentation
specs/modules/substrate-v1.md, src/store/event-outbox.ts
The specification documents out-of-order delivery and API-based state retrieval. Outbox documentation defines event_id as a reproducible, non-causal tie-breaker after occurred_at.
Timestamp tie behavior
src/store/event-outbox.ts, src/store/event-outbox.test.ts
claimBatch orders claims and returned results by occurred_at and event_id. Tests verify identical transaction timestamps and repeatable ascending event_id ordering.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: documenting the event contract's no-ordering consequence.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/ht-127-event-ordering-contract

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/store/event-outbox.ts`:
- Around line 153-155: Update claimBatch() to retain a full-precision raw
occurred_at ordering value from PostgreSQL instead of sorting on Date.getTime()
after toDate(). Sort using that raw value, and revise the interface contract and
nearby tie-ordering documentation to define ties according to the preserved
precision while keeping same-timestamp events unordered.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e6c6545a-e48b-4162-abf6-bef624200830

📥 Commits

Reviewing files that changed from the base of the PR and between a10ee48 and 03ab33d.

📒 Files selected for processing (3)
  • specs/modules/substrate-v1.md
  • src/store/event-outbox.test.ts
  • src/store/event-outbox.ts

Comment thread src/store/event-outbox.ts Outdated
The claimBatch re-sort compares Date.getTime(), which cannot hold the
column's microseconds, so events under a millisecond apart tie there too —
not only events sharing a transaction. The interface doc named one tie class
and implied it was the only one.

Documented rather than made exact. The sort is best-effort by construction:
spec §4 grants no cross-event ordering guarantee, and per-endpoint retries
reorder deliveries downstream regardless, so a microsecond-exact claim order
would be precision no consumer can observe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@zaridan

zaridan commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

claimBatch's drain order previously left occurred_at ties (same-
transaction events, or any two events under a millisecond apart)
unordered by design, since no order reaches the consumer regardless.
That left replay non-reproducible for debugging and testing.

(occurred_at, event_id) is a stable total order without adding a
column: event_id is the primary key, so it never ties. This is a
fixed-but-arbitrary tiebreak, not a meaningful one — event_id is a
random v4 UUID uncorrelated with write time. Both the claiming SQL
and the RETURNING re-sort now use it, matching the interface doc
(maintainer decision, 2026-08-08).

A sequence column was considered and declined (maintainer decision,
2026-08-08): the published contract already grants no cross-event
ordering (substrate-v1.md §4), and drain order cannot reach a
consumer regardless — SKIP LOCKED under concurrent drainers, queue
claiming ordered only by run_after with no tiebreak, an UPDATE...
RETURNING with no result-order guarantee, and independent per-
endpoint backoff rescheduling all destroy it downstream.
@zaridan

zaridan commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

The interface doc claimed the drain is "reproducible: the same rows
come out in the same order every time" without qualifying that this
only holds for repeated calls against a fixed eligible set. Under
concurrent drainers, SKIP LOCKED partitions eligible rows between
them unpredictably, so no single global order exists across
overlapping calls. The safety property (no double-claim) already
held; only the reproducibility wording overclaimed.
@zaridan
zaridan merged commit a4d2943 into main Aug 10, 2026
10 of 11 checks passed
@zaridan
zaridan deleted the docs/ht-127-event-ordering-contract branch August 10, 2026 19:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant