cli-relay 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sharoze Iftikhar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,355 @@
1
+ # cli-relay — persistent CLI router
2
+
3
+ Built by [RevOpsDev](https://revopsdev.com).
4
+
5
+ Resume-by-reference delegation across codex, agy, claude-code (command-code: fresh-only).
6
+ Built 2026-08-16/17, inspired by DeepSeek Harness's subagent architecture but deliberately
7
+ smaller — see design history below before re-deriving any of this from scratch.
8
+
9
+ ## Installation
10
+
11
+ Requires Node.js 18.17 or newer.
12
+
13
+ ```sh
14
+ npm install --global cli-relay
15
+ ```
16
+
17
+ The package has no runtime dependencies. Installation exposes the `cli-relay` command.
18
+
19
+ ## Usage
20
+
21
+ ```
22
+ cli-relay [--dry-run|--print-command] <backend> <thread> <fresh|resume> <prompt...>
23
+ cli-relay list
24
+ cli-relay doctor
25
+ cli-relay reset <thread>
26
+ cli-relay pin <thread> "<fact>"
27
+ cli-relay unpin <thread> <index>
28
+ cli-relay pins <thread>
29
+ ```
30
+
31
+ Backends: `codex`, `agy`, `claude-code`, `command-code` (command-code is fresh-only —
32
+ its resume showed a reproducible seed-turn bug live, see file header).
33
+
34
+ `cli-relay doctor` checks that every backend's binary is actually resolvable on PATH —
35
+ useful after a fresh machine setup or when a backend call fails and you're not sure whether
36
+ it's cli-relay or the backend itself. `--dry-run` / `--print-command` prints the exact argv
37
+ that would be spawned (prompt fully assembled, pins injected) without spawning anything or
38
+ touching the session map at all — it enforces the same refusals a real run would (e.g. won't
39
+ preview a `resume` on an unconfirmed thread, won't preview against a thread with a run
40
+ already in flight), so what it shows is genuinely what would happen, not a best-effort guess.
41
+
42
+ Not named `route` — that collides with the pre-existing BSD `/sbin/route` network tool,
43
+ found the hard way (see Known gaps history below).
44
+
45
+ Ctrl-C mid-run terminates the child cleanly and exits 130 — safe to interrupt, the map never
46
+ gets left in a stuck `status: "running"` state from a live interrupt (only from a hard crash,
47
+ which self-heals via the lock's own staleness check on the next call).
48
+
49
+ ## Design
50
+
51
+ Never replays transcripts. Stores only `{backend, native_session_id, confirmed, status,
52
+ consecutive_resume_failures, ...}` per named thread in `~/.cli-relay/sessions.json`, and asks
53
+ each backend to resume *itself* via its own native flag (`codex exec resume`, `agy
54
+ --conversation`, `claude -r`). All four backends' id/answer extraction is via real
55
+ structured JSON (`--json` / `--output-format json`), live-verified against each real CLI —
56
+ no regex stdout-scraping.
57
+
58
+ Full rationale, the "persistence-by-reference not persistence-by-replay" framing, and why
59
+ Harness's own subagent adapters are one-shot by deliberate design (not oversight) — three
60
+ independent deep-dives (Codex, GLM-5.2 via Command Code, a design critique from Fable)
61
+ converged on this shape. Session content isn't preserved past this repo; the key
62
+ conclusions are captured in this file's own comments and the header.
63
+
64
+ ### Configuration and adapters
65
+
66
+ Runtime settings come from built-in defaults merged with optional overrides in
67
+ `~/.cli-relay/config.json`. Keys may use the exported uppercase names or camelCase, for
68
+ example `SPAWN_TIMEOUT_MS` or `spawnTimeoutMs`. Available settings are defined in
69
+ `src/config.mjs`; derived values such as the lock path and stale-lock window always follow
70
+ their configured base values.
71
+
72
+ Built-in adapters live in `src/adapters/` and are discovered at runtime. Additional `.mjs`,
73
+ `.js`, or `.cjs` adapters can be placed in `~/.cli-relay/adapters/`; an adapter with the same
74
+ `name` as a built-in replaces it. Each adapter provides `fresh`, optional `resume`, `env`,
75
+ `parse`, optional `checkCompaction`, and optional `binaryCandidates` (an ordered list of
76
+ binary names `doctor` tries — most adapters only need one, but a backend that ships under
77
+ more than one binary name can list several). Housekeeping commands are dispatched before
78
+ adapter discovery, so they remain available if an adapter cannot be loaded. A malformed
79
+ adapter file no longer takes down the whole load either way: a broken file matching one of
80
+ the four required backend names (`codex`/`agy`/`claude-code`/`command-code`) is recorded and
81
+ reported by the startup completeness assertion (see below); a broken file under any other,
82
+ custom name is skipped with a `warning:` line to stderr, since it was never required in the
83
+ first place.
84
+
85
+ ### Architecture
86
+
87
+ `cli-relay.mjs` itself is orchestration only (arg parsing, process spawn/lifecycle, signal
88
+ handling, the two critical sections that read-modify-write the session map). Everything else
89
+ lives under `src/`:
90
+
91
+ - `src/config.mjs` — settings (see above).
92
+ - `src/core/lock.mjs` — the POSIX `mkdir`-based lock. Load-bearing and deliberately
93
+ conservative; see the Review history entry below before touching it — it took four rounds
94
+ of adversarial review to get the reclaim logic actually race-free.
95
+ - `src/core/map-store.mjs` — atomic read/write of `~/.cli-relay/sessions.json`.
96
+ - `src/core/pins.mjs`, `src/core/thread-lookup.mjs` — pinned-facts validation/injection, and
97
+ the "did you mean?" suggestion helper used by every command that looks up a thread by name
98
+ (case-insensitive substring match, either direction, up to 3 candidates — not a fuzzy-
99
+ distance algorithm, and it only fires when the thread doesn't exist at all, never when it
100
+ exists but is merely unconfirmed).
101
+ - `src/core/errors.mjs` — `RelayError`, a minimal typed error (`code`, `exitCode`, `cause`)
102
+ used at the hot-path throw sites, handled once in `main().catch()`. Preserves the exact
103
+ original exit codes and message text for every pre-existing error path — including the
104
+ asymmetry where a usage error (exit 2) prints with no `cli-relay error:` prefix while
105
+ everything else (exit 1) does; that split existed before `RelayError` did and is
106
+ intentional, not something to "fix" into consistency.
107
+ - `src/commands/` — `list`/`reset`/`pin`/`unpin`/`pins`/`doctor`, one file each.
108
+ - `src/adapter-loader.mjs` — discovery, validation, and the `assertAdapterRegistry`
109
+ completeness check described above.
110
+ - `src/adapters/` — one file per backend.
111
+
112
+ Six of these pieces (the registry completeness assertion, `doctor`, the `RelayError` model,
113
+ `--dry-run`, did-you-mean, and an audit of thread-identity ambiguity that concluded no change
114
+ was needed) were adapted from patterns found in a much larger sibling project,
115
+ [cli-continues](https://github.com/yigitkonur/cli-continues) — full brief in
116
+ `docs/cli-continues-cherrypick-brief.md`. cli-relay deliberately did not adopt that project's
117
+ actual approach (parsing and replaying each backend's transcript format) — see Design above
118
+ for why.
119
+
120
+ ## Testing
121
+
122
+ `tests/smoke.sh` — real end-to-end regression check against real backends (not mocks).
123
+ Backs up and restores your actual `~/.cli-relay/sessions.json` around the run, safe to run any
124
+ time. Covers: list/reset, fresh→resume context retention (agy), the circuit breaker's actual
125
+ 3-strikes trip (live-fired against codex with a bad id, not just traced), SIGINT mid-run
126
+ cleanup, and SIGINT while genuinely pre-spawn (lock held elsewhere — must abort immediately
127
+ without spawning). 32/32 passing as of the last run. Does not exercise `claude-code` (real
128
+ billing per call) or `command-code` resume (disabled). Also does not yet cover `doctor` or
129
+ `--dry-run` — both are verified manually against the real backends whenever they change (see
130
+ the 2026-09-01 Review history entries below for what that's caught), not by an automated
131
+ case in this file yet.
132
+
133
+ ```
134
+ bash tests/smoke.sh
135
+ ```
136
+
137
+ ## Review history
138
+
139
+ Built, then put through three Fable review passes plus a production-hardening pass:
140
+ - Pass 1: stale-lock-wedges-forever on crash, untracked SIGKILL timer, silent resume-failure
141
+ looked like success (no fail-loud exit code), fresh-on-confirmed-thread silent clobber,
142
+ concurrent-same-thread race, inconsistent JSON parse robustness across adapters.
143
+ - Pass 2 (post-fix verification): caught a regression in the pass-1 fix itself — the unified
144
+ JSON parser stopped at the first *syntactically valid* JSON line rather than the first line
145
+ with the actual expected fields, a new silent-wrong-answer path. Fixed.
146
+ - Pass 3: implemented `cli-relay list` / `cli-relay reset`, plus the 3-consecutive-failures
147
+ circuit breaker (see Design). Verified end to end except the exact 3-strikes trip at the time (agy
148
+ hung on a garbage id instead of failing fast — verified by code trace instead).
149
+ - Production-hardening pass: added SIGINT/SIGTERM handling (previously a Ctrl-C mid-run
150
+ orphaned the child and left the map stuck), found and fixed two early-exit branches that
151
+ bypassed the interrupt exit code in favor of their own failure code. Live-fired the circuit
152
+ breaker's actual trip point against codex (fails fast on a bad id, unlike agy) instead of
153
+ relying on the trace. Wrote `tests/smoke.sh`. Fixed a naming collision (`route` vs
154
+ `/sbin/route`) and moved the repo out of `~/bin` (executables only) into
155
+ `~/Projects/cli-relay` with a clean symlink.
156
+ - Final Fable pass on the signal-handling code itself: caught a real race — the "no active
157
+ child" branch of the signal handler couldn't distinguish "nothing spawned yet" from "child
158
+ just exited, main() is still committing the outcome," and force-exiting in the latter case
159
+ could silently drop a just-earned successful result (e.g. a fresh run's `confirmed: true`
160
+ never gets persisted). Fixed with a `childHasFinished` flag paired atomically with the
161
+ existing `activeChildPgid` tracking, splitting the old two-way branch into three. Also
162
+ threaded a signal-aware exit code (`128 + signal number`, SIGINT vs SIGTERM distinguishable)
163
+ through all three of `main()`'s exit points, and fixed `runChild`'s error handler to fold
164
+ `userInterrupted` into `cancelled` like its exit handler already did. One documented,
165
+ accepted asymmetry remains: `reset`'s own critical section isn't covered by this tracking
166
+ (low risk — `saveMap` is atomic regardless, see the comment at `cmdReset`).
167
+
168
+ **Modularization (2026-08-31).** The single 968-line `cli-relay.mjs` was split into
169
+ `src/{config,core,commands,adapters}/` (see Architecture above). Rather than pick one
170
+ implementation on trust, three independent models built the same refactor from the same
171
+ brief in isolated branches: Codex (gpt-5.6-sol), Command Code on `minimax-m3-free`, and
172
+ Command Code on GLM-5.2. All three independently found and fixed the same 4 real bugs in an
173
+ earlier attempt (a compaction-detection substring check that lost its JSON-quote delimiters
174
+ and risked false positives; a `LOCK_STALE_MS` that wasn't actually derived from
175
+ `SPAWN_TIMEOUT_MS`, so overriding one silently desynced the other; adapter loading that ran
176
+ before housekeeping-command dispatch, so one broken adapter could block `list`/`reset`/`pin`;
177
+ and hardcoded `~/.cli-relay/sessions.json` strings in user-facing messages despite the path being
178
+ configurable) — strong convergent signal those were genuine, not nitpicks. All three passed
179
+ the live smoke suite 32/32. Codex's was merged for being the leanest (395-line `cli-relay.mjs`,
180
+ 946 lines total vs. the other two's 1,219/1,336) with materially identical behavior; the
181
+ other two are preserved on their own branches for the record, not deleted.
182
+
183
+ **Lock hardening (2026-08-31), same day.** A GLM-5.2 second-opinion audit of the merged
184
+ result found two real race conditions: a concurrent `reset` mid-run could crash critical
185
+ section 2 and silently drop a just-confirmed session id (fixed: CS2 now detects a
186
+ mid-run-deleted thread and warns instead of resurrecting it or crashing), and the lock could
187
+ wedge permanently if its holder died in the exact window between `mkdir` and writing
188
+ `holder.json` (fixed: the lock's own directory age, not any single waiter's elapsed wait
189
+ time, decides reclaim eligibility). The fix for the second bug took **four rounds** of
190
+ adversarial codex review before it actually closed — each earlier attempt introduced a
191
+ narrower TOCTOU race in the reclaim logic itself (using a waiter's own deadline instead of
192
+ the lock generation's actual age; a path-based `rmSync` that wasn't atomic against a
193
+ concurrent reclaimer). The final design: atomic `renameSync`-based reclaim, re-verified by
194
+ content after the rename, restoring what it captured if it turns out not to be the same dead
195
+ instance originally judged. Two narrower, more theoretical races remain deliberately
196
+ unfixed — both require a process crash *and* multiple genuinely concurrent invocations
197
+ racing inside a multi-microsecond filesystem operation; closing them fully would mean
198
+ replacing the whole hand-rolled `mkdir`/`rename` scheme with real OS-level advisory locking
199
+ (`flock`/`fcntl`), judged disproportionate for a single-user tool. Documented, not hidden, in
200
+ `src/core/lock.mjs`'s own comments.
201
+
202
+ **cli-continues cherry-pick + hardening (2026-09-01).** A much larger sibling project,
203
+ [cli-continues](https://github.com/yigitkonur/cli-continues) (41k lines, resumes sessions
204
+ across 16 tools by parsing and replaying each one's transcript format), was deep-dive
205
+ audited by two independent models for patterns worth adapting — not its actual
206
+ transcript-replay approach, which cli-relay deliberately avoids. Both converged on the same
207
+ six items (see Architecture above); prior art research (agy, cross-checked via independent
208
+ web search) found no existing tool combining native resume-by-id, a dynamic per-backend
209
+ adapter registry, and zero-dependency `mkdir` locking the way cli-relay does — the closest
210
+ relative solves the same problem by transcript replay instead. Codex and GLM-5.2 each
211
+ implemented all six from the same brief in isolated branches; GLM's had three real gaps
212
+ codex's didn't (a one-directional did-you-mean substring match that missed typos in one
213
+ direction; a `--dry-run` that fabricated a misleading preview instead of refusing on an
214
+ unconfirmed thread the way a real run would; `doctor`'s binary checks using `spawnSync`
215
+ inside an `async` wrapper, silently serializing what was supposed to be concurrent via
216
+ `Promise.allSettled`) — codex's branch was carried forward. A **fresh** GLM-5.2 session (no
217
+ context on how the branch was built) then adversarially audited it: found the same
218
+ `--dry-run`-bypasses-a-real-refusal class of bug in a different spot (skipped the
219
+ "run-already-in-flight" check, not just the confirmation check) and a broken *custom*-named
220
+ external adapter crashing the entire load — including `doctor`, the one command meant to
221
+ diagnose exactly that. Both fixed and verified live. The audit's one headline finding (a
222
+ claimed exit-code regression in the new `RelayError` paths) was checked directly against
223
+ `main`'s actual pre-refactor source and found to be a false positive — the audit had
224
+ correctly flagged its own uncertainty (its sandbox blocked `git` access) rather than
225
+ asserting it, which is why it was checked rather than trusted or dismissed outright. Full
226
+ brief in `docs/cli-continues-cherrypick-brief.md`.
227
+
228
+ ## Field notes from real use
229
+
230
+ First real-world use (2026-08-17, an audit task run across all three model lanes in
231
+ parallel) surfaced two things design review and smoke tests couldn't have caught, plus
232
+ confirmed the core value prop actually landed:
233
+
234
+ **Worked as designed:** resumed a thread twice — once for a full re-audit, once for a
235
+ fix — without restating the brief; codex picked up full context both times, including
236
+ correctly remembering it was mid-audit after an unrelated manual `&`/`wait` mistake had
237
+ SIGTERM'd the first run at 120s. That SIGTERM'd run still left a `confirmed: true` thread
238
+ with its `native_session_id` intact — resumable, not lost — exactly the point of storing
239
+ native ids rather than transcripts. `cli-relay list` and the raw map gave enough visibility
240
+ (`last_signal: SIGTERM`, `cancelled_by_wrapper: true`) to diagnose exactly what had happened
241
+ without guessing. Running three model lanes in parallel (Command Code/GLM, codex, agy) was
242
+ cheap specifically because they share one invocation shape instead of three different CLI
243
+ syntaxes — worth doing again for anything where cross-model corroboration matters, each
244
+ lane found genuinely different things in that test.
245
+
246
+ **Bug found and fixed:** `codex exec resume` has no `--sandbox` flag at all — passing one is
247
+ a hard CLI error — so a resumed thread silently got LESS filesystem access than the fresh
248
+ call that created it, and couldn't apply a fix it had just identified. The `resume` adapter
249
+ now passes `--dangerously-bypass-approvals-and-sandbox` instead; reasonable given `fresh`
250
+ already grants codex write access to whatever cwd it's pointed at, so resume defaulting to
251
+ less access than fresh granted the same thread was an inconsistency, not a real safety
252
+ boundary. Verified live: a resumed thread can now create a file it couldn't before.
253
+
254
+ **Gap found, mitigated but not solved:** `agy` didn't respect an in-prompt instruction to
255
+ work in a scratch copy — it read from the canonical source-of-truth path instead (no harm
256
+ that time: read-only, and the canonical repo was verified to stay clean, but a real
257
+ instruction-following gap). Not strictly a `cli-relay` bug — it's `agy`'s own behavior — but
258
+ `cli-relay` wasn't doing anything to scope it either. Now passes `--add-dir <cwd>` (agy's own
259
+ explicit workspace-scoping flag) on every agy call as a stronger signal than prose. This is
260
+ defense in depth, not an enforced guarantee — don't rely on it for anything where a
261
+ canonical/production path must not be touched; verify after, the way this first real use did.
262
+
263
+ **Bug found and fixed (2026-08-20), while live-testing the compaction-detection work below:**
264
+ Command Code returned a genuine `sessionId` inside an *error* response body (`"insufficient
265
+ credits"`, an account billing issue, not a cli-relay bug) with `finalText: ""` — the old
266
+ fresh-mode check only required an id to mark a thread `confirmed: true`, so a real API error
267
+ was silently getting recorded as a clean successful thread. Fixed: fresh mode now requires
268
+ BOTH a real id and a non-empty answer, the same standard resume mode's exit-3 check already
269
+ held it to. Found by accident (an unrelated credits exhaustion), not by design — worth
270
+ remembering that live testing against real backends keeps finding real gaps neither review
271
+ nor synthetic tests reach, same pattern as every other bug in this file.
272
+
273
+ ## Compaction risk (2026-08-20)
274
+
275
+ Long-running resumed threads carry a real risk: the *backend's own* internal context
276
+ compaction can silently reorder or lose fidelity on which fact is current — an early,
277
+ now-superseded statement can outweigh a later correction once summarized, with no signal to
278
+ either side that it happened. This is not a cli-relay bug, it's a property of every backend's
279
+ own memory management, but resume-by-reference actively increases exposure to it (that's the
280
+ whole point of resuming — restating context, which forces the backend to re-derive/re-compact
281
+ its own history, less often).
282
+
283
+ **Detected, not prevented, and only where proven — not guessed:**
284
+ - `codex`: a real `context_compacted` event was directly observed in a live codex session's
285
+ detailed rollout log (`~/.codex/sessions/**/rollout-*.jsonl`) — NOT visible in the simplified
286
+ `--json` stdout stream this router otherwise parses, so detection means reading that file
287
+ separately by thread id after each call.
288
+ - `command-code`: the installed CLI's own compiled source contains `compaction_start`/
289
+ `compaction_done` events using the same `.emit()` pattern already confirmed to reach the
290
+ external NDJSON stream for other event types — strong evidence, not yet live-observed
291
+ (would need a genuinely huge context to force for real).
292
+ - `agy`: explicitly NOT checked. Its compiled binary confirms a compaction concept exists
293
+ (`"Conversation compacted"`, boundary-marker strings) but the only structured trace of it
294
+ lives in agy's *render* package, not its data model — `stream-json` output showed no
295
+ compaction-shaped event. Building detection on a render-layer artifact would be guessing.
296
+ - `claude-code`: skipped by explicit choice, not tested. `--autocompact` exists and is at
297
+ least configurable if this becomes worth revisiting.
298
+
299
+ When detected, the thread's `compaction_detected` flag is set (sticky — once true, stays
300
+ true) and surfaced loudly in `cli-relay list` and every subsequent call's output.
301
+
302
+ **Also added: a turn-count advisory.** `RESUME_WARNING_THRESHOLD = 10` — not derived from any
303
+ verified per-model context-window size (that kind of number goes stale, the same trap the
304
+ Nemotron/GLM catalog rotation already burned this project on once). Purely a crude, model-
305
+ agnostic proxy, purely advisory, never blocks — warns past 10 resumes on one thread, visible
306
+ in `cli-relay list`'s `turns` column.
307
+
308
+ **This detection alone is reactive, not a real fix** — it tells you something risky already
309
+ happened, it doesn't stop it. The proactive counterpart, shipped 2026-08-24 (full design in
310
+ `docs/pinned-facts-design.md`): a per-thread **pinned-facts ledger**. `cli-relay pin <thread>
311
+ "<fact>"` stores a fact that cli-relay itself re-injects into every future `fresh` **and**
312
+ `resume` prompt on that thread, external to and independent of whatever the backend's own
313
+ compaction does to its internal memory — a correction's survival no longer depends on the
314
+ backend remembering it correctly through a compaction event at all. Pins deliberately persist
315
+ across a `fresh` restart (unlike turn count/compaction flag, which reset) — the recommended
316
+ recovery from a compaction-risky thread (pin the load-bearing facts, then restart fresh) now
317
+ carries them forward automatically instead of requiring manual re-typing. Verified live:
318
+ resuming a thread correctly answered a fact that was never in the visible prompt, and a
319
+ genuinely new native session after a `fresh` restart still knew it. Deliberately NOT
320
+ auto-detecting "this looks like a correction" from prompt text to auto-pin it — that's
321
+ guessing at intent, the same trap this whole project has avoided everywhere else.
322
+
323
+ ## Known gaps (not blocking, worth knowing)
324
+
325
+ - `command-code` resume stays disabled until its seed-turn-drop bug is root-caused.
326
+ - `claude-code` calls hit real Anthropic billing against the Pro plan (confirmed ~$0.07-0.13
327
+ per short test call) — not free the way codex/agy effectively are for testing.
328
+ - Grandchild processes that double-fork/setsid out of a backend's process group would survive
329
+ a router-initiated kill (documented in `cli-relay.mjs`, not solved — no known instance of this
330
+ happening yet).
331
+ - **`SPAWN_TIMEOUT_MS` bumped 10m → 20m (2026-08-24).** Observed live 2026-08-20: at least 3
332
+ real threads doing genuine audit-scale work hit `last_timed_out: true`, not stuck processes.
333
+ 20m is still a
334
+ guess, not a verified figure — watch `cli-relay list` for more timeouts before assuming
335
+ it's the right number either.
336
+ - **`agy`'s model catalog rotates without notice (found 2026-08-31).** The hardcoded
337
+ `gemini-3.5-flash-medium` in `src/adapters/agy.mjs` was silently removed from agy's own
338
+ model list, breaking every `cli-relay agy` call until caught and bumped to
339
+ `gemini-3.6-flash-medium`. No detection for this beyond the call itself failing loud (which
340
+ it does correctly) — if agy calls start failing with "invalid model selection," check
341
+ `agy models` for a renamed/retired model before assuming cli-relay itself is broken.
342
+ - **A narrow SIGINT window can still stick a thread at `status: "running"` (found 2026-09-01,
343
+ not introduced by anything recent — pre-existing since the original signal-handling work).**
344
+ Between critical section 1 saving `status: "running"` and `runChild` actually setting
345
+ `activeChildPgid`, a SIGINT lands in the signal handler's "nothing spawned yet" branch and
346
+ exits immediately with no unwind. The lock itself is already released by that point (CS1
347
+ finished), so the thread doesn't self-heal via lock staleness — it stays stuck until
348
+ `LOCK_STALE_MS` (~21 min) passes on the *next* invocation against that thread. Narrow
349
+ window, real gap; not closed here.
350
+ - `doctor`'s `which`/`where` child processes aren't tracked by the SIGINT handler — a Ctrl-C
351
+ during `doctor` reports "nothing spawned yet" even though those children are briefly alive.
352
+ Low severity (`which` exits in milliseconds).
353
+ - A prompt whose text is literally `--dry-run` or `--print-command` gets stripped from the
354
+ prompt and treated as the flag — an edge case the flag's argv-scanning approach introduces,
355
+ not expected to matter in practice.