Skip to content

poller: seen cap 20000 + per-room watermark, so evicted old ids never resurface as new - #95

Open
ThinkOffApp wants to merge 6 commits into
mainfrom
fix/poller-stale-gate
Open

poller: seen cap 20000 + per-room watermark, so evicted old ids never resurface as new#95
ThinkOffApp wants to merge 6 commits into
mainfrom
fix/poller-stale-gate

Conversation

@ThinkOffApp

@ThinkOffApp ThinkOffApp commented Sep 2, 2026

Copy link
Copy Markdown
Owner

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:

  • seen cap 20000 (36-byte ids → ~700 KB) so quiet rooms never fall off in practice (claudemm);
  • per-room watermark in the history file — the newest processed/seeded created_at per 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 test watermark-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_limit in the Mini's dogfood.json can go back to 25 (set to 10 as a first mitigation today).

🤖 Generated with Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-02T14:54:32.376843Z 15c7db1 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@ThinkOffApp

Copy link
Copy Markdown
Owner Author

Code landed in 170c0c5 — the first push carried only the test (a patch anchor duplicated after the #92 merge). Stale-gate test green, suite green.

@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: 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".

Comment thread test/stale-gate.test.mjs Outdated
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');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

@ThinkOffApp

Copy link
Copy Markdown
Owner Author

Green at 45cb830: suite pass 309/309. The two seen-state tests had midnight fixtures the age gate now rightly skips; the stale-gate assertion now checks for a delivered LINE, since #92's asker-context legitimately quotes the old text inside the fresh line.

ThinkOffApp and others added 4 commits September 2, 2026 17:57
… 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>
@ThinkOffApp
ThinkOffApp force-pushed the fix/poller-stale-gate branch from 45cb830 to 93293f5 Compare September 2, 2026 14:57
…(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 ThinkOffApp changed the title poller: never deliver a message first seen when already old; seen cap 20000 poller: seen cap 20000 + per-room watermark, so evicted old ids never resurface as new Sep 2, 2026

@ThinkOffApp ThinkOffApp left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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>
@ThinkOffApp

Copy link
Copy Markdown
Owner Author

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.

@ThinkOffApp

Copy link
Copy Markdown
Owner Author

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 created_at. isStaleAgainst(mark, ts) is the explicit form; classifyFetched takes the snapshot. Regression test an outage backlog behind the newest message is delivered: seed at 1 Sep 20:00, newest-first batch of [today 14:50, today 06:00, February]; first two delivered, February swallowed, mark ends at 14:50. Full suite 313/313.

@ThinkOffApp

Copy link
Copy Markdown
Owner Author

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.

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