Skip to content

poller: per-room history so a mention arrives with its thread; state-of-play line (#90) - #92

Merged
ThinkOffApp merged 2 commits into
mainfrom
feat/room-context-history
Sep 2, 2026
Merged

poller: per-room history so a mention arrives with its thread; state-of-play line (#90)#92
ThinkOffApp merged 2 commits into
mainfrom
feat/room-context-history

Conversation

@ThinkOffApp

Copy link
Copy Markdown
Owner

Items 2, 6 and 7 of #90 (petrus: "go ahead").

Problem. A mention reached the agent with a 100-character preview of its parent, or target … not in recent window when the parent was older than the 10-message fetch, and the room's settled facts lived in messages the agent never saw. Tonight hermes read "new card" as a GPU card for exactly this reason. The room API has no single-message fetch and ignores pagination, but every poller already sees every message pass by.

Change.

  • src/common/room-history.mjs: RoomHistory, a per-room JSON history next to seen_file (400 messages per room, saved every poll), plus threadSuffix (parent in full with author and time, up to two ancestors), previousSuffix (the asker's previous message), stateOfPlayLine (messages starting state: or settled:, deduped, newest wins, one line) and ownLastPostLine.
  • reply-context.resolveReplyTargets(msgs, lookup): an optional lookup resolves targets outside the batch through the history; embedded/batch snippet 120 to 300 chars. Existing callers unchanged.
  • room-poller: fetch 25 per poll (poller.fetch_limit), remember every batch, one deeper fetch (100) per room per poll when a reply target is still unknown; each notification line now carries body (400) + thread + the asker's previous message for owner messages and mentions. When a room's batch contains an owner message or a mention, one STATE OF PLAY line and one YOUR LAST POST HERE line are prepended for that room.
  • Notification-file contract kept: one physical line per message. The thread rides inside the line.
  • README documents poller.history_file, poller.fetch_limit and the state: convention; the codex example config gets a history_file.

Tests. 7 new in test/room-history.test.mjs (persist/cap/dedupe, cross-poll target resolution, bounded chain, unresolved marker, previous message, state entries, own last post); suite 304/304.

Not in this PR. Item 3 (addressing gate, needs #78 reactions), item 5 (post budget). Item 1 (per-message seen-save) is claudemm's and touches the same pollRooms loop; whichever merges second rebases.

🤖 Generated with Claude Code

…of-play line (#90 items 2, 6, 7)

Why: a mention reached the agent with a 100-char preview of its parent, or
"target not in recent window" when the parent was older than the 10-message
fetch; settled facts lived in messages the agent never saw. hermes read
"new card" as a GPU card for exactly this reason (issue #90).

What:
- src/common/room-history.mjs: RoomHistory (JSON file next to seen_file,
  400 messages per room, persisted every poll) plus threadSuffix (parent in
  full with author and time, up to two ancestors), previousSuffix (the
  asker's previous message), stateOfPlayLine ("state:" / "settled:"
  messages, deduped, newest wins) and ownLastPostLine.
- reply-context.resolveReplyTargets(msgs, lookup): resolves targets outside
  the batch through the history; snippet 120 -> 300 chars.
- room-poller: fetch 25 per poll (poller.fetch_limit), remember every
  batch, one deeper fetch (100) when a reply target is still unknown, and
  the notification line carries body (400) + thread + previous message for
  owner messages and mentions. When a room's batch has an owner message or a
  mention, one STATE OF PLAY line and one YOUR LAST POST HERE line are
  prepended for that room.
- Notification-file contract kept: one physical line per message.
- README: poller.history_file, poller.fetch_limit, the state: convention.

Tests: 7 new in test/room-history.test.mjs; suite 304/304.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@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-02T00:45:25.907244Z 9252ab0 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

Reviewed the room-poller hunk for the interaction with #91 (per-message seen-state). No blocking issue in #92 itself. Two notes for the rebase I will do after this merges: (1) #92 buffers each room's lines and appends them after the loop with the state-of-play / own-last-post header; #91 needs each message's line on disk before its seen-marker. Reconcile by emitting the header lazily — once, right before the first priority line of that room (owner/mention is known per message) — then append line + marker per message. Readers still see header-then-lines. (2) history.save() once per batch has the same crash window as the old seen-save; worth the same atomic write (temp + fsync + rename, now in src/common/seen-ids.mjs) and a per-message call, or at least per room.

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

P1: On a fresh install, the seed branch fetches and marks the current 50 messages as seen but never calls history.remember() for them. The first subsequent reply to any seeded parent therefore still renders as "target ... not in recent window", which defeats the core cross-poll context guarantee until that parent happens to be refetched. Please remember the seed batches before saving seen IDs (and add an integration/regression test for a reply to a seeded message).

@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: 9252ab03ca

ℹ️ 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".

* and m._replyTarget in place; returns the batch for chaining.
*/
export function resolveReplyTargets(msgs) {
export function resolveReplyTargets(msgs, lookup) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Apply history to the unified GroupMind adapter

When GroupMind runs through the supported ide-agent-kit platform watch path, bin/cli.mjs creates a UnifiedPoller with groupmindAdapter, but src/adapters/groupmind.mjs:19-35 still fetches only 10 messages and invokes this function without the new lookup. Consequently poller.history_file, poller.fetch_limit, state/previous/own context, and old-parent resolution have no effect on that path, so replies outside its ten-message window retain the unresolved marker. Pass the history-aware lookup and formatting through the unified adapter as well.

Useful? React with 👍 / 👎.

// and the room's "state:" facts (issue #90). One file per poller.
const historyFile = config?.poller?.history_file || seenFile.replace(/\.txt$/, '') + '-history.json';
const fetchLimit = parsePositiveInt(config?.poller?.fetch_limit, 25);
const history = new RoomHistory(historyFile);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Seed the new history from the startup fetch

When the history file is absent—on first launch or an upgrade—this initializes an empty history, while the existing startup seed fetches 50 messages only to mark their IDs seen. The initial poll then remembers just fetchLimit messages (25 by default), permanently discarding seeded messages 26–50 from history; a recent state: fact or the agent's own post in that range will therefore be missing from the next owner's non-reply notification. Remember and save each seed batch, or perform an equivalent initial history backfill.

Useful? React with 👍 / 👎.

…w); seedRoom() exported with a regression test

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ThinkOffApp

Copy link
Copy Markdown
Owner Author

Fixed: the first-run seed now goes through seedRoom(), which marks the batch seen and stores it in the history, then history.save(). Regression test seeds two messages, then resolves a next-poll reply to one of them through the history. Suite 305/305.

ThinkOffApp added a commit that referenced this pull request Sep 2, 2026
…t priority line

Rebased onto #92 (fa043ae). #92 buffered each room's lines and wrote the
state-of-play / own-last-post header with them after the loop; #91 writes
each line as it is handled. Reconciled: the header goes to the
notification file right before the first line that makes the batch worth
a wake, so readers still see header then lines and every line is on
disk before its seen-marker. Test: header once, before the owner line,
no line written twice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ThinkOffApp
ThinkOffApp merged commit fa043ae into main Sep 2, 2026
3 checks passed
ThinkOffApp added a commit that referenced this pull request Sep 2, 2026
…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>
ThinkOffApp added a commit that referenced this pull request Sep 2, 2026
… 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>
ThinkOffApp added a commit that referenced this pull request Sep 2, 2026
…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>
ThinkOffApp pushed a commit that referenced this pull request Sep 2, 2026
…(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>
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