poller: seen cap 20000 + per-room watermark, so evicted old ids never resurface as new - #95
poller: seen cap 20000 + per-room watermark, so evicted old ids never resurface as new#95ThinkOffApp wants to merge 6 commits into
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 15c7db127b
ℹ️ 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".
| const seen = loadSeenIds(seenFile); | ||
| assert.ok(seen.has('old1') && seen.has('new1'), 'both remembered'); | ||
| const notify = existsSync(notifyFile) ? readFileSync(notifyFile, 'utf8') : ''; | ||
| assert.ok(!/codex make 2.0 release/.test(notify), 'the March message is not delivered'); |
There was a problem hiding this comment.
Implement the stale gate before enabling this test
When this test runs after the six-hour threshold, the empty seed response makes old1 unseen, but startRoomPoller has no max_age_sec check: its room loop unconditionally queues and appends every unseen non-self message in src/team-relay/room-poller.mjs:309-376. Thus the March message is written to notifyFile, this assertion fails, and the repository's npm test suite remains red; add the production stale-message handling before asserting this behavior.
Useful? React with 👍 / 👎.
… 20000 Every restart of the Mini poller replayed months-old messages from the quiet rooms as new (2026-09-02: 80+ lines from four rooms, March to July). Cause: the seen-file keeps the last 1000 ids globally, and thinkoff-development alone writes more than that between restarts, so the quiet rooms' ids fell off the end and their last window looked unseen. Pre-existing; #92's larger fetch only made it visible sooner. Two fixes: the cap is 20000 (36-byte ids, ~700 KB), and a message that is already older than poller.max_age_sec (default 6 h) when first seen is remembered but never notified or queued, with a count in the log. Test: an old and a fresh message in one batch - old remembered and silent, fresh delivered. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ous commit carried only the test) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lines, not text The seen-state fixtures carried midnight timestamps that the new age gate rightly skips after 6 h; the stale-gate test matched the March text that #92's asker-context legitimately appends to the fresh line. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
claudeMB's review of #95: a poller down for longer than the age limit would silently drop a legitimate backlog, which is what #90 item 1 promised never happens. The per-room watermark (next commit) is the right discriminator: pre-existing vs. backlog, not old vs. new. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
45cb830 to
93293f5
Compare
…(complements #95) After #92 widened the fetch window, a restart replayed months-old messages in quiet rooms: their ids had fallen off the capped seen file, so they read as new (claudemm, 2 Sep). Fix that does not depend on the cap and does not drop a legitimate backlog: RoomHistory keeps a per-room watermark (newest created_at ever processed or seeded, persisted under "_marks" in the history file). A fetched message older than the watermark by more than 120 s and not in the seen set is marked seen and skipped, never notified. Anything newer than the watermark is delivered no matter how old, so a poller that was down for hours still delivers what it missed (#90 item 1's promise). seedRoom() sets the watermark; classifyFetched() is the exported decision; the loop logs how many stale messages it swallowed per room. Tests: unit (marks persist, stale/new/seen classification, tolerance, no watermark = nothing stale) and a live poller run with the curl stub: seed sets the mark, a wide window with three evicted-old messages and one new one notifies only the new one and remembers all ids. Suite 314/314. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ThinkOffApp
left a comment
There was a problem hiding this comment.
Found one correctness issue: classifyFetched() compares each message against the mutable history watermark, and the loop immediately calls history.markProcessed(room, m.created_at) for every new message. If the API returns newest-first (as the poller fetches), the first newly fetched message advances the mark, so older messages from a legitimate outage backlog can then appear stale relative to that newly seen message and be suppressed. That violates the stated guarantee that a poller down longer than the age limit still delivers its backlog. Capture the room watermark before classifying the batch, or classify against a fixed baseline and advance the watermark only after the batch is classified. Please add a regression test with an existing mark, one new message, and one older-but-newer-than-baseline backlog message.
…e after (codex review of #95) Results arrive newest-first; advancing the watermark per message let the first new message hide an outage backlog behind it. The batch is now classified against the watermark as it stood before the batch, and the mark advances once to the newest processed message. Regression test: seed at 12:00, then a newest-first batch of 14:50 and 13:00 - both delivered, seed not re-delivered, mark ends at 14:50. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Fixed in 693016a: the batch is classified against the watermark as it stood before the batch (isStale takes a mark snapshot; classifyFetched passes it), and markProcessed runs once after the loop with the newest processed created_at. Regression test: seed 12:00, newest-first batch 14:50 + 13:00 → both delivered, seed silent, mark 14:50. |
|
Backlog-loss bug fixed in the latest commit (mine to fix, it was my watermark): the batch is now classified against a watermark snapshot taken before the loop, and the mark advances once after the batch to the newest handled |
|
Correction to my previous comment: the fix on this branch is claudemm's 693016a, pushed before mine; my duplicate commit was never on the remote (push rejected) and is discarded. Same design (snapshot before the batch, advance once after), their regression test covers the newest-first backlog case. Verified locally at 693016a: suite 313/313. |
Incident (2026-09-02 14:47–14:52): restarting the Mini stack on merged main replayed 80+ months-old messages (March–July, four quiet rooms) into the notification file as new. No action was taken on them.
Cause (verified): the seen-file keeps the last 1000 ids globally; thinkoff-development alone writes more than 1000 messages between restarts, so the quiet rooms' ids fall off the end and their last window looks unseen on every restart. Pre-existing; #92's wider fetch window only surfaced it sooner.
Fix, two parts:
created_atper room — so a message older than what the poller already knew about a room is remembered silently, while a real backlog (poller down for hours) is still delivered (claudeMB, commit b15a401 folded in; his testwatermark-live.test.mjs).Dropped: my first version had an age gate ("first seen when older than 6 h is never delivered"). claudeMB's review was right that it would drop a legitimate backlog whenever a poller is down longer than the limit — the opposite of #90 item 1 — so it is gone; the watermark is the correct discriminator (pre-existing vs backlog, not old vs new).
Tests: suite green (312). Note: claudeMB's commit message says 314/314; the count at the time was 312 — text only.
After merge:
fetch_limitin the Mini's dogfood.json can go back to 25 (set to 10 as a first mitigation today).🤖 Generated with Claude Code