Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ breaking changes may land in a minor release.
writes became atomic. Their writers now refuse with the `PermissionError` a plain
`write_text` used to raise. Machine-minted state (run archives, stop requests, the
config-digest stamp) is unaffected.
- **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.

### Fixed

Expand Down
50 changes: 38 additions & 12 deletions docs/adapter-authoring-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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
Expand All @@ -177,6 +174,35 @@ 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 — 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
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.
Expand Down
9 changes: 7 additions & 2 deletions src/bmad_loop/adapters/multiplexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down