Queue OSC 7501 reports and prompt starts as ordered grid effects #115

Merged
natsukium merged 3 commits from grid/osc7501-effects into main 2026-10-12 18:42:24 +09:00
Owner

Third step of #67. The grid now turns OSC 7501 into typed effects, kept in byte-stream order, for the daemon to apply. The grid itself stores no records. The daemon ignores the new effects for now, so nothing changes for users yet.

What changes

  • PtyEffect::ProgramStatus(Report | Clear) is queued for each OSC 7501 report or clear that BEL or ST actually terminated.
    • A sequence interrupted by ESC or CAN/SUB queues nothing; this relies on OscEnd from #110.
    • So does a report the decoder drops.
    • ? goes unanswered for now. A reply advertises support, and that should wait until the records are visible over IPC.
  • PtyEffect::PromptStarted is queued for an OSC 133 A that is not k=s, on either screen. The daemon will expire working and blocked records on it.
  • RIS stays the existing HardReset; its doc now says it also ends the records.
  • Everything goes through the one ordered queue, so the daemon sees report working; OSC 133 A differently from OSC 133 A; report working.

Bounded without drops

Reports and prompt starts must never be dropped: the daemon's records have to match what the program sent. Instead, the queue is bounded by yielding:

  • After 64 undrained effects of these two kinds (STATUS_OUTBOX_YIELD), the grid asks the parser to yield.
  • advance_until_yield stops, and the daemon's parse thread waits until the session task has drained. This is the same mechanism a full APC queue uses.
  • It keeps the queue bounded even during an upgrade's settle hold, when the task drains only on request.

To support this, the first commit widens advance_until_yield so it also checks for a yield after any byte that can end an OSC. That includes the byte after an ESC that left an OSC, since that byte is where an ST-terminated OSC now dispatches. advance itself is unchanged.

Also:

  • The budget is recounted from the queue when a state dump is restored, like the APC budget.
  • Report is boxed inside the effect, so the other effect variants stay small.

Tests

  • vt: advance_until_yield stops after an OSC ended by BEL, by ST, or by ST split across chunks. It also stops on the byte that both interrupts one OSC and opens the next. It never checks for a yield after bytes outside a string.
  • grid, effects:
    • each terminator, a clear, an interrupted report, and dropped reports
    • ? gets no reply
    • the 4096-byte cap counts the terminator that was actually seen: the same body is accepted with BEL and is one byte over with ST
    • OSC 133 A: k=s and B/C/D queue nothing, while an A on the alternate screen still counts
    • a report before RIS stays ahead of HardReset
    • the existing ordering test now includes both new effects
  • grid, yield:
    • the 64th effect stops the parse right after its terminator, and 63 do not
    • draining refills the budget
    • prompts and reports share the budget
    • interrupted reports never spend it
  • grid, proptests:
    • arbitrary payloads after 7501;
    • a parse that yields and drains whenever the grid asks, under random chunking, delivers every report in order, with at most 64 per drain
  • grid, state dump: a queue one effect short of the budget survives a dump and restore, and the next report yields. The dump proptest's input fragments now include 7501 and OSC 133 A sequences.
  • daemon: build_sink fed 192 reports pauses three times and delivers every report.
  • Mutation testing (cargo-mutants on the diff): 26 caught, 2 unviable, 1 timeout. The timeout comes from run → Some(0), which loops forever.
  • just check passes.
Third step of #67. The grid now turns OSC 7501 into typed effects, kept in byte-stream order, for the daemon to apply. The grid itself stores no records. The daemon ignores the new effects for now, so nothing changes for users yet. ## What changes - **`PtyEffect::ProgramStatus(Report | Clear)`** is queued for each OSC 7501 report or clear that BEL or ST actually terminated. - A sequence interrupted by ESC or CAN/SUB queues nothing; this relies on `OscEnd` from #110. - So does a report the decoder drops. - `?` goes unanswered for now. A reply advertises support, and that should wait until the records are visible over IPC. - **`PtyEffect::PromptStarted`** is queued for an OSC 133 `A` that is not `k=s`, on either screen. The daemon will expire working and blocked records on it. - **RIS** stays the existing `HardReset`; its doc now says it also ends the records. - Everything goes through the one ordered queue, so the daemon sees `report working; OSC 133 A` differently from `OSC 133 A; report working`. ## Bounded without drops Reports and prompt starts must never be dropped: the daemon's records have to match what the program sent. Instead, the queue is bounded by yielding: - After 64 undrained effects of these two kinds (`STATUS_OUTBOX_YIELD`), the grid asks the parser to yield. - `advance_until_yield` stops, and the daemon's parse thread waits until the session task has drained. This is the same mechanism a full APC queue uses. - It keeps the queue bounded even during an upgrade's settle hold, when the task drains only on request. To support this, the first commit widens `advance_until_yield` so it also checks for a yield after any byte that can end an OSC. That includes the byte after an ESC that left an OSC, since that byte is where an ST-terminated OSC now dispatches. `advance` itself is unchanged. Also: - The budget is recounted from the queue when a state dump is restored, like the APC budget. - `Report` is boxed inside the effect, so the other effect variants stay small. ## Tests - **vt:** `advance_until_yield` stops after an OSC ended by BEL, by ST, or by ST split across chunks. It also stops on the byte that both interrupts one OSC and opens the next. It never checks for a yield after bytes outside a string. - **grid, effects:** - each terminator, a clear, an interrupted report, and dropped reports - `?` gets no reply - the 4096-byte cap counts the terminator that was actually seen: the same body is accepted with BEL and is one byte over with ST - OSC 133 `A`: `k=s` and B/C/D queue nothing, while an `A` on the alternate screen still counts - a report before RIS stays ahead of `HardReset` - the existing ordering test now includes both new effects - **grid, yield:** - the 64th effect stops the parse right after its terminator, and 63 do not - draining refills the budget - prompts and reports share the budget - interrupted reports never spend it - **grid, proptests:** - arbitrary payloads after `7501;` - a parse that yields and drains whenever the grid asks, under random chunking, delivers every report in order, with at most 64 per drain - **grid, state dump:** a queue one effect short of the budget survives a dump and restore, and the next report yields. The dump proptest's input fragments now include 7501 and OSC 133 `A` sequences. - **daemon:** `build_sink` fed 192 reports pauses three times and delivers every report. - **Mutation testing** (cargo-mutants on the diff): 26 caught, 2 unviable, 1 timeout. The timeout comes from `run` → `Some(0)`, which loops forever. - `just check` passes.
A sink could stop the parse only after an APC, the one dispatch whose
effects had a budget. OSC 7501 program status reports become effects
the daemon must apply in order and never drop, so the grid needs the
same lever: stop after the report that fills its budget and let the
daemon drain before parsing on.

The yield check now runs after any byte that may end an APC or OSC
string, including the byte after an ESC that left an OSC, since that
byte is where an ST-terminated OSC dispatches. advance stays unchanged.

Assisted-by: Claude Code
grid: queue OSC 7501 reports and prompt starts as ordered effects
Some checks failed
darwin / detect relevant changes (pull_request) Successful in 8s
darwin / darwin docs-only no-op (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / build felis (aarch64-darwin) (pull_request) Successful in 1m8s
windows / detect relevant changes (pull_request) Successful in 27s
windows / windows docs-only no-op (pull_request) Has been skipped
windows / cargo nextest (Windows) (pull_request) Successful in 6m4s
windows / frontend smoke (Windows) (pull_request) Successful in 1m39s
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
bench / instruction-count regression gate (pull_request) Failing after 1m35s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 3m19s
pr / nix flake check (pull_request) Successful in 1m10s
pr / cargo build / clippy / test / deny (pull_request) Successful in 3m46s
pr / wire schema is compatible with the base (pull_request) Successful in 4s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 54s
windows / cargo clippy (Windows cross) (pull_request) Successful in 36s
861cc426d7
OSC 7501 program status records live in the daemon, not the grid, and
their lifetime depends on what else the program wrote: a report sent
before a shell prompt expires with it, one sent after survives, and RIS
ends them all. The grid therefore hands the daemon facts in byte-stream
order through the one queue that already keeps it: a decoded report or
clear as `ProgramStatus`, a primary OSC 133 `A` on either screen as
`PromptStarted`, and RIS as the existing `HardReset`.

Only a sequence BEL or ST actually ended becomes a report. `?` stays
unanswered for now: a reply advertises support, which should wait until
the records are observable over IPC. The daemon ignores both new
effects until it keeps the records.

Reports and prompt starts are never dropped, since the daemon's records
must match what the program sent, so the queue is bounded by yielding
instead: after 64 undrained effects of the two kinds the parse stops
until the session task drains, the same lever a full APC queue pulls. That keeps the queue bounded
even while an upgrade's settle hold drains only on request. The budget
is recounted from the queue on restore, like the APC one.

Assisted-by: Claude Code
grid: queue a prompt start only when a transient status report is owed
All checks were successful
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
windows / detect relevant changes (pull_request) Successful in 9s
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
windows / windows docs-only no-op (pull_request) Has been skipped
windows / cargo nextest (Windows) (pull_request) Successful in 4m34s
windows / frontend smoke (Windows) (pull_request) Successful in 45s
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m47s
pr / wire schema is compatible with the base (pull_request) Successful in 3s
bench / instruction-count regression gate (pull_request) Successful in 1m1s
pr / nix flake check (pull_request) Successful in 37s
pr / cargo build / clippy / test / deny (pull_request) Successful in 2m17s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 38s
windows / cargo clippy (Windows cross) (pull_request) Successful in 16s
18a02f652e
Every primary OSC 133 A queued a PromptStarted effect so the daemon
could expire working and blocked program-status records. Shells that
never speak OSC 7501 paid for it on every prompt: the bench gate
measured shell_prompt at +4.6% (daemon_side) and +10.7%
(multi_cycle_end_to_end), past its +2% limit.

A prompt start expires only working and blocked records, and after it
none are left until the next working or blocked report. So the grid
now arms a flag on such a report and a prompt start queues the effect
only while armed, which brings the shell_prompt benches back to the
base.

The flag is exact rather than a hint, but erring toward a push would
be harmless: an expiry with nothing transient changes nothing. RIS
rebuilds the grid and so disarms it, matching the daemon's reset of
every record. Old state dumps decode it as false, which is right
because records do not cross an upgrade.

Assisted-by: Claude Code
natsukium deleted branch grid/osc7501-effects 2026-10-12 18:42: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!115
No description provided.