Keep OSC 7501 program-status records per session and list them #128

Merged
natsukium merged 3 commits from daemon/osc7501-records into main 2026-10-12 23:04:40 +09:00
Owner

Fourth and last step of #67. The daemon now keeps OSC 7501 program-status records per session and exposes them on every SessionInfo as program_status, so felis sessions list --format json, felis sessions info, the bridge and any roster reader see them. felis still draws nothing; support is experimental (spec revision 0.4).

  • OSC 7501 ; ? is answered with the bare reply, attached or not.
  • Records follow the spec: replace whole, clear removes the subtree, at most 256 per session with least-recently-updated eviction. A primary prompt start and PTY end (after the child's last bytes are parsed) drop working/blocked; RIS drops all. Records do not cross an in-place upgrade.
  • SessionInfo.program_status joins protocol minor 1 under the new ungated-field rule (#127): declared in ADDED_FIELDS, sent to every peer, a minor-0 peer skips it.
  • A roster reply that would exceed the frame limit is refused with an error naming its size and felis sessions info <id>, instead of a frame that drops the connection. Reaching it needs hundreds of sessions each full of maximal records.
  • Two existing paths fixed on the way: a failed PTY reply no longer discards the effects queued behind it, and roster-bearing Ops replies are charged by content against the subscriber backlog instead of a flat 64 bytes.
  • Human sessions info output spells bidi and control characters in producer text as <U+XXXX>; IPC and JSON carry the decoded text.

Docs: new reference and explanation pages for program status, support-matrix row, ipc.md ledger and field, cli.md, the product skill, CHANGELOG.

Checks: just check green; cargo-mutants on the store and the oversize path, 0 missed; pi design review (3 rounds, direction then simplified per review) and code review PASS.

Closes #67.

Fourth and last step of #67. The daemon now keeps OSC 7501 program-status records per session and exposes them on every `SessionInfo` as `program_status`, so `felis sessions list --format json`, `felis sessions info`, the bridge and any roster reader see them. felis still draws nothing; support is experimental (spec revision 0.4). - `OSC 7501 ; ?` is answered with the bare reply, attached or not. - Records follow the spec: replace whole, `clear` removes the subtree, at most 256 per session with least-recently-updated eviction. A primary prompt start and PTY end (after the child's last bytes are parsed) drop `working`/`blocked`; RIS drops all. Records do not cross an in-place upgrade. - `SessionInfo.program_status` joins protocol minor 1 under the new ungated-field rule (#127): declared in `ADDED_FIELDS`, sent to every peer, a minor-0 peer skips it. - A roster reply that would exceed the frame limit is refused with an error naming its size and `felis sessions info <id>`, instead of a frame that drops the connection. Reaching it needs hundreds of sessions each full of maximal records. - Two existing paths fixed on the way: a failed PTY reply no longer discards the effects queued behind it, and roster-bearing Ops replies are charged by content against the subscriber backlog instead of a flat 64 bytes. - Human `sessions info` output spells bidi and control characters in producer text as `<U+XXXX>`; IPC and JSON carry the decoded text. Docs: new reference and explanation pages for program status, support-matrix row, ipc.md ledger and field, cli.md, the product skill, CHANGELOG. Checks: `just check` green; cargo-mutants on the store and the oversize path, 0 missed; pi design review (3 rounds, direction then simplified per review) and code review PASS. Closes #67.
A program learns that the terminal keeps its status only from the
reply to `OSC 7501 ; ?`, and the spec recommends DA1 right after the
query so silence costs nothing; with the records now kept and listed
there is support to announce. The reply is bare: revision 0.4 reads a
reply without `states=` / `kinds=` as exactly the base vocabulary,
which is what the decoder admits. It always ends in ST, as the OSC 99
`p=?` reply does, and a cancelled query is not answered.

Assisted-by: Claude Code
protocol, grid, daemon, cli: keep OSC 7501 records per session and list them
All checks were successful
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 9s
darwin / darwin docs-only no-op (pull_request) Has been skipped
bench / instruction-count regression gate (pull_request) Successful in 56s
darwin / build felis (aarch64-darwin) (pull_request) Successful in 1m14s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 48s
pr / nix flake check (pull_request) Successful in 40s
windows / detect relevant changes (pull_request) Successful in 1m1s
windows / windows docs-only no-op (pull_request) Has been skipped
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m53s
pr / wire schema is compatible with the base (pull_request) Successful in 3s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 32s
windows / cargo clippy (Windows cross) (pull_request) Successful in 16s
windows / cargo nextest (Windows) (pull_request) Successful in 4m46s
windows / frontend smoke (Windows) (pull_request) Successful in 55s
3a28a68371
A TUI agent waiting on its user has no signal felis can read: it sets
no OSC 133 marks, so `sessions send --wait` never fires. OSC 7501 is the
producer saying so itself. The grid already queues reports, prompt
starts and RIS on the ordered effect queue; this applies them in the
session task and puts the records on every SessionInfo, so `sessions
list --format json`, `sessions info`, the bridge and any roster reader
see them. Support is experimental and draws nothing.

The store holds the spec's 256 records per session, so a producer that
follows the spec is never cut short by felis. The cost is that one
`Listed` frame carries every session's records and can, in principle,
outgrow the frame limit; no real roster comes near it. Rather than
writing a frame the peer must drop, which ends the connection with no
explanation, the daemon now refuses such a reply with the roster's size
and points at `felis sessions info <id>`, which reads one session.

The field joins protocol minor 1: no released peer speaks minor 1, so
no peer can lack the field while having the rest of that minor. It is
sent to every peer, and a minor-0 one skips it as an unknown field.

Two existing paths would lose a record otherwise:

- `replay_effects` returned on the first failed PTY write after taking
  the whole queue, so a `done` written right before exit, behind a
  query reply the dead PTY refused, vanished; the replay now records
  the first write error and keeps applying state effects.
- An `Ops` reply was charged a flat 64 bytes against the subscriber
  backlog, so a peer listing repeatedly while reading slowly could
  queue rosters far past the cap; roster-bearing replies are now
  charged by their contents.

PTY EOF expires transient records after the drive that parses the
child's last output, so the final report survives the expiry. Records
are dropped on an in-place upgrade. The human `sessions info` line
spells direction controls in producer text as `<U+XXXX>`; IPC and JSON
carry the decoded value.

Assisted-by: Claude Code
docs, skills: lead the program-status docs with what a user does
All checks were successful
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 9s
darwin / darwin docs-only no-op (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Successful in 12s
windows / windows docs-only no-op (pull_request) Has been skipped
bench / instruction-count regression gate (pull_request) Successful in 46s
darwin / build felis (aarch64-darwin) (pull_request) Successful in 1m11s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 46s
pr / nix flake check (pull_request) Successful in 27s
windows / cargo nextest (Windows) (pull_request) Successful in 3m30s
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m46s
pr / wire schema is compatible with the base (pull_request) Successful in 3s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 28s
windows / frontend smoke (Windows) (pull_request) Successful in 39s
windows / cargo clippy (Windows cross) (pull_request) Successful in 14s
ba56043c2f
The program-status docs described the wire and the IPC shape but gave a
user no route to the task they come with: finding the session whose
program waits on them. Nothing linked the new pages from the doc map,
the reference opened with IPC terms, and the only polling recipe, in
the product skill, waited on the root record alone, so a program that
exits without a final report or reports under a sub-id held the loop to
its deadline, and the loop said nothing when that happened.

A how-to now covers checking that a program reports, listing blocked
sessions, waiting for one with an exit status per outcome, reporting
from one's own program (with detection), and clearing a stale record.
The reference states the rejection rules as the decoder applies them
(an over-long app discards the report; a value with a byte outside the
value charset is skipped), says that prompt expiry needs shell marks,
and that nothing announces a change. The skill drops producer names
nothing in the repository confirms.

Assisted-by: Claude Code
natsukium deleted branch daemon/osc7501-records 2026-10-12 23:04:40 +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!128
No description provided.