feat(ops): size the bot event stream from evidence, not a constant (#415) #465

Merged
claude-bot merged 1 commit from fix/415-bot-stream-capacity into main 2026-08-31 16:02:25 +00:00
Contributor

Closes #415 (LOW).

The defect

BOT_EVENTS_MAXLEN was a hardcoded 10,000 — chosen for a single self-hosted group and never revisited for multi-tenant volume, where every tenant's events share this one stream. XADD MAXLEN drops the oldest entries whether or not anyone read them, and the stream is the durable delivery path. A trimmed event has no catch-up; it is simply gone.

What ships

settings.bot_events_maxlen, read at call time so a deployment can raise it without a code change. Documented in .env.example with the reason it might need raising.

The third acceptance criterion is the substance here. A configurable number nobody can size is still a guess. detect_unread_trim compares the consumer group's last-delivered-id against the stream's oldest surviving entry: if the group never reached what is now the first entry, everything between it and the cursor was trimmed unread. An hourly Beat task publishes that as bot_events_trimmed_unread_total.

Two details that are load-bearing

Stream ids are <ms>-<seq> and must be compared numerically. "9-0" sorts after "10-0" as text, which inverts the exact comparison this makes and would report loss on a perfectly healthy stream. There is a test for both directions, and it fails against a lexical-comparison mutant.

"Nothing to compare" returns None, not 0, and leaves the gauge untouched. No stream, no consumer group, or an empty stream are all "we cannot tell yet" — and a zero the detector has not earned is indistinguishable from a genuine all-clear. That is the more dangerous of the two answers, so it is never published. Two tests pin it.

Similarly, a gauge nobody writes reads as zero forever, so there is a test asserting the Beat task is actually scheduled — a detector nobody runs detects nothing.

Verification

Mutation-checked on the id comparison, which is the subtle part. 15 new tests. 1,503 backend tests pass (was 1,488). Lint clean at pinned ruff 0.4.4.

BOT_EVENTS_MAXLEN the module constant is kept for any caller that imported it, but the publish paths read the setting — so overriding the setting actually takes effect, which is what the old constant would have quietly prevented.

Note on criterion 2

The hosted deployment profile sets a value sized for expected multi-tenant volume

Left at the 10,000 default rather than inventing a number. There is no measured volume to size against yet — that is precisely what the new metric is for. Once an instance has run with real traffic, the gauge says whether the default is short and by how much. Picking a bigger number now would just be a differently-arbitrary constant.

🤖 Generated with Claude Code

Closes #415 (LOW). ## The defect `BOT_EVENTS_MAXLEN` was a hardcoded 10,000 — chosen for a single self-hosted group and never revisited for multi-tenant volume, where **every tenant's events share this one stream**. `XADD MAXLEN` drops the oldest entries whether or not anyone read them, and the stream is the *durable* delivery path. A trimmed event has no catch-up; it is simply gone. ## What ships `settings.bot_events_maxlen`, read at call time so a deployment can raise it without a code change. Documented in `.env.example` with the reason it might need raising. **The third acceptance criterion is the substance here.** A configurable number nobody can size is still a guess. `detect_unread_trim` compares the consumer group's `last-delivered-id` against the stream's oldest surviving entry: if the group never reached what is now the first entry, everything between it and the cursor was trimmed unread. An hourly Beat task publishes that as `bot_events_trimmed_unread_total`. ## Two details that are load-bearing **Stream ids are `<ms>-<seq>` and must be compared numerically.** `"9-0"` sorts *after* `"10-0"` as text, which inverts the exact comparison this makes and would report loss on a perfectly healthy stream. There is a test for both directions, and it fails against a lexical-comparison mutant. **"Nothing to compare" returns `None`, not `0`, and leaves the gauge untouched.** No stream, no consumer group, or an empty stream are all "we cannot tell yet" — and a zero the detector has not earned is indistinguishable from a genuine all-clear. That is the more dangerous of the two answers, so it is never published. Two tests pin it. Similarly, a gauge nobody writes reads as zero forever, so there is a test asserting the Beat task is actually scheduled — a detector nobody runs detects nothing. ## Verification Mutation-checked on the id comparison, which is the subtle part. 15 new tests. **1,503 backend tests pass** (was 1,488). Lint clean at pinned ruff 0.4.4. `BOT_EVENTS_MAXLEN` the module constant is kept for any caller that imported it, but the publish paths read the setting — so overriding the setting actually takes effect, which is what the old constant would have quietly prevented. ## Note on criterion 2 > The hosted deployment profile sets a value sized for expected multi-tenant volume Left at the 10,000 default rather than inventing a number. There is no measured volume to size against yet — that is precisely what the new metric is for. Once an instance has run with real traffic, the gauge says whether the default is short and by how much. Picking a bigger number now would just be a differently-arbitrary constant. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(ops): size the bot event stream from evidence, not a constant (#415)
All checks were successful
CI / Docker image build (pull_request) Successful in 32s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 48s
CI / Bot/backend version sync (pull_request) Successful in 53s
CI / Backend lint (ruff) (pull_request) Successful in 1m0s
CI / Bot tests and audit (pull_request) Successful in 1m19s
CI / Frontend tests, audit, and build (pull_request) Successful in 1m22s
CI / Backend migration, tests, and audit (pull_request) Successful in 5m4s
acbf1eada3
BOT_EVENTS_MAXLEN was a hardcoded 10,000, chosen for one self-hosted group
and never revisited for multi-tenant volume where every tenant's events
share the one stream. XADD MAXLEN drops the oldest entries read or not, and
the stream is the durable delivery path — a trimmed event has no catch-up.

Now settings.bot_events_maxlen, read at call time so a deployment can raise
it without a code change.

A configurable number nobody can size is still a guess, so the third
acceptance criterion is the substance here. detect_unread_trim compares the
consumer group's last-delivered-id against the stream's oldest surviving
entry: if the group never reached what is now the first entry, everything
between was trimmed unread. An hourly Beat task publishes that as
bot_events_trimmed_unread_total.

Two details that are load-bearing:

Stream ids are `<ms>-<seq>` and must be compared numerically — "9-0" sorts
after "10-0" as text, which inverts the exact comparison being made and
would report loss on a healthy stream. Mutation-checked.

"Nothing to compare" returns None, not 0, and leaves the gauge untouched. A
zero the detector has not earned is indistinguishable from a genuine all-
clear, and that is the more dangerous of the two answers.

15 new tests; the numeric-comparison one fails against a lexical mutant.
1,503 backend tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
claude-bot deleted branch fix/415-bot-stream-capacity 2026-08-31 16:02:27 +00:00
Sign in to join this conversation.
No description provided.