notify: two banners for one event, and neither said which pane - #153
Merged
Conversation
Follow-up to #152, which fixed the attribution and left this on the table because it changes a contract rather than a bug. A pane's OSC 9 had two emitters. `PtyPane::queue_host_notify` relayed the body verbatim, unconditionally, per SPEC-parity P2(b). `App::on_pty_output` raised roost's own, `{display_name}: {body}`, skipped for the focused pane. They were different *surfaces* — one reached the terminal, the other forked `osascript` — which is the only reason nobody noticed they were two notifications for one event. #152 deleted the fork, and the overlap became two near-identical banners. Measured on the real binary before this change, one `printf '\e]9;NEEDS-YOU\a'` in an unfocused pane: \x1b]9;shell · tmp: NEEDS-YOU\x07\x1b]9;NEEDS-YOU\x07 The named one wins. With eight panes open, a banner reading "Claude needs your permission to use Bash" that cannot say *which* pane is the exact pain DESIGN.md's "not knowing who needs me" exists to solve — and it is the only one of the two that stays quiet about the pane you are already looking at. The relay is retired: `queue_host_notify` and `host_notify_bytes` are deleted, along with the two unit tests that pinned a builder nothing calls. So P2's (b) is now delivered by (a): one OSC 9 per notification, body `{display_name}: {body}`, suppressed for the focused pane, bounded and C0-stripped by the same `sanitize_for_host` + `HOST_NOTIFY_CAP`, rate-limited per pane by `last_desktop_notify` and process-wide by `infra::notify`'s budget. Same measurement after: \x1b]9;shell · tmp: NEEDS-YOU\x07 `infra::notify` stops writing to stdout from wherever the event was noticed and hands bytes to `App::notify_host` instead, so roost's own notifications go out on the same `host_out` queue as P2's relays and P6's title — placed between frames like everything else roost writes to the host (W3), rather than mid-loop. The e2e gate had to change, and how it changed is worth reading: it drove a **single** pane, which is now the one case that deliberately emits nothing. It now splits first and notifies the unfocused pane, and asserts the body *ends with* the pane's own text and is *not equal* to it — the shape, not the naming, which is U2/C4's contract and pinned there. It also stopped being able to send `Alt+n` right after `settle`. `settle` proves roost *drew*; roost draws before its control socket is up, and a keystroke landing in that window can still be eaten by the startup keyboard probe (`KBD_PROBE_BUDGET`). The test now waits for the control plane and polls for the split, which is a real "roost has started" signal rather than a frame-shaped guess — worth knowing for any harness test that types early.
navbytes
force-pushed
the
notify/one-named-banner
branch
from
August 21, 2026 06:37
94dc021 to
8e0a8a7
Compare
navbytes
added a commit
that referenced
this pull request
Aug 21, 2026
…e request (#155) v0.1.10 shipped at 01:48 UTC today, before any of this session's work landed, so everything below is unreleased. Touching `.github/release-request` dispatches Release, which builds four targets, creates the `v0.1.11` tag from Cargo.toml itself, and publishes with SHA256SUMS.txt. **Two silent data-loss bugs.** - `Alt+w` on the last pane — whose own prompt calls it "quit roost" — removed the pane and *then* quit, so `shutdown` saved a tab whose layout named a pane its map no longer had. The repair minted a blank `shell` spec on next launch, and the session id, cwd, title and note of the pane you quit on were gone. `Alt+q` on the same pane kept all of them. (#147) - A recycled pane id inherited the dead pane's session-detection clock. `last_shell_seen` and `pending_detect` were the only two `PaneId`-keyed maps `close_pane_id` did not prune, so a pane taking that id could scan hours back and resume a stranger's conversation, permanently. (#141) **Idle cost down ~13x.** 2.75% CPU and 888 B/s of terminal traffic, to 0.2–0.4% and ~62 B/s, flat in the pane count at every step because it was all fixed per-frame chrome: a `String` allocated per cell per frame (#140); the quick-launch picker's `$PATH` probe running ~1,500 `stat(2)` a second with no dialog on screen, plus the whole help table, every frame (#144); an unconditional 30 fps repaint (#146); and 12 fps even with nothing animating (#149). **Desktop notifications changed channel, and this one is user-visible.** They used to fork `osascript`, which macOS attributes to **Script Editor** — there is no flag for that; a CLI cannot post under its own name. roost now asks the terminal instead, with a bell and an `OSC 9`, which is the mechanism SPEC-parity P2 already named. Ghostty, iTerm2, WezTerm and kitty raise a real notification from it, attributed to themselves, and it survives ssh. **A host that ignores OSC 9 (Apple Terminal, Alacritty) now gets only the bell.** (#152) And a pane's notification no longer arrives twice: there were two emitters, invisible as a duplicate only while one of them was the `osascript` fork. The named one won, so one banner, saying which pane, and silence for the pane you are already looking at. (#153) **Three new fuzzers** over input surfaces nothing reached before — the mouse router, the key router, and paste — each walking a layout being reshaped underneath it, drawing a real frame and re-checking the layout fuzzer's own invariants after every step. The mouse one found #147 on seed 15. (#147, #148) **rustfmt and clippy, enforced in CI** (#150), tuned to the style the tree already had rather than imposed: the config was picked by measuring churn (556 hunks vs 1,632 for the naive setting). The lint set is in `Cargo.toml`'s `[lints]` tables so it binds locally too, and three candidate lints were measured and rejected. It found ten `unsafe` blocks missing the `SAFETY:` comment every other one in the tree has. Plus: the reformat kept out of `git blame` (#151), and three test gates that blamed roost for the machine being out of ptys (#154). Verified before cutting: 1,048 tests, clippy clean at `-D warnings`, `fmt --check` clean. Both agent adapters checked end to end against live binaries (`pi --session …`, `claude --resume …`), the session-resolution table across all five adapters, the 45s status decays timed against the clock, and two 15-minute soaks (~78k random keystrokes each, ~1,800 panes) with no orphan, no leak and no unreadable workspace. Note for after the merge: `HOMEBREW_TAP_TOKEN` is still unset, so release.yml will skip the tap sync again (it did for v0.1.10). The formula needs the usual hand-sync.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #152, which fixed the attribution and left this on the
table because it changes a contract rather than a bug.
A pane's OSC 9 had two emitters.
PtyPane::queue_host_notifyrelayedthe body verbatim, unconditionally, per SPEC-parity P2(b).
App::on_pty_outputraised roost's own,{display_name}: {body},skipped for the focused pane. They were different surfaces — one
reached the terminal, the other forked
osascript— which is the onlyreason nobody noticed they were two notifications for one event. #152
deleted the fork, and the overlap became two near-identical banners.
Measured on the real binary before this change, one
printf '\e]9;NEEDS-YOU\a'in an unfocused pane:The named one wins. With eight panes open, a banner reading "Claude
needs your permission to use Bash" that cannot say which pane is the
exact pain DESIGN.md's "not knowing who needs me" exists to solve — and
it is the only one of the two that stays quiet about the pane you are
already looking at. The relay is retired:
queue_host_notifyandhost_notify_bytesare deleted, along with the two unit tests thatpinned a builder nothing calls.
So P2's (b) is now delivered by (a): one OSC 9 per notification, body
{display_name}: {body}, suppressed for the focused pane, bounded andC0-stripped by the same
sanitize_for_host+HOST_NOTIFY_CAP,rate-limited per pane by
last_desktop_notifyand process-wide byinfra::notify's budget. Same measurement after:infra::notifystops writing to stdout from wherever the event wasnoticed and hands bytes to
App::notify_hostinstead, so roost's ownnotifications go out on the same
host_outqueue as P2's relays andP6's title — placed between frames like everything else roost writes to
the host (W3), rather than mid-loop.
The e2e gate had to change, and how it changed is worth reading: it
drove a single pane, which is now the one case that deliberately
emits nothing. It now splits first and notifies the unfocused pane, and
asserts the body ends with the pane's own text and is not equal to
it — the shape, not the naming, which is U2/C4's contract and pinned
there.
It also stopped being able to send
Alt+nright aftersettle.settleproves roost drew; roost draws before its control socket isup, and a keystroke landing in that window can still be eaten by the
startup keyboard probe (
KBD_PROBE_BUDGET). The test now waits for thecontrol plane and polls for the split, which is a real "roost has
started" signal rather than a frame-shaped guess — worth knowing for
any harness test that types early.