Skip to content

fix(watch): mark monitor-delivered messages as read - #486

Closed
u-ichi wants to merge 1 commit into
fujibee:mainfrom
u-ichi:fix/watch-mark-emitted-read
Closed

fix(watch): mark monitor-delivered messages as read#486
u-ichi wants to merge 1 commit into
fujibee:mainfrom
u-ichi:fix/watch-mark-emitted-read

Conversation

@u-ichi

@u-ichi u-ichi commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Problem

watch.sh emits each new message to the Monitor stream and then advances its per-session watermark, but it does not update that message row's read_at. This leaves two conflicting states for the same message:

  • the Monitor consumer has received the body and the watcher watermark says it was delivered;
  • the database, unread counts, and a later inbox read still treat the row as unread.

We observed this through Grok Build's official Monitor path: the message body reached the Grok UI, while the corresponding messages.read_at remained NULL. A later unread query therefore could not distinguish that delivered row from a message that had never reached the Monitor stream. This affects any runtime using watch.sh, not only Grok.

Reproduction

  1. Start watch.sh for a registered agent and wait for its watermark file.
  2. Send a new message to that agent.
  3. Confirm that the message line appears on the watcher's stdout.
  4. Query the emitted message row: read_at is still NULL, even though the watermark has advanced past its ID.

Change

After stdout accepts a message line, watch.sh now updates read_at for that exact message ID. The watermark advances only after that update succeeds.

Condition read_at Watermark Watcher
stdout and DB update succeed updated for the emitted row advances to the row ID continues
stdout is closed unchanged unchanged exits
DB update fails unchanged unchanged exits nonzero; the row is replayable after recovery
fresh watcher skips older history unchanged for skipped rows starts at current max ID continues

Scope

read_at here means that the message was successfully emitted to the Monitor stream. It does not claim that the receiving agent completed or handled the work. This PR does not change the schema, dispatcher behavior, message claiming, agent-specific hooks, or historical-message replay policy.

Tests

  • successful stdout emission marks only the delivered row and advances the watermark
  • a fresh watcher does not mark skipped historical rows as read
  • closed stdout leaves the row unread and the watermark unchanged, then restart replays it
  • a forced DB update failure exits nonzero, preserves the row and watermark, and replays after recovery
  • bash -n scripts/watch.sh
  • git diff --check

The Bats suite was not available locally; the equivalent delivery and failure scenarios above were run with an isolated SQLite database. GitHub CI runs the Bats suite on Ubuntu, macOS, and Windows.

@u-ichi u-ichi changed the title fix(watch): mark emitted messages as read fix(watch): mark monitor-delivered messages as read Jul 24, 2026
fujibee pushed a commit that referenced this pull request Jul 27, 2026
…t replay it (#439)

`inbox.sh` and `check-inbox.sh` both mark delivered rows' `read_at` before
displaying them, but `watch.sh` — the live Monitor-mode delivery path — never
touched `read_at` at all. A message delivered live through a watcher stayed
`read_at IS NULL` forever, so a later `inbox.sh` / `check-inbox.sh` call, or the
`history.sh` unread marker, would surface it again as if it were still unread.

Adds a `mark_read()` helper called from both the normal delivery branch and the
`ctrl:despawn` control-row branch, so control messages handled by the watcher
also stop lingering as unread.

The helper is scoped to the definitive receiver for a role. A broad watcher
(no `actas` name) subscribes to every registered role in the project, so
marking unconditionally would let a leader's default watcher write read state
for a role that has its own exclusive watcher. It skips when an exclusive ready
sentinel exists for that (team, agent).

This is a best-effort mark on local write success, not a delivery
acknowledgement; a stronger guarantee needs the claim/ack redesign tracked in
issue #373.

The same defect was independently diagnosed and fixed in PR #486 by
@u-ichi, who reached the same root cause — that the watermark stops a watcher
re-streaming a row but never touches `read_at`. That PR is closed in favour of
this one, which additionally guards the broad-watcher case, keeps the mark
idempotent, and degrades rather than exiting when the database is unavailable.
Issue #373, which #486's author filed, is where the durable fix belongs.

Closes #439
@fujibee

fujibee commented Jul 27, 2026

Copy link
Copy Markdown
Owner

@u-ichi

Thank you for this, and for #373 — the diagnosis in both is correct: the watermark stops a watcher re-streaming a row but never touches read_at, so a later inbox.sh re-surfaces messages the monitor already delivered.

#439 was taken for this specific fix, and the reason is narrow rather than a judgement on the code here. A broad watcher (one with no actas name) subscribes to every role registered in the project, so marking read_at unconditionally lets it write read state for a role that has its own exclusive watcher — a leader's default watcher clobbering the read state an actas-ed member is responsible for. #439 carries the guard for that case and applies the same marking to the ctrl:despawn path so the two do not drift.

Worth saying plainly: #439 explicitly notes that this whole approach is a best-effort mark on local write success, not a delivery acknowledgement, and that a real guarantee needs the claim/ack redesign — citing your #373 as where that belongs. So the durable answer to this problem is the one you filed, not either of these two patches. Your independent diagnosis is also credited in the merge commit of #439.

Closing this one to keep a single implementation of the same behaviour. #372 and #373 are still open and are being looked at on their own merits; #372 adds interface surface, so it needs a more deliberate evaluation before anything is implemented, which is why it has not moved yet.

@fujibee fujibee closed this Jul 27, 2026
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.

2 participants