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 +21 -0
- package/README.md +355 -0
- package/cli-relay.mjs +481 -0
- package/package.json +33 -0
- package/src/adapter-loader.mjs +138 -0
- package/src/adapters/agy.mjs +21 -0
- package/src/adapters/claude-code.mjs +18 -0
- package/src/adapters/codex.mjs +58 -0
- package/src/adapters/command-code.mjs +18 -0
- package/src/commands/doctor.mjs +59 -0
- package/src/commands/list.mjs +52 -0
- package/src/commands/pin.mjs +37 -0
- package/src/commands/pins.mjs +27 -0
- package/src/commands/reset.mjs +36 -0
- package/src/commands/unpin.mjs +32 -0
- package/src/config.mjs +84 -0
- package/src/core/adapter-env.mjs +3 -0
- package/src/core/env.mjs +7 -0
- package/src/core/errors.mjs +8 -0
- package/src/core/lock.mjs +123 -0
- package/src/core/map-store.mjs +20 -0
- package/src/core/parse-json-result.mjs +24 -0
- package/src/core/pins.mjs +24 -0
- package/src/core/thread-lookup.mjs +12 -0
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.
|