Count a canceled bridge stream's terminal from the items written #130

Merged
natsukium merged 1 commit from bridge/eof-stream-terminal into main 2026-10-12 22:25:46 +09:00
Owner

When a cancel op or stdin EOF canceled a live bridge stream, the bridge could write a clean end whose count claimed items it never wrote: e.g. sessions.search piped in with stdin closed right after printed {"v":1,"id":1,"event":"end","count":2} and no item lines.

Cause. Link::cancel marks the stream canceled in the client driver before the Cancel frame leaves, so items the daemon already had in flight decode as dropped-after-cancel. The daemon still answers End { count } with its producer's emitted total, and the bridge relayed that count. The same path served sessions.search, sessions.capture and notifications.subscribe, under both EOF and an explicit cancel.

Fix. The bridge counts the item lines it queues for stdout per op, under the lock that queues items and publishes the terminal, and uses that count in end. On an uncanceled stream the two counts are equal, so output there is unchanged and the goldens pass. A canceled stream still ends with a clean end, as the cancel-a-live-stream golden pins; the canceled failing terminal stays for a stream the daemon has not terminated when the EOF shutdown stops waiting.

  • Tests: a scripted daemon writes two matches and End{count:2} after it reads the Cancel, so the drop is deterministic. The explicit-cancel and EOF tests both require exactly end with count: 0 (they fail on main with count: 2).
  • Live repro against an isolated daemon: count: 2 with no items before, count: 0 after (4/4 each); with stdin held open both items and count: 2 as before.
  • cli.md bridge contract and CHANGELOG.md updated. just check passes; reviewed by pi (sol): PASS, both non-blocking notes applied.

Fixes #112.

When a `cancel` op or stdin EOF canceled a live bridge stream, the bridge could write a clean `end` whose `count` claimed items it never wrote: e.g. `sessions.search` piped in with stdin closed right after printed `{"v":1,"id":1,"event":"end","count":2}` and no item lines. **Cause.** `Link::cancel` marks the stream canceled in the client driver before the Cancel frame leaves, so items the daemon already had in flight decode as dropped-after-cancel. The daemon still answers `End { count }` with its producer's emitted total, and the bridge relayed that count. The same path served `sessions.search`, `sessions.capture` and `notifications.subscribe`, under both EOF and an explicit `cancel`. **Fix.** The bridge counts the item lines it queues for stdout per op, under the lock that queues items and publishes the terminal, and uses that count in `end`. On an uncanceled stream the two counts are equal, so output there is unchanged and the goldens pass. A canceled stream still ends with a clean `end`, as the `cancel-a-live-stream` golden pins; the `canceled` failing terminal stays for a stream the daemon has not terminated when the EOF shutdown stops waiting. - Tests: a scripted daemon writes two matches and `End{count:2}` after it reads the Cancel, so the drop is deterministic. The explicit-cancel and EOF tests both require exactly `end` with `count: 0` (they fail on main with `count: 2`). - Live repro against an isolated daemon: `count: 2` with no items before, `count: 0` after (4/4 each); with stdin held open both items and `count: 2` as before. - `cli.md` bridge contract and `CHANGELOG.md` updated. `just check` passes; reviewed by pi (sol): PASS, both non-blocking notes applied. Fixes #112.
cli: count a canceled bridge stream's terminal from the items written
All checks were successful
darwin / detect relevant changes (pull_request) Successful in 9s
darwin / darwin docs-only no-op (pull_request) Has been skipped
pr / nix flake check (pull_request) Successful in 39s
darwin / build felis (aarch64-darwin) (pull_request) Successful in 1m11s
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m50s
pr / wire schema is compatible with the base (pull_request) Successful in 4s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 28s
windows / cargo clippy (Windows cross) (pull_request) Successful in 15s
windows / detect relevant changes (pull_request) Successful in 13s
windows / windows docs-only no-op (pull_request) Has been skipped
windows / cargo nextest (Windows) (pull_request) Successful in 5m9s
windows / frontend smoke (Windows) (pull_request) Successful in 49s
b8759ff0ce
A bridge stream canceled by a `cancel` op or by stdin EOF ended with a
clean `end` carrying the daemon's own count while none of the items it
counted reached stdout. Link::cancel marks the stream canceled in the
client driver before the Cancel frame leaves, so every item the daemon
had in flight decodes as DroppedAfterCancel; the daemon still closes the
stream with End { count } covering those items, and the bridge relayed
that count. A consumer reads a clean terminal with count N as a complete
result of N lines, so `printf <search> | felis bridge` reported two
matches and printed none, and the count varied with how much of the
producer's last slice crossed the cancel.

The docs already decide the terminal: closing stdin cancels in-flight
operations, and the pinned cancel conversation ends a canceled stream
with a clean `end`. So the shape stays and only the count changes: the
bridge counts the item lines it queues, under the same lock that
publishes the terminal, and reports that. The daemon's count is no
longer carried through the link at all. Draining on EOF instead would
contradict the documented cancel, and a `canceled` error terminal for
every canceled stream would change the pinned explicit-cancel shape.

The scripted-daemon tests hold the producer until its Cancel arrives,
so the drop is certain rather than timing-dependent.

Assisted-by: Claude Code
natsukium deleted branch bridge/eof-stream-terminal 2026-10-12 22:25:46 +09:00
Sign in to join this conversation.
No reviewers
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
felis-terminal/felis!130
No description provided.