Skip to content

fix(session-start): stand down instead of an unfiltered watcher when a resumed seat is unidentified (#982) - #993

Open
fujibee wants to merge 2 commits into
mainfrom
fix/982-resume-unfiltered-watcher
Open

fix(session-start): stand down instead of an unfiltered watcher when a resumed seat is unidentified (#982)#993
fujibee wants to merge 2 commits into
mainfrom
fix/982-resume-unfiltered-watcher

Conversation

@fujibee

@fujibee fujibee commented Aug 25, 2026

Copy link
Copy Markdown
Owner

Fixes #982.

Problem

On a resumed session with no role-session record, session-start.sh fell back to the generic watch.sh directive (no 4th <agent> arg). On a project with several registered seats, that watcher subscribes to every pair and, as it delivers, stamps read_at and advances each pair's read cursor to the tip — so it consumes other seats' unread mail, and those seats never get those messages delivered. This actually happened: 37 messages addressed to two other seats were marked read by a mis-seated resume. Fail-open is the wrong default here.

Change — fail closed

  1. Narrowing. When no role-session record matches but an actas.<team>__<agent>.session lock is owned by this session's bare sid, re-seat to that pair and emit the role-filtered directive. The record the branch wanted and the lock that exists carry the same fact. An ambiguous 2+ match is left unidentified rather than guessed.
  2. Fail-closed fallback. When the seat still can't be established: emit a generic watcher only for a single-pair project (no one else's mail to take); with more than one pair, emit no watcher and say why, naming /agmsg actas <name>. A silent no-watcher is indistinguishable from "no messages arriving", so the stand-down is explicit and points at the recovery.

Severity is accurate: this is a delivery miss, not a loss — messages stay in the store and history.sh <team> <agent> returns them. No storage-layer change.

Tests

tests/test_resume_seat_guard.bats (5 tests, on a two-seat project — the shape in which the defect exists; a single-seat project can't reproduce it). The load-bearing tests run the directive as emitted and observe which pairs' read cursors move — a string check on the directive text would stay green if the wrong watcher were emitted or watch.sh ignored its 4th arg.

Verified by mutation:

  • Fail-open regression (always emit generic) → the stand-down tests go red, and the behavioral test shows a bystander seat's cursor actually advanced.
  • Broken narrowing → the seat's own mail is not delivered (red).

Existing #339 role-directive tests and the full delivery/session-start/role-session suites stay green. Enforced-assertions checker at baseline (638).

…a resumed seat is unidentified (#982)

On a resumed session with no role-session record, the fallback emitted the
generic watch.sh directive (no 4th <agent> arg). On a project with several
registered seats that watcher subscribes to every pair and, as it delivers,
stamps read_at and advances each pair's read cursor to the tip -- so it
consumes other seats' unread mail, which those seats then never receive.
Fail-open is the wrong default here.

Fail closed instead:

- Narrowing: when no role-session record matches but an actas
  <team>__<agent>.session lock is owned by this session's bare sid, re-seat to
  that pair and emit the role-filtered directive. The record the branch wanted
  and the lock that exists carry the same fact; an ambiguous 2+ match is left
  unidentified rather than guessed.
- When the seat still cannot be established: emit a generic watcher only for a
  single-pair project (nobody else's mail to take); with more than one pair,
  emit no watcher and say why, naming `/agmsg actas <name>`. A silent
  no-watcher is indistinguishable from "no messages arriving", so the
  stand-down is explicit and points at the recovery.

This is a delivery miss, not a loss: messages remain in the store and
history.sh returns them.

Tests (tests/test_resume_seat_guard.bats) run the directive as emitted and
observe which pairs' read cursors move -- a string check on the directive text
would stay green if the wrong watcher were emitted or watch.sh ignored its 4th
arg. A fail-open regression advances a bystander seat's cursor (red); a broken
narrowing fails to deliver the seat's own mail (red).
…d vs inferred from the actas lock (#982)

The role-filtered directive is prose the next session reads and acts on: it
launches the watcher the text describes. The narrowing path (#982) reused the
record path's wording — "this session was recorded as that role's seat" — but it
has no record; it inferred the seat from an actas lock this sid still owns.
Telling the reader a thing was recorded when it was inferred hands them a claim
they cannot check, and this whole fix exists because a directive was trusted and
executed as-is.

Split the sentence on how the seat was established:
- record path: "was recorded as that role's seat" (unchanged).
- narrowing path: names the actas lock as the basis and why it stands in for the
  record, so the reader can weigh "what if that lock is stale?".

Tests assert both directions of the distinction, so an inferred seat can never
again read as a recorded one.
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.

resume: a seat without a role-session record gets an unfiltered watcher that marks other seats' messages read

1 participant