diff --git a/docs/compatible-agents.md b/docs/compatible-agents.md index b10e8f555..9a296fd08 100644 --- a/docs/compatible-agents.md +++ b/docs/compatible-agents.md @@ -90,7 +90,7 @@ Open a **draft pull request** with the entry, fixture, documentation, and verifi - **Must capture before killing the target PTYs**, within the host's bounded teardown. Each host owns the target scope; interrupt every live PTY within it, regardless of recognized command, and exclude exited PTYs. (rationale) - **Must write one `^C` into each target PTY per interrupt call, never signal it.** The shared machine alone decides the second press. Host interrupts must settle within their timeout. (rationale) - **Never finish early on quiet or replace the retry gates with a blanket second press.** Poll until every target yields or the capture budget expires; retry timing and ask detection live in the module's comments. (rationale) -- **Must scan only bytes received after the mark taken before the first interrupt.** Never widen the scan into earlier output; buffer eviction may discard fresh bytes but must not promote stale bytes into the scan. (rationale) +- **Must scan only output received after the mark taken before the first interrupt.** Never widen the scan into earlier output; buffer eviction may discard fresh output but must not promote stale output into the scan. (rationale) - **Must report each detected command immediately**, retaining earlier detections if a later target times out. Source of truth: `captureAgentRecovery` / `RecoveryHost` in `lib/src/host/recovery-capture.ts`; pinned by `lib/src/host/recovery-capture.test.ts`. @@ -102,7 +102,7 @@ Source of truth: `captureAgentRecovery` / `RecoveryHost` in `lib/src/host/recove - **Must rebuild only a known invocation plus an opaque id.** The command is *rebuilt* as label, space, captured id, never sliced from the buffer, keeping the hint's executable alias; a long option's id may follow a space or `=`, and only Claude's legacy `claude --continue` omits it. The id must begin with an ASCII alphanumeric and contain only ASCII alphanumerics, hyphens, and underscores. The invocation must end on a word break but nothing stronger (rationale). - **Must observe a separator after the newest invocation before capturing it.** Buffer end is insufficient, even at the capture deadline; never fall back to an older hint while the newest is unterminated. Pinned by `waits through every ID split` in `lib/src/host/recovery-capture.test.ts` (rationale). - **Must strip the scan window as a whole, in one pass, with an unterminated control swallowing the rest of it** — the string controls (OSC, DCS, SOS, PM, APC) **in either introducer form, `ESC` or bare C1**, and equally a CSI the window was cut off *inside* (rationale). **Must match every escape by its full ECMA-48 shape**, never by the Fe range (rationale). **Must share one implementation**: `stripTerminalControls` removes string controls by running `TerminalControlStreamFilter`, so the batch and streaming readers cannot disagree. -- **Must strip in boundary mode**: *every complete* control becomes a newline rather than vanishing, except SGR and charset designators, the two classes that neither move the cursor nor erase. **Must discard incomplete trailing presentation controls without creating a boundary** (rationale). +- **Must strip in boundary mode**: complete non-string ESC/CSI sequences and standalone C1 controls become newlines, except SGR and charset designators. ESC and C1 counterparts must produce the same boundary. String controls and their payloads vanish; LF, CR, and TAB remain text boundaries; other C0 controls that do not move or erase text vanish. **Must discard incomplete trailing presentation controls without creating a boundary** (rationale). - **Must select the rightmost match in the last 50 lines**, newest *by position* and never by pattern order (rationale). Source of truth: `CODING_AGENTS` in `lib/src/lib/coding-agents.ts`; `detectResumeCommand` / `normalizeResumeCommand` in `lib/src/lib/resume-patterns.ts`; `stripTerminalControls` in `lib/src/lib/terminal-controls.ts`; pinned by `lib/src/lib/coding-agents.test.ts`, `lib/src/lib/resume-patterns.test.ts`, and `lib/src/lib/terminal-controls.test.ts`. diff --git a/docs/compatible-agents.rationale.md b/docs/compatible-agents.rationale.md index 58d6ba110..f246dba24 100644 --- a/docs/compatible-agents.rationale.md +++ b/docs/compatible-agents.rationale.md @@ -50,7 +50,7 @@ Rows 1–2 are why a blanket second press is wrong; `Press Ctrl-C again` was abs **Why the Fe range is not enough to match an escape.** `ESC 7` / `ESC 8` and `ESC c` have final bytes outside it, so a matcher keyed on the introducer alone strips the ESC and leaks the final byte into the text. -**Why boundary-mode stripping inverts the rule.** Observed in the wild: a stored `claude --resume codex`. Deleting controls instead of replacing them with a newline welded two fragments never adjacent on screen into one id-shaped token, which then passed the id grammar. Erasures count too — `\x1b[2K` means the text before it on that line is gone — while SGR and charset designators are the only classes where the text either side really is contiguous. +**Why boundary-mode stripping inverts the rule.** Observed in the wild: a stored `claude --resume codex`. Deleting controls instead of replacing them with a newline welded two fragments never adjacent on screen into one id-shaped token, which then passed the id grammar. Erasures count too — `\x1b[2K` means the text before it on that line is gone. Among the non-string ESC/CSI presentation sequences, SGR and charset designators leave the surrounding text contiguous. String payloads are removed by the earlier framing pass; this policy does not imply that every string protocol leaves the cursor unchanged. ## Recovery record diff --git a/docs/specs/alert.md b/docs/specs/alert.md index 4b6b75172..85a67cd49 100644 --- a/docs/specs/alert.md +++ b/docs/specs/alert.md @@ -155,7 +155,7 @@ An **await** parks on one Session until it finishes what it is doing, then repor **An await crosses from the renderer to the host process that holds the manager**, and the wait itself never leaves the host: -- The renderer asks to park (`await`, under an `awaitId` its client mints) and, if it gives up, to cancel (`awaitCancel`); **the host answers exactly one `alert:awaitResult {awaitId, outcome}` per await, to the realm that parked it**, a cancel included. **A realm's repeated `awaitId` is ignored, never answered twice**; a malformed `id` or `until` is answered `cancelled`. +- The renderer asks to park (`await`, under an `awaitId` its client mints) and, if it gives up, to cancel (`awaitCancel`); **the host answers exactly one `alert:awaitResult {awaitId, outcome}` per await, to the realm that parked it**, a cancel included. **An `awaitId` already parked in that realm is ignored**; a malformed `id` or `until` is answered `cancelled`. - **A realm that ends — a disposed or recreated webview, a reloaded or closed window — has everything it parked cancelled and answered by the host, *synchronously*** (rationale); a disposing adapter settles its own. - `cancelled` has no wire outcome of its own: the renderer reports it to `dor` as an error, which is also what forgets the in-flight control request. - The fake adapter runs the same host in process. The Pocket phone adapter has no `dor` and protocol-v1 carries no await, so it settles every request `cancelled` at once. @@ -175,7 +175,7 @@ Source of truth: `awaitCompletion` in `lib/src/lib/alert-manager.ts`; `AlertComm Rules: -- **The key is `commandWatchKey(rawCommandLine)`**: the last command of a list (`&&`, `||`, `;`, `&`, newline) and the first stage of its pipeline, grouping dropped, leading `VAR=value` words and transparent wrappers (`sudo`, `npx`, …) skipped, then argv[0]'s basename minus any launcher suffix (`docs/specs/terminal-state.md`). **Reserved words are grammar, fish's included**: a leading `do`, `then`, `if`, `!`, `and`, `not`, `begin`, … is stripped, a segment a closer (`done`, `fi`, `esac`, `end`) leads is skipped with its redirection or pipe, and a `for` header, a case pattern, and fish's `case` line are dropped, so `for f in *; do make; done` keys on `make` (rationale). **A redirection is never the program or a runner's script**, a separate target skipped with it (rationale). **An unknown wrapper flag stops the skip**, keying the wrapper itself. **A script runner keys as `