Skip to content

fix(watch): mark read_at on live delivery so a later inbox.sh does not replay it#439

Open
Masashi-Ono0611 wants to merge 2 commits into
fujibee:mainfrom
Masashi-Ono0611:fix/watch-mark-read-at
Open

fix(watch): mark read_at on live delivery so a later inbox.sh does not replay it#439
Masashi-Ono0611 wants to merge 2 commits into
fujibee:mainfrom
Masashi-Ono0611:fix/watch-mark-read-at

Conversation

@Masashi-Ono0611

@Masashi-Ono0611 Masashi-Ono0611 commented Jul 19, 2026

Copy link
Copy Markdown

Summary

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, even though the recipient already saw it via the watcher.

This adds a small mark_read() helper in watch.sh and calls it from both the
normal delivery branch (replacing the ambient assumption that display alone
was enough) and the ctrl:despawn control-row branch, so control messages
handled by the watcher also stop lingering as unread.

Why this is a distinct fix from #338 / #373

Test plan

  • New tests: tests/test_watch.bats ("watch: marks a delivered message's
    read_at so a later inbox.sh does not re-surface it") and
    tests/test_despawn.bats ("despawn: graceful — ctrl:despawn control row is
    marked read (does not linger as unread)").

  • bats tests/*.bats — 728/729 passing; the sole failure
    (install: watch.sh self-cleans a prior watcher on re-invocation for the same sid) is a pre-existing order-dependent flake unrelated to this diff,
    confirmed by running tests/test_install.bats alone (46/46 pass).

  • Cross-reviewed by two independent peers before submission (both found real
    issues, since fixed): a not-yet-integer-validated $id interpolated into
    SQL, the despawn control-row gap, and flaky fixed-sleep test timing
    (replaced with bounded polling helpers).

  • CI green

Update (2026-07-19): additional review pass, two more fixes

A follow-up /review:self-multi-model pass (Codex + Fugu, run independently
of the peer review above) found two more real issues before this had any
maintainer eyes on it, now fixed in the latest commit:

  • Scoping gap (Fugu, confirmed by direct code reading): a broad
    (non-actas) watcher subscribes to every registered role in the project.
    Without scoping, it would also mark read_at for a role that has its OWN
    exclusive watcher — e.g. a leader's default SessionStart watcher racing the
    read state an actas'd member's exclusive watcher is responsible for.
    mark_read now skips when an exclusive ready sentinel exists for
    (team, to) and this watcher isn't it. New test:
    tests/test_watch.bats ("a broad watcher does not mark read_at for a role
    with its own exclusive ready sentinel").
  • Pipe-delivery limitation (Codex, P2): a pipe write to the Monitor stream
    can succeed (accepted into a kernel buffer) even if the downstream reader
    is about to exit — there's no ack protocol here. Marking read_at
    immediately after a "successful" printf therefore isn't a delivery
    guarantee. This is now documented as a known best-effort limitation in the
    code rather than silently assumed away; a real fix needs the claim/ack
    redesign already tracked in Atomic message consumption: prevent duplicate delivery and inject-failure loss with claim/ack #373 — out of scope for this small bugfix.

Also: a genuine SQLite update failure is now logged to stderr instead of
silently swallowed.

Full suite re-verified: 730/730 (this worktree's copy of the pre-existing
install: watch.sh self-cleans... flake did not reproduce on this run).

…lay it

watch.sh's monitor delivery loop only advances the per-session watermark on
delivery; it never touches read_at. Messages streamed live therefore stay
read_at IS NULL forever, and a subsequent inbox.sh call (e.g. a respawned
session's actas re-registration) re-surfaces the entire history as "new".

Add a shared mark_read() helper (id validated as a bare integer before
interpolation into SQL, matching inbox.sh's own defensive stance) and call
it from both the normal delivery path and the ctrl:despawn control-row path,
so a broad watcher's later inbox scan does not resurface a stale despawn
control message either.

Tests: new coverage in test_watch.bats (delivery marks read_at, inbox.sh
does not re-report it) and test_despawn.bats (ctrl:despawn row is marked
read too). Existing fixed-sleep timing in the new watch.sh test replaced
with bounded polling to avoid CI flakiness.

Full bats suite green (728+ cases).
Two gaps found by /review:self-multi-model (Codex + Fugu, 2026-07-19):

- A broad (non-actas) watcher subscribes to every registered role in the
  project, so without scoping it would mark read_at for a role that has its
  OWN exclusive watcher — e.g. a leader's default SessionStart watcher racing
  the read state an actas'd member's exclusive watcher is responsible for.
  mark_read now skips when an exclusive ready sentinel exists for (team, to)
  and this watcher isn't it.

- A pipe write to the Monitor stream can succeed (into a kernel buffer) even
  if the downstream reader is about to exit — there is no ack protocol here.
  Documented as a known best-effort limitation rather than papering over it;
  a real fix needs the claim/ack redesign tracked in fujibee#373.

Also logs (rather than silently swallowing) a genuine SQLite update failure.
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