Decode OSC 7501 program status reports #105

Merged
natsukium merged 2 commits from vt/osc7501-decoder into main 2026-10-12 14:12:24 +09:00
Owner

First step of #67: a decode-only OSC 7501 (Program Status Protocol) decoder in felis-vt. Nothing calls it yet, so there is no behavior change and no protocol doc. The support-matrix row and the reference/explanation twins land with the daemon records, when support becomes observable.

What it does

felis_vt::program_status::parse_osc7501(body, Terminator) turns one sequence into a Query, a Report, a Clear { id }, or a Dropped(reason). It implements spec revision 0.4 (2026-10-10). The issue was written against 0.3; 0.4 only adds optional states=/kinds= to the ? reply and removes the terminfo section, so the decoder is unaffected.

  • Atomic: every pair is checked before anything is built, including each occurrence of a repeated key. Otherwise an over-limit app followed by a valid one would pass.
  • Bad id: a malformed or over-limit id drops the report instead of falling back to the root record.
  • Invalid text: text that decodes from base64 but is not UTF-8 drops the report, as does text with a control character.
  • Closed vocabulary: State and Kind are closed enums here; IPC will carry them as open strings. A new state from a later revision fails to compile until the daemon decides its lifetime.

Not in this PR

The parser dispatches an OSC on any ESC, not only on ST, so ESC ] 7501;state=done ESC [0m would look complete. The decoder takes a terminator with no "interrupted" value. A separate felis-vt parser PR will distinguish ST from an interrupting ESC before the grid wiring calls this decoder.

Tests

  • Unit: one test per decoder rule, every limit at max and max+1, the spec's examples, control-character edges, and earlier-bad-duplicate drops.
  • Property tests:
    • totality on arbitrary input;
    • round-trip of valid reports through any spelling (permuted pairs, padded or unpadded base64, extra whitespace), using a test-local base64 encoder;
    • pairs the spec says to ignore change nothing;
    • one poisoned occurrence anywhere drops the report;
    • invariants hold on every accepted report.
  • Fuzz: new program_status target with seeds; 200,000 runs, no panics.
  • cargo-mutants on the module: 94 mutants, 89 caught, 4 unviable, 1 missed. The miss showed that the key-charset check never changed the result, so the check was removed.
First step of #67: a decode-only OSC 7501 (Program Status Protocol) decoder in `felis-vt`. Nothing calls it yet, so there is no behavior change and no protocol doc. The support-matrix row and the reference/explanation twins land with the daemon records, when support becomes observable. ## What it does `felis_vt::program_status::parse_osc7501(body, Terminator)` turns one sequence into a `Query`, a `Report`, a `Clear { id }`, or a `Dropped(reason)`. It implements spec revision 0.4 (2026-10-10). The issue was written against 0.3; 0.4 only adds optional `states=`/`kinds=` to the `?` reply and removes the terminfo section, so the decoder is unaffected. - **Atomic:** every pair is checked before anything is built, including each occurrence of a repeated key. Otherwise an over-limit `app` followed by a valid one would pass. - **Bad `id`:** a malformed or over-limit `id` drops the report instead of falling back to the root record. - **Invalid text:** text that decodes from base64 but is not UTF-8 drops the report, as does text with a control character. - **Closed vocabulary:** `State` and `Kind` are closed enums here; IPC will carry them as open strings. A new state from a later revision fails to compile until the daemon decides its lifetime. ## Not in this PR The parser dispatches an OSC on *any* ESC, not only on ST, so `ESC ] 7501;state=done ESC [0m` would look complete. The decoder takes a terminator with no "interrupted" value. A separate `felis-vt` parser PR will distinguish ST from an interrupting ESC before the grid wiring calls this decoder. ## Tests - **Unit:** one test per decoder rule, every limit at max and max+1, the spec's examples, control-character edges, and earlier-bad-duplicate drops. - **Property tests:** - totality on arbitrary input; - round-trip of valid reports through any spelling (permuted pairs, padded or unpadded base64, extra whitespace), using a test-local base64 encoder; - pairs the spec says to ignore change nothing; - one poisoned occurrence anywhere drops the report; - invariants hold on every accepted report. - **Fuzz:** new `program_status` target with seeds; 200,000 runs, no panics. - **cargo-mutants** on the module: 94 mutants, 89 caught, 4 unviable, 1 missed. The miss showed that the key-charset check never changed the result, so the check was removed.
Programs such as Claude Code, Codex and Terraform report through OSC
7501 whether they are idle, working, blocked on the user, done or
failed. felis has no other reliable way to tell that a TUI in a
session is waiting on the user, since a TUI never emits OSC 133 D.
This decoder is the first layer of experimental support. It turns
one sequence into a query, a report, a clear, or a drop with a reason.
Storing records and their lifetimes stays with the daemon, so the
decoder holds no state.

It implements spec revision 0.4. Every pair is checked before
anything is built, and every occurrence of a repeated key is checked,
not only the last. Otherwise an over-limit `app` followed by a valid
one would pass, while the spec discards any report that breaks a
limit. A malformed `id` drops the report instead of being skipped,
because skipping it would route the report to the root record. Text
that is valid base64 but not UTF-8 drops too. The spec only names
base64 failure and control characters, but storing replacement
characters would keep a record the program never sent.

State and kind are closed enums here although the IPC layer will
carry them as open strings. A new state from a later revision then
fails to compile until the daemon decides that state's lifetime.

The parser still dispatches an OSC on any ESC, not only on ST. The
decoder therefore takes a terminator with no "interrupted" value, and
the caller must not pass it an interrupted body.

Assisted-by: Claude Code
fuzz: cover the OSC 7501 decoder
Some checks failed
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / darwin docs-only no-op (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 8s
darwin / build felis (aarch64-darwin) (pull_request) Successful in 1m10s
windows / windows docs-only no-op (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Successful in 11s
windows / cargo nextest (Windows) (pull_request) Successful in 3m51s
pr / nix flake check (pull_request) Successful in 45s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 2m13s
windows / frontend smoke (Windows) (pull_request) Successful in 5m23s
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
pr / cargo build / clippy / test / deny (pull_request) Successful in 3m49s
pr / wire schema is compatible with the base (pull_request) Successful in 3s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 1m16s
windows / cargo clippy (Windows cross) (pull_request) Successful in 24s
bench / instruction-count regression gate (pull_request) Failing after 21s
718bf1331c
The decoder parses untrusted program output byte by byte under
several hard caps, so a coverage-guided target explores corners that
the proptests only sample. The first input byte picks the terminator
because the sequence-length cap depends on it.

Assisted-by: Claude Code
natsukium force-pushed vt/osc7501-decoder from 718bf1331c
Some checks failed
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / darwin docs-only no-op (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 8s
darwin / build felis (aarch64-darwin) (pull_request) Successful in 1m10s
windows / windows docs-only no-op (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Successful in 11s
windows / cargo nextest (Windows) (pull_request) Successful in 3m51s
pr / nix flake check (pull_request) Successful in 45s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 2m13s
windows / frontend smoke (Windows) (pull_request) Successful in 5m23s
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
pr / cargo build / clippy / test / deny (pull_request) Successful in 3m49s
pr / wire schema is compatible with the base (pull_request) Successful in 3s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 1m16s
windows / cargo clippy (Windows cross) (pull_request) Successful in 24s
bench / instruction-count regression gate (pull_request) Failing after 21s
to 7fdeef30d1
All checks were successful
fuzz / cargo fuzz nightly long-run (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
darwin / detect relevant changes (pull_request) Successful in 8s
darwin / darwin docs-only no-op (pull_request) Has been skipped
darwin / build felis (aarch64-darwin) (pull_request) Successful in 1m9s
bench / instruction-count regression gate (pull_request) Successful in 50s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 2m45s
pr / nix flake check (pull_request) Successful in 1m1s
windows / cargo nextest (Windows) (pull_request) Successful in 6m20s
pr / cargo build / clippy / test / deny (pull_request) Successful in 3m47s
pr / wire schema is compatible with the base (pull_request) Successful in 4s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 1m11s
windows / cargo clippy (Windows cross) (pull_request) Successful in 34s
windows / frontend smoke (Windows) (pull_request) Successful in 1m21s
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
2026-10-12 13:33:26 +09:00
Compare
natsukium deleted branch vt/osc7501-decoder 2026-10-12 14:12:24 +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!105
No description provided.