From b4d0768de529c38ee054094e8511895789836a30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davor=20Raci=C4=87?= Date: Mon, 24 Aug 2026 20:59:10 +0200 Subject: [PATCH 1/2] docs(adapters): state the qualified-id obligation once at the seam MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `session:@N` at the minting and listing seams — but the rule a backend author needs in order to do that safely was only inferable from two backend-specific docstrings and a parenthetical. The authoring guide now states the general obligation: an id minted in anything but the bare native form binds every seam core compares it against to the identical form, and every verb it is replayed through to accepting it. Where a verb cannot take that form, the backend translates inside the verb rather than exempting the id. The worked example is the live one — psmux's option trio routing a qualified target through `_option_scope` and refusing a bare id rather than guessing a server. `select_window` carried such a translation until the 3.3.8 floor made the server resolve a scoped id itself (psmux/psmux#497), so it survives as a past-tense aside rather than as documentation of code that is no longer in the tree. Also names the third quiet failure the rule prevents, which the issue did not: a `list_windows`/`list_window_ids` split reports every kill candidate as verifiably gone, survivors included (#435). `TerminalMultiplexer.new_parked_window` now says its id is opaque and MAY be qualified, matching what `new_window` already said. Documentation only; no behavior change, no test changes. Closes #311 --- CHANGELOG.md | 10 ++++++ docs/adapter-authoring-guide.md | 46 ++++++++++++++++++++------- src/bmad_loop/adapters/multiplexer.py | 9 ++++-- 3 files changed, 51 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4241bc85..5db04fd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ breaking changes may land in a minor release. ## [Unreleased] +### Changed + +- **Document the qualified-id obligation once for adapter authors (#311).** + `docs/adapter-authoring-guide.md` states the general rule a native-id backend must follow — + an id minted in anything but the bare native form binds every seam core compares it against to + the identical form, and every verb it is replayed through to accepting it — instead of leaving + it to be inferred from psmux's per-seam specifics, which shrink to the instances they are. + `TerminalMultiplexer.new_parked_window` now says its id is opaque and MAY be qualified, matching + `new_window`. Documentation only; no behavior change. + ## [0.11.1] — 2026-08-23 ### Added diff --git a/docs/adapter-authoring-guide.md b/docs/adapter-authoring-guide.md index 1b35a5b3..5e0e929c 100644 --- a/docs/adapter-authoring-guide.md +++ b/docs/adapter-authoring-guide.md @@ -155,20 +155,17 @@ module-level `parse_target()` — or the backend's own **native id** (whatever y `target()`, precisely so the seam grammar never has to carry a pane or window id: the parked-window return target — `current_return_target`, above — and the native window id, which psmux qualifies to `session:@N` because its ids are -per-server (falling back to the bare id where that grammar would not survive — -the backend owns those conditions, and applies them uniformly, so the -`new_window`/`list_window_ids` symmetry rule holds under the fallback too). +per-server (falling back to the bare id where that grammar would not survive). Both are replayed opaquely; neither is parsed by core. psmux applies the same qualification to `new_parked_window`, the `window_id` columns of `list_windows` -and `current_window_id`; the latter two must agree, since the ctl-window prune -compares them to skip its own window. To preserve unambiguous lookup, -`new_parked_window` must agree with the `list_windows` column too; a backend that -qualifies one side only remains usable but falls back to resolving parked -windows by name, which is ambiguous whenever several kinds share a run id -(#482). tmux consumes the token natively (it coincides with tmux exact-match -syntax), so `BaseTmuxBackend` passes it straight through. A native-id backend -calls `parse_target()` first — `None` means "already a native id, use as-is", -otherwise resolve `(session, window)` yourself; the herdr adapter's +and `current_window_id`. To preserve unambiguous lookup, `new_parked_window` must +agree with the `list_windows` column; a backend that qualifies one side only +remains usable but falls back to resolving parked windows by name, which is +ambiguous whenever several kinds share a run id (#482). tmux consumes the token +natively (it coincides with tmux exact-match syntax), so `BaseTmuxBackend` passes +it straight through. A native-id backend calls `parse_target()` first — `None` +means "already a native id, use as-is", otherwise resolve `(session, window)` +yourself; the herdr adapter's `_parse_target` ([backend.py](https://github.com/pbean/bmad-loop-adapter-herdr/blob/main/src/bmad_loop_adapter_herdr/backend.py)) is the worked example (workspace-by-label → tab-by-name → root pane, resolved @@ -177,6 +174,31 @@ stay a stable _by-name_ reference: core formats targets ahead of use (a parked window's return target, for one), so eager resolution to a live id goes stale — inheriting the default and resolving lazily is almost always right. +**The qualified-id obligation.** The psmux qualifications above are instances of +one rule, and it is the rule — not the instances — a native-id backend needs: +**if an id-minting seam returns anything other than a bare native id, every seam +whose output core compares that id against must emit the identical form, and +every verb the id is replayed through must accept it.** Where a verb cannot take +that form — or cannot take a window scope at all — the backend translates inside +that verb rather than exempting the id from qualification: psmux has no +per-window _user_ options to write to (#310), so its option trio runs the +qualified target through `_option_scope` and writes a session-scoped key carrying +the window's id, refusing a bare id rather than guessing a server (the built-ins +pass straight through to the base). It carried a second such translation on +`select_window` until its 3.3.8 floor made the server resolve a scoped id itself +(psmux/psmux#497). Where the composed grammar cannot carry the value at all, the +backend degrades uniformly across every seam that mints or lists the id, so the +pairings still line up under the fallback. Those pairings are not a closed set: a +new one appears whenever a caller compares two id-producing seams to each other, +so treat the rule as the contract and each documented pairing as an instance of +it. Every way of getting it wrong is quiet: a mint/list split reads every live +window as instantly dead; a `list_windows`/`current_window_id` split makes the +ctl-window prune kill the window it is running in (#291); a +`list_windows`/`list_window_ids` split reports every kill candidate as verifiably +gone, survivors included (#435). And a verb handed a form it rejects fails or +silently no-ops — the seam is best-effort or its caller swallows the raise, so no +error reaches core either way. + Operations that can race a window dying (`pipe_pane`) or a session already being gone (`kill_session`) must tolerate it rather than raise; everything else raises a `MultiplexerError` subclass on failure, which call sites catch at the seam (e.g. diff --git a/src/bmad_loop/adapters/multiplexer.py b/src/bmad_loop/adapters/multiplexer.py index f158eb8b..b57dbc75 100644 --- a/src/bmad_loop/adapters/multiplexer.py +++ b/src/bmad_loop/adapters/multiplexer.py @@ -171,8 +171,13 @@ def new_parked_window( """Create a window that runs ``argv`` then *parks* — waiting on a key so the exit status stays inspectable instead of the window closing the moment the process exits — and finally returns an attached client to its origin - (keyed by the per-window ``return_opt``). Returns the native window id; - for its required form see :meth:`list_window_ids`'s note on #482.""" + (keyed by the per-window ``return_opt``). Returns the native window id. + + That id is **opaque to core** exactly as :meth:`new_window`'s is, so a + backend MAY return an already-qualified target rather than a bare id + (psmux returns ``session:@N``) — and an obligation follows from that + choice here too, a different pairing than :meth:`new_window`'s: for the + form it binds the id to, see :meth:`list_window_ids`'s note on #482.""" @abstractmethod def list_window_ids(self, session: str) -> list[str]: From 406b9a08b774a487821bd1ec9ff5d827e9d065d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davor=20Raci=C4=87?= Date: Mon, 24 Aug 2026 21:21:02 +0200 Subject: [PATCH 2/2] docs(adapters): address review on the qualified-id obligation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three review findings, all documentation: - Name the bare-id fallback as lossy. The general rule said the degrade keeps every pairing consistent, which is true but reads as a free escape hatch; a bare psmux id routes by the caller's server, so the degrade condition has to stay the narrow one the grammar forces. - Scope the quiet-failure claim to id-shape faults. A transport failure is a different thing and list_window_ids must still raise it — the ABC is explicit that an empty list must not be conflated with "couldn't ask". - Condense the changelog entry to a release-note summary. --- CHANGELOG.md | 11 ++++------- docs/adapter-authoring-guide.md | 12 ++++++++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5db04fd7..18649738 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,13 +9,10 @@ breaking changes may land in a minor release. ### Changed -- **Document the qualified-id obligation once for adapter authors (#311).** - `docs/adapter-authoring-guide.md` states the general rule a native-id backend must follow — - an id minted in anything but the bare native form binds every seam core compares it against to - the identical form, and every verb it is replayed through to accepting it — instead of leaving - it to be inferred from psmux's per-seam specifics, which shrink to the instances they are. - `TerminalMultiplexer.new_parked_window` now says its id is opaque and MAY be qualified, matching - `new_window`. Documentation only; no behavior change. +- **Document the qualified-id obligation once for adapter authors (#311).** The authoring guide + states the rule a native-id backend must follow rather than leaving it to be inferred from + psmux's per-seam specifics, and `TerminalMultiplexer.new_parked_window` now says its id is + opaque and MAY be qualified, matching `new_window`. Documentation only; no behavior change. ## [0.11.1] — 2026-08-23 diff --git a/docs/adapter-authoring-guide.md b/docs/adapter-authoring-guide.md index 5e0e929c..86637843 100644 --- a/docs/adapter-authoring-guide.md +++ b/docs/adapter-authoring-guide.md @@ -188,10 +188,14 @@ pass straight through to the base). It carried a second such translation on `select_window` until its 3.3.8 floor made the server resolve a scoped id itself (psmux/psmux#497). Where the composed grammar cannot carry the value at all, the backend degrades uniformly across every seam that mints or lists the id, so the -pairings still line up under the fallback. Those pairings are not a closed set: a -new one appears whenever a caller compares two id-producing seams to each other, -so treat the rule as the contract and each documented pairing as an instance of -it. Every way of getting it wrong is quiet: a mint/list split reads every live +pairings still line up under the fallback — but the fallback is lossy, not a free +escape hatch: a bare psmux id routes by the _caller's_ server, so the degrade +condition must stay the narrow one the grammar forces (#221), never a +convenience. Those pairings are not a closed set: a new one appears whenever a +caller compares two id-producing seams to each other, so treat the rule as the +contract and each documented pairing as an instance of it. Every way of getting +the _form_ wrong is quiet — these are id-shape faults, never transport faults, +which `list_window_ids` must still raise: a mint/list split reads every live window as instantly dead; a `list_windows`/`current_window_id` split makes the ctl-window prune kill the window it is running in (#291); a `list_windows`/`list_window_ids` split reports every kill candidate as verifiably