Skip to content

poller: remember each message as it is handled, durably (#90 item 1) - #91

Merged
ThinkOffApp merged 4 commits into
mainfrom
fix/seen-state-per-message
Sep 2, 2026
Merged

poller: remember each message as it is handled, durably (#90 item 1)#91
ThinkOffApp merged 4 commits into
mainfrom
fix/seen-state-per-message

Conversation

@ThinkOffApp

@ThinkOffApp ThinkOffApp commented Sep 2, 2026

Copy link
Copy Markdown
Owner

#90 item 1 (split agreed with claudeMB in the room).

Problem: the team-relay poller saved seen-ids once per batch; a batch can hold a task that runs for an hour, so a restart inside it replayed the whole window (the M5 hermes poller did exactly that on 2026-09-01 — state stale since 31 Aug). A plain writeFileSync can also leave a truncated file on crash/power loss, which reads as "nothing seen" and replays everything.

Change:

  • src/common/seen-ids.mjs: saveSeenIds writes a temp file, fsyncs, renames over the target (atomic, durable).
  • src/team-relay/room-poller.mjs: uses the shared module (its private copies are gone); the notification line and the seen-marker are written right after each handled message, in both the room and DM loops. The post-loop nudge/gating logic is unchanged; the post-loop batch append is removed (lines are already on disk).

Tests (test/seen-state.test.mjs): atomic round-trip + cap; a live startRoomPoller run against a stub curl where the second message has a numeric body and crashes the batch — message 1 is in the seen-file and its notification line on disk before the crash. Positive control: the same test fails on the previous poller. Suite 299/299.

🤖 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-02T00:46:11.969886Z eefd1ce 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.

…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 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: eefd1ce531

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

newCount++;
// Handled = queued + notified; only then is it safe to remember it.
appendNotifications(notifyFile, [line]);
saveSeenIds(seenFile, seen);

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 Preserve pending nudges before committing seen state

When the process exits or a later message throws after this save, an earlier owner message or @mention is already marked seen even though the batch-level triggerNudge has not run. On restart that message is skipped and no longer sets hasOwnerMessage/hasMention, so the configured tmux or command wake can be permanently lost; the DM loop has the same ordering. Persist pending wake state or issue the required nudge before making the message non-retryable.

Useful? React with 👍 / 👎.

Comment on lines +320 to +321
appendNotifications(notifyFile, [line]);
saveSeenIds(seenFile, seen);

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 Flush delivered records before the durable seen marker

On a power loss, appendFileSync does not guarantee that the queue and notification writes have reached stable storage, while saveSeenIds immediately fsyncs the seen file. The marker can therefore survive while the supposedly handled event and notification disappear; after restart the poller skips the message and loses it permanently. The queue and notification files must be flushed before durably committing the seen ID.

Useful? React with 👍 / 👎.

Comment thread src/common/seen-ids.mjs
} finally {
closeSync(fd);
}
renameSync(tmp, path);

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 Fsync the parent directory after renaming

When the host loses power after this rename, fsyncing only the temporary file does not make the directory-entry replacement durable. Depending on the filesystem, restart can expose the previous seen file or no target at all, causing the exact replay window this change is intended to prevent. Open and fsync the parent directory after renameSync before considering the state durable.

Useful? React with 👍 / 👎.

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

Reviewed the atomic seen-state update and per-message persistence in both room and DM paths. Focused and full test suites pass (299/299); no blocking issues found.

Petrus Pennanen and others added 3 commits September 2, 2026 02:49
…w); seedRoom() exported with a regression test

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The team-relay poller saved its seen-ids once per batch, and a batch can
hold a task that runs for an hour: a restart inside it replayed the whole
window. The M5 hermes poller did exactly that on 2026-09-01 (state stale
since 31 Aug). Now the notification line and the seen-marker are written
right after each handled message, rooms and DMs, and the shared
saveSeenIds writes atomically (temp + fsync + rename) so a crash or power
loss never leaves a truncated file that reads as 'nothing seen'. The
poller's private copy of the seen-id helpers is gone in favour of the
shared module.

Tests: atomic write round-trip and cap; a live poll where the second
message crashes the batch - message 1 is persisted and notified before
the crash (fails on the previous code, verified).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…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 force-pushed the fix/seen-state-per-message branch from eefd1ce to 48a7225 Compare September 2, 2026 00:54
@ThinkOffApp

Copy link
Copy Markdown
Owner Author

Rebased onto #92's head (fa043ae) so it merges cleanly right after #92. Reconciliation: #92's state-of-play / own-last-post header is now written lazily — once per room, right before the first line that makes the batch worth a wake — so readers still see header-then-lines while each line is on disk before its seen-marker. New test covers header placement and no duplicate lines. Suite pass 308.

@ThinkOffApp
ThinkOffApp merged commit 31eb765 into main Sep 2, 2026
3 checks passed
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