@henols/vice-mcp 0.2.3 → 0.2.4

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.
@@ -6,29 +6,28 @@
6
6
  // rebuild.
7
7
  // broker-launch.mts
8
8
  //
9
- // C (complete, plan 02 of Phase 01.6.2): the single `in_flight` launch-guard
10
- // owner (plan 01, unchanged -- every launch call site in the whole broker
11
- // goes through tryLaunchOne(), which is what makes the single-owner
12
- // guarantee mechanical rather than a convention plan 02's own concurrency
13
- // race test can silently violate), PLUS the readiness probe's single
14
- // in-process mechanism (collapsed from a three-way branch by Phase
15
- // 01.6.2.1's own plan 02 -- D-05 as amended by P-05/P-06/P-07; see
16
- // probeReady()'s own header comment below for the amendment's record),
9
+ // This module owns three concerns that started life separately and were
10
+ // folded together here: the single `in_flight` launch-guard owner -- every
11
+ // launch call site in the whole broker goes through tryLaunchOne(), which
12
+ // is what makes the single-owner guarantee mechanical rather than a
13
+ // convention a concurrency race test could silently violate -- PLUS the
14
+ // readiness probe's single in-process mechanism (collapsed from a
15
+ // three-way branch down to one; see probeReady()'s own header comment
16
+ // below for the full record of that collapse and its later amendment),
17
17
  // the launching -> ready promotion sweep (promoteLaunchingInstances() --
18
- // plan 41-05, folded todo: this used to be step 1 inside a warm-floor
19
- // maintenance function that speculatively pre-launched spare instances;
20
- // that floor is RETIRED and VICE now launches strictly on demand, but the
21
- // promotion sweep outlived it), and the fixed-order evaluation pass both
22
- // surviving concerns run through.
18
+ // this used to be step one inside a warm-floor maintenance function that
19
+ // speculatively pre-launched spare instances; that floor is RETIRED and
20
+ // VICE now launches strictly on demand, but the promotion sweep outlived
21
+ // it), and the fixed-order evaluation pass both surviving concerns run
22
+ // through.
23
23
  //
24
- // Plan 03, Task 2 grows this module into a real per-child supervisor
25
- // (C2/D-23), absorbing resources/vice-supervisor.sh wholesale: superviseChild()
24
+ // This file also grew a real per-child supervisor: superviseChild()
26
25
  // launches an instance through tryLaunchOne() (the SAME single guarded
27
26
  // primitive above) and installs an exit handler on the spawned child that
28
27
  // respawns on crash (doubling backoff, clamped at a ceiling), gives up
29
28
  // cleanly after too many crashes inside a window, never respawns a
30
29
  // deliberately-killed instance, and writes the per-instance boot/crash log
31
- // D-23 preserves at the exact path shape the retiring bash supervisor used.
30
+ // at the exact path shape the retiring bash supervisor used.
32
31
  import { spawn as nodeSpawn } from "node:child_process";
33
32
  import { mkdirSync, mkdtempSync, openSync, closeSync, existsSync } from "node:fs";
34
33
  import { join, basename } from "node:path";
@@ -37,13 +36,13 @@ import { tmpdir } from "node:os";
37
36
  // synchronous check, synchronous set, released in a finally, with no
38
37
  // `await` between the check and the set.
39
38
  //
40
- // D-07 (01.6.2.1-03-PLAN.md): launch PRIORITY is layered on this owner, and
41
- // never replaces or weakens it. An in-flight boot always completes and is
42
- // NEVER killed or abandoned to serve a later arrival -- preemption was
43
- // considered and rejected (01.6.2-CONTEXT.md D-07) because a kill/relaunch
44
- // overlap re-creates the exact concurrent-spawn window the 2026-08-01
45
- // outage came from (one SEGV, one exit 1, one exit 0 at the identical spawn
46
- // second). Once a boot reaches `ready`, a waiting request takes it
39
+ // Launch PRIORITY is layered on this owner, and never replaces or weakens
40
+ // it. An in-flight boot always completes and is NEVER killed or abandoned
41
+ // to serve a later arrival -- preemption was considered and rejected
42
+ // because a kill/relaunch overlap re-creates the exact concurrent-spawn
43
+ // window the 2026-08-01 outage came from (one SEGV, one exit 1, one exit 0
44
+ // at the identical spawn second). Once a boot reaches `ready`, a waiting
45
+ // request takes it
47
46
  // regardless of which reason booted it (vice-broker.mts's
48
47
  // selectWarmInstance() performs no `reason` check at all -- proven by
49
48
  // vice-broker-acquire.test.ts). Priority governs only which REASON wins
@@ -54,10 +53,9 @@ let inFlight = false;
54
53
  // only, never a second guard: nothing branches on this value's presence to
55
54
  // decide whether a launch may proceed (that is `inFlight` alone, checked
56
55
  // and set synchronously exactly as before). Its only consumer is the
57
- // launch-slot decision log line (D-07's standing constraint that a
58
- // lifecycle decision must be reconstructable from the log after an
59
- // incident -- both 2026-08-01 and 2026-08-02 were diagnosed from broker log
60
- // lines).
56
+ // launch-slot decision log line: a lifecycle decision must be
57
+ // reconstructable from the log after an incident -- both the 2026-08-01
58
+ // and 2026-08-02 outages were diagnosed from broker log lines.
61
59
  let inFlightReason = null;
62
60
  /** True while a launch is in progress -- exported for the race test plan 02
63
61
  * writes against two concurrent tryLaunchOne() calls. */
@@ -69,31 +67,31 @@ export function isLaunchInFlight() {
69
67
  // most once per process -- the repo-root.ts `warnedEnvOutsideFrom` gate
70
68
  // pattern, reused here.
71
69
  let warnedBinmonBindWidened = false;
72
- // Plan 03-04 (DIRECT-06, D-13): the SAME one-time-note idiom as
73
- // warnedBinmonBindWidened above, for the SECOND (`-remotemonitor`) port's
74
- // bind -- a separate boolean because the two flags widen independently (a
75
- // caller could widen one host override and not the other, though in
76
- // practice both resolve from the same `binmonHost` value below).
70
+ // The SAME one-time-note idiom as warnedBinmonBindWidened above, for the
71
+ // SECOND (`-remotemonitor`) port's bind -- a separate boolean because the
72
+ // two flags widen independently (a caller could widen one host override
73
+ // and not the other, though in practice both resolve from the same
74
+ // `binmonHost` value below).
77
75
  let warnedRemoteMonitorBindWidened = false;
78
- /** Phase 33, plan 33-05 (`REPRO-01`): the random seed the stock determinism
79
- * block pins, and the exact value the reproduction was measured with on this
80
- * host -- exported so a capture record's reproducibility key can cite ONE
81
- * definition rather than re-deriving a literal that could silently drift away
82
- * from the launches it claims to describe. MEASURED 2026-09-02 against genuine
83
- * stock 3.9 over `-binarymonitor` (33-RESEARCH.md M3): two cold boots WITHOUT
84
- * the block differ at 59 of the 4080 addresses in the untouched `$C000-$CFEF`
85
- * window; WITH it, at 0 of 4080. */
76
+ /** The random seed the stock determinism block pins, and the exact value
77
+ * the reproduction was measured with on this host -- exported so a capture
78
+ * record's reproducibility key can cite ONE definition rather than
79
+ * re-deriving a literal that could silently drift away from the launches it
80
+ * claims to describe. MEASURED 2026-09-02 against genuine stock 3.9 over
81
+ * `-binarymonitor`: two cold boots WITHOUT the block differ at 59 of the
82
+ * 4080 addresses in the untouched `$C000-$CFEF` window; WITH it, at 0 of
83
+ * 4080. */
86
84
  export const STOCK_DETERMINISM_SEED = 4242;
87
- /** Phase 33, plan 33-05 (`REPRO-01`): the determinism block, in ONE fixed
88
- * order, emitted UNCONDITIONALLY by the stock branch below (never gated on
89
- * `profile` -- see buildViceArgs()'s own comment above `args`). Exported and
90
- * frozen so tests and evidence scripts assert against this single definition
91
- * instead of a second hand-copied array, and so no caller can mutate the
92
- * shared value into a launch that no longer matches the recorded seed.
85
+ /** The determinism block, in ONE fixed order, emitted UNCONDITIONALLY by
86
+ * the stock branch below (never gated on `profile` -- see buildViceArgs()'s
87
+ * own comment above `args`). Exported and frozen so tests and evidence
88
+ * scripts assert against this single definition instead of a second
89
+ * hand-copied array, and so no caller can mutate the shared value into a
90
+ * launch that no longer matches the recorded seed.
93
91
  *
94
- * The order is fixed and load-bearing beyond readability: `REPRO-04` keys
95
- * captures on an argv digest, so a block whose element order varied between
96
- * two launches on the same port would produce two digests for one launch
92
+ * The order is fixed and load-bearing beyond readability: capture records
93
+ * key on an argv digest, so a block whose element order varied between two
94
+ * launches on the same port would produce two digests for one launch
97
95
  * intent. */
98
96
  export const STOCK_DETERMINISM_FLAGS = Object.freeze([
99
97
  "-seed",
@@ -115,56 +113,49 @@ export const STOCK_DETERMINISM_FLAGS = Object.freeze([
115
113
  * understands neither `-mcpserver` nor `-binarymonitor` flags, and that need
116
114
  * does not depend on which backend is configured.
117
115
  *
118
- * `backend: "stock"` (FORKRM-01, plan 52-06: the only value `ViceBackend` has
119
- * left) returns `-binarymonitor -binarymonitoraddress
120
- * ip4://<host>:<port>` (docs/phase1-probe-results.md's confirmed real-world
121
- * command line). The host resolves from `binmonHost` or
122
- * VICE_BROKER_BINMON_HOST, defaulting to `127.0.0.1` -- deliberately
123
- * narrow, because VICE's binary
124
- * monitor is unauthenticated by design and grants full read/write over the
125
- * emulated machine plus process control to anything that can reach it
126
- * (planner decision, `02-03-PLAN.md`). Widening the bind away from loopback
127
- * emits exactly one stderr note per process, naming the resolved bind
128
- * address and what the exposure grants.
116
+ * `backend: "stock"` (the only value `ViceBackend` has, now that the fork
117
+ * backend is gone) returns `-binarymonitor -binarymonitoraddress
118
+ * ip4://<host>:<port>`, the confirmed real-world command line. The host
119
+ * resolves from `binmonHost` or VICE_BROKER_BINMON_HOST, defaulting to
120
+ * `127.0.0.1` -- deliberately narrow, because VICE's binary monitor is
121
+ * unauthenticated by design and grants full read/write over the emulated
122
+ * machine plus process control to anything that can reach it. Widening the
123
+ * bind away from loopback emits exactly one stderr note per process,
124
+ * naming the resolved bind address and what the exposure grants.
129
125
  *
130
- * Plan 03-04 (DIRECT-06, D-13): when `remoteMonitorPort` is a number, the
131
- * stock branch APPENDS `-remotemonitor -remotemonitoraddress
132
- * ip4://<host>:<remoteMonitorPort>`, reusing the SAME resolved `host` value
133
- * the binmon address already used -- one resolution, not two. When
134
- * `remoteMonitorPort` is omitted (undefined), the returned argv is
135
- * byte-identical to what this function always returned -- no
136
- * `-remotemonitor` at all. `-remotemonitoraddress`'s exact spelling was
137
- * live-probed against a real fork-3.10 binary and CONFIRMED (RESEARCH.md
138
- * Assumption A1, closed by 13-PROBE-RESULTS.md §A1: the flag bound a real,
139
- * accepting text-monitor listener, corroborated independently by
140
- * `ss -ltnp`). Genuine stock 3.9 was not independently probed in that run --
141
- * the spelling itself is a symmetrical CLI flag pair and is not
142
- * version-sensitive, so this is recorded as a low-risk carry-forward rather
143
- * than implied stock-3.9 coverage. Widening THIS bind away from loopback emits
144
- * its own one-time stderr note (`warnedRemoteMonitorBindWidened`), naming
145
- * the resolved address and stating that VICE's TEXT monitor accepts
146
- * arbitrary monitor commands and is unauthenticated. Phase 41 dials this
147
- * port (text-connect.ts's textConnect()) and Phase 41-05 (D-16) made it
148
- * MANDATORY on every stock launch -- a stock launch that cannot bind it now
149
- * fails the whole acquire rather than launching without it (see D-13's own
150
- * rationale for why the flag itself is set at launch time and not added
151
- * later: doing so would require relaunching a live instance, destroying all
152
- * emulation state).
126
+ * When `remoteMonitorPort` is a number, the stock branch APPENDS
127
+ * `-remotemonitor -remotemonitoraddress ip4://<host>:<remoteMonitorPort>`,
128
+ * reusing the SAME resolved `host` value the binmon address already used --
129
+ * one resolution, not two. When `remoteMonitorPort` is omitted (undefined),
130
+ * the returned argv is byte-identical to what this function always
131
+ * returned -- no `-remotemonitor` at all. `-remotemonitoraddress`'s exact
132
+ * spelling was live-probed against a real fork-3.10 binary and CONFIRMED:
133
+ * the flag bound a real, accepting text-monitor listener, corroborated
134
+ * independently by `ss -ltnp`. Genuine stock 3.9 was not independently
135
+ * probed in that run -- the spelling itself is a symmetrical CLI flag pair
136
+ * and is not version-sensitive, so this is recorded as a low-risk
137
+ * carry-forward rather than implied stock-3.9 coverage. Widening THIS bind
138
+ * away from loopback emits its own one-time stderr note
139
+ * (`warnedRemoteMonitorBindWidened`), naming the resolved address and
140
+ * stating that VICE's TEXT monitor accepts arbitrary monitor commands and
141
+ * is unauthenticated. text-connect.ts's textConnect() dials this port, and
142
+ * it is MANDATORY on every stock launch -- a stock launch that cannot bind
143
+ * it now fails the whole acquire rather than launching without it, because
144
+ * the flag has to be set at launch time: adding it later would require
145
+ * relaunching a live instance, destroying all emulation state.
153
146
  *
154
- * Phase 33, plan 33-05 (`D-15`, `REPRO-01`, `REPRO-05`): the stock branch now
155
- * also emits STOCK_DETERMINISM_FLAGS unconditionally, and takes an optional
156
- * `profile` for the two additive launch knobs. Deliberately in the same
157
- * register as tryLaunchOne's own widened `spawn` field below, and for the same
158
- * reason: `profile` is optional, so every pre-existing caller and every
159
- * pre-existing test stub keeps compiling and behaving identically, and an
160
- * ABSENT profile produces exactly the same argv as an empty one or one whose
161
- * knobs are both `false`. What optionality could NOT save (33-RESEARCH.md P7,
162
- * and the `D-15` amendment rider dated 2026-09-02): the determinism block is
163
- * unconditional on stock, so all FIVE stock whole-argv assertions in
164
- * broker-launch.test.ts move even with `profile` absent -- it is the block and
165
- * not the profile that moves them. `D-15`'s byte-identity claim therefore
166
- * survives in full only for the `profile` half: an absent profile adds no
167
- * flag. */
147
+ * The stock branch also emits STOCK_DETERMINISM_FLAGS unconditionally, and
148
+ * takes an optional `profile` for the two additive launch knobs. Deliberately
149
+ * in the same register as tryLaunchOne's own widened `spawn` field below,
150
+ * and for the same reason: `profile` is optional, so every pre-existing
151
+ * caller and every pre-existing test stub keeps compiling and behaving
152
+ * identically, and an ABSENT profile produces exactly the same argv as an
153
+ * empty one or one whose knobs are both `false`. What optionality could NOT
154
+ * save: the determinism block is unconditional on stock, so all FIVE stock
155
+ * whole-argv assertions in broker-launch.test.ts move even with `profile`
156
+ * absent -- it is the block and not the profile that moves them. The
157
+ * byte-identity claim therefore survives in full only for the `profile`
158
+ * half: an absent profile adds no flag. */
168
159
  export function buildViceArgs(port, { backend, mcpHost, binmonHost, viceArgsEnv, remoteMonitorPort, profile, }) {
169
160
  const rawViceArgs = viceArgsEnv ?? process.env.VICE_ARGS;
170
161
  if (typeof rawViceArgs === "string" && rawViceArgs.trim() !== "") {
@@ -194,76 +185,77 @@ export function buildViceArgs(port, { backend, mcpHost, binmonHost, viceArgsEnv,
194
185
  // `-binarymonitor` or the monitor never binds and the subsequent connect
195
186
  // hangs in the backlog looking exactly like a wedge.
196
187
  //
197
- // WORDING CORRECTED (33 review WR-09). This paragraph used to say
198
- // "immediately after `-default`", which the `-console` block below now
199
- // violates by construction whenever `profile.headless` is set -- leaving
200
- // the next editor to find code contradicting the comment and having to
201
- // re-derive which one is authoritative. What is load-bearing is the
202
- // RELATIVE ORDER (`-default` precedes everything it resets), not
203
- // adjacency.
188
+ // WORDING CORRECTED. This paragraph used to say "immediately after
189
+ // `-default`", which the `-console` block below now violates by
190
+ // construction whenever `profile.headless` is set -- leaving the next
191
+ // editor to find code contradicting the comment and having to re-derive
192
+ // which one is authoritative. What is load-bearing is the RELATIVE
193
+ // ORDER (`-default` precedes everything it resets), not adjacency.
204
194
  //
205
195
  // The `-console` block's own citation was `alive=yes bound=1`, which does
206
- // NOT cover this paragraph's property: I-2's failure mode is Drive8Type
207
- // silently reverting to 0 (NONE) WHILE THE MONITOR STILL BINDS FINE, so
208
- // liveness and boundness cannot tell the good case from the failure being
209
- // guarded against. Re-verified against the resource itself
196
+ // NOT cover this paragraph's property: the failure mode this flag guards
197
+ // against is Drive8Type silently reverting to 0 (NONE) WHILE THE MONITOR
198
+ // STILL BINDS FINE, so liveness and boundness cannot tell the good case
199
+ // from the failure being guarded against. Re-verified against the
200
+ // resource itself
210
201
  // [VERIFIED: live probe 2026-09-03, genuine unpatched stock
211
202
  // /usr/bin/x64sc (VICE 3.9), DISPLAY and WAYLAND_DISPLAY both unset]:
212
203
  // [-default -console -drive8type 1541 <determinism> -binarymonitor]
213
204
  // alive=yes bound=1 Drive8Type=1541 Drive8TrueEmulation=1
214
205
  // read over `RESOURCE_GET` (0x51) with `-console` interposed. So the
215
- // citation now covers the RESOURCE and not only liveness. Confirmed sufficient live in
216
- // Phase 8.1's standalone probe (08.1-WALKTHROUGH-EVIDENCE.md §4):
206
+ // citation now covers the RESOURCE and not only liveness. Confirmed
207
+ // sufficient live in a standalone probe:
217
208
  // `resourceget "Drive8Type"` moved 0 -> 1541 and a `load` over the text
218
209
  // monitor succeeded immediately. Deliberately NOT setting
219
210
  // -drive8truedrive / Drive8TrueEmulation here: this build's own default
220
- // already reads Drive8TrueEmulation=1 (same probe), so 08.2-RESEARCH.md's
221
- // primary recommendation is that only -drive8type needs adding.
222
- // Assumption A3 in that doc's Assumptions Log (some other stock build
223
- // might default Drive8TrueEmulation to 0) is read and deliberately not
224
- // pre-emptively defended against here; plan 03's live test is what would
225
- // surface it if that assumption is ever wrong on a different build.
211
+ // already reads Drive8TrueEmulation=1 (same probe), so only
212
+ // `-drive8type` needs adding.
213
+ // A different stock build might default Drive8TrueEmulation to 0,
214
+ // which is read and deliberately not pre-emptively defended against
215
+ // here; a live test against that build is what would surface it if
216
+ // this assumption is ever wrong there.
226
217
  //
227
- // Phase 33, plan 33-05, `-console` at index 1 (`REPRO-05`, `D-15`): the
228
- // headless route is `-console`, and its POSITION is as load-bearing as
229
- // `-default`'s. `-console` is handled in the SAME `main.c` pre-scan as
230
- // `-default` -- that loop `break`s at the first option it does not
231
- // recognise and then strips the prefix it handled from argv
218
+ // The headless route is `-console`, and its POSITION is as
219
+ // load-bearing as `-default`'s. `-console` is handled in the SAME
220
+ // `main.c` pre-scan as `-default` -- that loop `break`s at the first
221
+ // option it does not recognise and then strips the prefix it handled
222
+ // from argv
232
223
  // [CITED: vice-3.8/src/main.c:184-192, 232-238] -- and `console_mode`
233
224
  // gates GTK initialisation at two call sites (`ui_init_with_args`,
234
225
  // `ui_init`) that BOTH run before the late command-line parser
235
226
  // `initcmdline_check_args()` [CITED: vice-3.8/src/main.c:296-345]. A
236
227
  // `-console` seen only by the late parser therefore arrives after GTK has
237
228
  // already tried and failed. MEASURED 2026-09-02 with `DISPLAY` and
238
- // `WAYLAND_DISPLAY` both unset (33-RESEARCH.md P5):
229
+ // `WAYLAND_DISPLAY` both unset:
239
230
  // [-default -console -binarymonitor] alive=yes bound=1
240
231
  // [-default -drive8type 1541 -console -binarymonitor] alive=no bound=0 Gtk-WARNING: cannot open display:
241
232
  // [-default -console -drive8type 1541 -binarymonitor] alive=yes bound=1
242
233
  // So `-console` goes immediately after `-default` and BEFORE
243
- // `-drive8type` -- which is compatible with the I-2 paragraph above as
244
- // corrected (33 review WR-09): that constraint is `-drive8type` AFTER
245
- // `-default`, not adjacent to it, and the interposition was re-verified
246
- // over `RESOURCE_GET` to leave Drive8Type=1541 rather than only to leave
234
+ // `-drive8type` -- which is compatible with the paragraph above as
235
+ // corrected: that constraint is `-drive8type` AFTER `-default`, not
236
+ // adjacent to it, and the interposition was re-verified over
237
+ // `RESOURCE_GET` to leave Drive8Type=1541 rather than only to leave
247
238
  // the monitor bound. It is pinned by an ordering assertion rather than by
248
239
  // this comment -- a bare flag push with no reason is exactly what let the
249
240
  // `-default` ordering constraint be rediscovered by a red CI run last
250
241
  // time.
251
242
  //
252
- // Phase 33, plan 33-05, the determinism block (`REPRO-01`): emitted
253
- // UNCONDITIONALLY on stock, never gated on `profile`. Read over
254
- // `RESOURCE_GET` (0x51) on this build under `-default -drive8type 1541`
255
- // [VERIFIED: live probe 2026-09-02, 33-RESEARCH.md M2],
243
+ // The determinism block is emitted UNCONDITIONALLY on stock, never
244
+ // gated on `profile`. Read over `RESOURCE_GET` (0x51) on this build
245
+ // under `-default -drive8type 1541`
246
+ // [VERIFIED: live probe 2026-09-02],
256
247
  // `-raminitrandomchance 0` is the LOAD-BEARING one: the factory value is
257
248
  // **10**, i.e. 0.1% of all RAM bits randomly flipped at power-up, and it is
258
249
  // the dominant term in the divergence this milestone removes. The
259
250
  // `-raminitstartrandom 0` / `-raminitrepeatrandom 0` pair already reads 0
260
251
  // at factory and is DEFENSIVE against an operator `vicerc` -- belt and
261
- // braces alongside the scratch `XDG_CONFIG_HOME` the I-1 rider threads in
252
+ // braces alongside the scratch `XDG_CONFIG_HOME` threaded in
262
253
  // spawnAndRecordInstance() below.
263
254
  //
264
- // `+autostart-delay-random` is a FIFTH flag beyond `REPRO-01`'s text
265
- // (which names only `-seed` plus the three `raminit*`), recorded here as a
266
- // deliberate ADDITION rather than smuggled in. `AutostartDelayRandom`
255
+ // `+autostart-delay-random` is a FIFTH flag beyond the determinism
256
+ // block's own headline text (which names only `-seed` plus the three
257
+ // `raminit*`), recorded here as a deliberate ADDITION rather than
258
+ // smuggled in. `AutostartDelayRandom`
267
259
  // ships at **1** on this build [VERIFIED: same probe], it draws an
268
260
  // additional random delay of up to 10 frames
269
261
  // [CITED: vice-3.8/src/autostart.c:1432-1436], and -- the effect that is
@@ -273,21 +265,20 @@ export function buildViceArgs(port, { backend, mcpHost, binmonHost, viceArgsEnv,
273
265
  // behavioural change and not only a timing one. Pin it once; never toggle
274
266
  // it between the two runs of a capture pair.
275
267
  //
276
- // Phase 33, plan 33-05, `-warp` (`D-15`): position-free (MEASURED
277
- // 2026-09-02), placed here only so the argv reads in the order a human
278
- // would describe it. Launch-time is the ONLY route on stock: there is no
279
- // runtime `WarpMode` resource at all (`RESOURCE_GET` replies `err=0x01`
280
- // OBJECT_MISSING on 3.9), so no runtime setter can exist here. Worth
281
- // roughly **1.97x** on this host and this launch profile -- 5.23 emulated
282
- // seconds against 2.65 over the same 5 s wall clock [MEASURED:
283
- // evidence/33-wallclock-control.md, Control B instance 1] -- and NOT the
284
- // order of magnitude a reader may assume; `AUTOSTART` additionally turns
285
- // warp on by itself during the load whatever argv says, so both loads are
286
- // warped either way. It is behaviour-neutral under a frame-anchored
287
- // protocol (identical registers and one identical 64K sha256 across a
288
- // warped and an unwarped run, same source), which is the precondition
289
- // `33-06` needs before `profile.warp` ships; it is NOT neutral for a
290
- // wall-clock bracket, which it invalidates by 1.76x.
268
+ // `-warp` is position-free (MEASURED 2026-09-02), placed here only so
269
+ // the argv reads in the order a human would describe it. Launch-time is
270
+ // the ONLY route on stock: there is no runtime `WarpMode` resource at
271
+ // all (`RESOURCE_GET` replies `err=0x01` OBJECT_MISSING on 3.9), so no
272
+ // runtime setter can exist here. Worth roughly **1.97x** on this host
273
+ // and this launch profile -- 5.23 emulated seconds against 2.65 over
274
+ // the same 5 s wall clock [MEASURED: evidence/33-wallclock-control.md,
275
+ // Control B instance 1] -- and NOT the order of magnitude a reader may
276
+ // assume; `AUTOSTART` additionally turns warp on by itself during the
277
+ // load whatever argv says, so both loads are warped either way. It is
278
+ // behaviour-neutral under a frame-anchored protocol (identical registers
279
+ // and one identical 64K sha256 across a warped and an unwarped run, same
280
+ // source), which is a precondition for shipping `profile.warp`; it is
281
+ // NOT neutral for a wall-clock bracket, which it invalidates by 1.76x.
291
282
  const args = ["-default"];
292
283
  if (profile?.headless) {
293
284
  args.push("-console");
@@ -331,42 +322,40 @@ function spawnAndRecordInstance(reason, port, deps) {
331
322
  const now = deps.now ?? (() => Date.now());
332
323
  const viceBin = deps.viceBin ?? process.env.VICE_BIN ?? "x64sc";
333
324
  const backend = deps.backend ?? "stock";
334
- // Plan 41-05 (D-16): the ONE construction site for a fresh InstanceRecord
335
- // asserts the invariant every downstream consumer (HeldLease,
336
- // textConnect(), etc.) was written against -- a stock record NEVER lacks a
337
- // text-monitor port. acquirePortAndLaunch() above already fails the whole
338
- // acquire before ever reaching this function when the second allocation
339
- // fails, so a caller that lands here with `backend: "stock"` and no
340
- // `remoteMonitorPort` is a defect in THIS module (a call site that bypassed
341
- // that guarantee), not a state a stock record may legitimately carry --
342
- // throw by name rather than silently writing a record that violates it.
325
+ // The ONE construction site for a fresh InstanceRecord asserts the
326
+ // invariant every downstream consumer (HeldLease, textConnect(), etc.) was
327
+ // written against -- a stock record NEVER lacks a text-monitor port.
328
+ // acquirePortAndLaunch() above already fails the whole acquire before ever
329
+ // reaching this function when the second allocation fails, so a caller
330
+ // that lands here with `backend: "stock"` and no `remoteMonitorPort` is a
331
+ // defect in THIS module (a call site that bypassed that guarantee), not a
332
+ // state a stock record may legitimately carry -- throw by name rather
333
+ // than silently writing a record that violates it.
343
334
  // The fork case is real and unaffected: this check is stock-only.
344
335
  if (backend === "stock" && deps.remoteMonitorPort === undefined) {
345
- throw new Error("spawnAndRecordInstance: backend \"stock\" requires remoteMonitorPort (D-16) -- a stock launch that cannot bind a text-monitor port must fail the acquire before reaching this construction site, never write a portless stock record");
336
+ throw new Error("spawnAndRecordInstance: backend \"stock\" requires remoteMonitorPort -- a stock launch that cannot bind a text-monitor port must fail the acquire before reaching this construction site, never write a portless stock record");
346
337
  }
347
338
  const viceArgs = buildViceArgs(port, {
348
339
  backend,
349
340
  mcpHost: deps.mcpHost,
350
341
  binmonHost: deps.binmonHost,
351
342
  remoteMonitorPort: deps.remoteMonitorPort,
352
- // Phase 33, plan 33-06: the ONE place a launch's profile becomes argv.
353
- // The record built below mirrors the SAME value, so an instance's
354
- // recorded profile and its actual argv are written in one step and
355
- // cannot disagree.
343
+ // The ONE place a launch's profile becomes argv. The record built
344
+ // below mirrors the SAME value, so an instance's recorded profile and
345
+ // its actual argv are written in one step and cannot disagree.
356
346
  profile: deps.profile,
357
347
  });
358
348
  const log = deps.log ?? defaultLog;
359
- // I-1 rider (audit §4.4, 08.2-02-PLAN.md Task 2): production stock
360
- // launches used to set no scratch XDG_CONFIG_HOME and would read whatever
361
- // vicerc the operator's own $HOME already carried -- shared with the
362
- // operator's own VICE usage and with the fork build. For backend ===
363
- // "stock" only, compute a fresh, isolated config dir with mkdtempSync
364
- // (atomic creation, random suffix, 0700 permissions -- the primitive that
365
- // makes a collision or a symlink-swap into the operator's real config
366
- // unreachable) and pass it as a third options argument carrying `env`
367
- // only. Never `shell: true`: the existing array-form spawn(viceBin,
368
- // viceArgs) call avoids shell interpretation entirely and that property
369
- // must survive this widening.
349
+ // Production stock launches used to set no scratch XDG_CONFIG_HOME and
350
+ // would read whatever vicerc the operator's own $HOME already carried --
351
+ // shared with the operator's own VICE usage and with the fork build. For
352
+ // backend === "stock" only, compute a fresh, isolated config dir with
353
+ // mkdtempSync (atomic creation, random suffix, 0700 permissions -- the
354
+ // primitive that makes a collision or a symlink-swap into the operator's
355
+ // real config unreachable) and pass it as a third options argument
356
+ // carrying `env` only. Never `shell: true`: the existing array-form
357
+ // spawn(viceBin, viceArgs) call avoids shell interpretation entirely and
358
+ // that property must survive this widening.
370
359
  //
371
360
  // Scope boundary (do not remove this note): the production broker daemon
372
361
  // always supplies its own deps.spawn / deps.spawnFactory, so the widened
@@ -374,13 +363,12 @@ function spawnAndRecordInstance(reason, port, deps) {
374
363
  // function's job is only to COMPUTE the value at the one seam that should
375
364
  // own it; the forwarding to nodeSpawn() happens at three further hops --
376
365
  // makeLoggingSpawn() in vice-broker.mts, and withCrashSupervision()'s
377
- // wrapper body and launchSupervised()'s defaultRealSpawn in this file
378
- // (plan 41-05, folded todo: a FOURTH hop, the retired warm floor's own
379
- // inner stashingSpawn closure in vice-broker.mts, is REMOVED along with
380
- // the function that held it). All three now forward the options argument
381
- // (plan 08.2-06 closed them in this same phase, with a handleAcquire()
366
+ // wrapper body and launchSupervised()'s defaultRealSpawn in this file (a
367
+ // fourth hop, the retired warm floor's own inner stashingSpawn closure in
368
+ // vice-broker.mts, is REMOVED along with the function that held it). All
369
+ // three now forward the options argument, with a handleAcquire()
382
370
  // composition test that omits buildColdSpawnFactory so an injected stub
383
- // cannot fake the proof). If you add another spawn hop, it must forward
371
+ // cannot fake the proof. If you add another spawn hop, it must forward
384
372
  // options too, or production stock launches silently lose their config
385
373
  // isolation again.
386
374
  //
@@ -415,25 +403,25 @@ function spawnAndRecordInstance(reason, port, deps) {
415
403
  viceBin,
416
404
  viceArgs,
417
405
  dryRun: false,
418
- // Plan 41-03 (D-14): non-optional, defaulted to an empty map -- "no
419
- // claim on any channel" is an empty map, never an absent field. The
420
- // ONE place a fresh InstanceRecord is constructed, so this is the ONE
421
- // place this default is set.
406
+ // Non-optional, defaulted to an empty map -- "no claim on any channel"
407
+ // is an empty map, never an absent field. The ONE place a fresh
408
+ // InstanceRecord is constructed, so this is the ONE place this default
409
+ // is set.
422
410
  monitorClients: {},
423
- // Plan 41-05 (D-16): key omitted only on the FORK path now -- the guard
424
- // above already throws before this point for any stock call with no
411
+ // Key omitted only on the FORK path now -- the guard above already
412
+ // throws before this point for any stock call with no
425
413
  // remoteMonitorPort, so a stock record reaching this line always
426
414
  // supplies the key. "Absent" means fork, never "stock allocation
427
415
  // failed" (that state no longer exists).
428
416
  ...(deps.remoteMonitorPort === undefined ? {} : { remoteMonitorPort: deps.remoteMonitorPort }),
429
- // Phase 33, plan 33-06: same key-omitted-when-undefined idiom as
430
- // remoteMonitorPort directly above. An absent request must produce a
431
- // record with NO `profile` key at all -- not `profile: undefined` --
432
- // because "absent means profile-less" is the property a broker restarted
433
- // mid-phase relies on when it reads records written before this field
434
- // existed. A copy, not the caller's own object: the record outlives this
435
- // call and a caller mutating its profile afterwards must not silently
436
- // change what this instance claims it was launched with.
417
+ // Same key-omitted-when-undefined idiom as remoteMonitorPort directly
418
+ // above. An absent request must produce a record with NO `profile` key
419
+ // at all -- not `profile: undefined` -- because "absent means
420
+ // profile-less" is the property a broker restarted mid-phase relies on
421
+ // when it reads records written before this field existed. A copy, not
422
+ // the caller's own object: the record outlives this call and a caller
423
+ // mutating its profile afterwards must not silently change what this
424
+ // instance claims it was launched with.
437
425
  ...(deps.profile === undefined ? {} : { profile: { ...deps.profile } }),
438
426
  };
439
427
  deps.state.instances.set(port, record);
@@ -470,45 +458,45 @@ export function tryLaunchOne(reason, port, deps) {
470
458
  * allocate-a-port-then-launch sequence -- not merely the synchronous spawn
471
459
  * instant tryLaunchOne() alone guards. This closes a genuine race window
472
460
  * tryLaunchOne() cannot: nextFreePort()'s own port-in-use probe is
473
- * asynchronous (plan 02, C4 -- a real bind-and-release check), so two
474
- * overlapping callers could otherwise BOTH be told the SAME candidate port
475
- * is free before either commits it to state.instances -- a double-launch on
476
- * one port, silently overwriting the earlier record. The guard is checked
477
- * and set SYNCHRONOUSLY before the first `await`, exactly like
478
- * tryLaunchOne()'s own discipline, so a second concurrent call is refused
479
- * immediately (`launch_in_flight`) rather than racing on the allocation.
461
+ * asynchronous (a real bind-and-release check), so two overlapping callers
462
+ * could otherwise BOTH be told the SAME candidate port is free before
463
+ * either commits it to state.instances -- a double-launch on one port,
464
+ * silently overwriting the earlier record. The guard is checked and set
465
+ * SYNCHRONOUSLY before the first `await`, exactly like tryLaunchOne()'s own
466
+ * discipline, so a second concurrent call is refused immediately
467
+ * (`launch_in_flight`) rather than racing on the allocation.
480
468
  *
481
- * Plan 41-05 (folded todo): this guard's own reasoning OUTLIVED the warm
482
- * floor it was originally written alongside -- it exists because of the
483
- * 2026-08-01 triple-launch outage (three simultaneous x64sc launches: one
484
- * SEGV, one exit 1, one exit 0 at the identical spawn second) and is
485
- * regression-tested (CLAUDE.md), and that history has nothing to do with
486
- * whether a warm floor exists. Today the only caller of this function is the
487
- * cold-acquire arm (vice-broker.mts's handleAcquire(), via `serveAcquires()`
488
- * in runBrokerPass()); the overlap this guard closes is now TWO OR MORE
469
+ * This guard's own reasoning OUTLIVED the warm floor it was originally
470
+ * written alongside -- it exists because of the 2026-08-01 triple-launch
471
+ * outage (three simultaneous x64sc launches: one SEGV, one exit 1, one exit
472
+ * 0 at the identical spawn second) and is regression-tested (CLAUDE.md),
473
+ * and that history has nothing to do with whether a warm floor exists.
474
+ * Today the only caller of this function is the cold-acquire arm
475
+ * (vice-broker.mts's handleAcquire(), via `serveAcquires()` in
476
+ * runBrokerPass()); the overlap this guard closes is now TWO OR MORE
489
477
  * concurrent acquires -- e.g. two requests arriving over the TCP control
490
478
  * listener at nearly the same moment, or one arriving while an EARLIER
491
479
  * acquire's own launch is still resolving -- never a warming pass, which no
492
480
  * longer exists. This is also the function that restores vice-broker.sh's
493
481
  * own process_requests() throttle (its `in_flight` local): whatever launches
494
482
  * this broker ever attempts, they never overlap, matching the bash
495
- * original's declined-to-change behaviour (RESEARCH.md §A1/§C). D-07
496
- * (01.6.2.1-03-PLAN.md) layers non-preemptive PRIORITY on top of this same
497
- * "one at a time" guard, never replacing it, and the anti-pattern it names --
498
- * killing or relaunching preemptively to serve a newer request -- is likewise
499
- * unaffected by the floor's removal: this function still only ever refuses a
500
- * second concurrent caller (`launch_in_flight`), and never kills or preempts
501
- * whichever caller already holds the slot. Among multiple QUEUED acquires,
502
- * which one wins this slot NEXT, once it frees, falls out of the
503
- * arrival-ordered pending-acquire structure (broker-control.mts's D-08
504
- * mechanism) that requeues a refused acquire for the next pass -- not from
505
- * anything in this function. The refusal below logs which reason currently
506
- * holds the slot and which reason is waiting, so the decision is
507
- * reconstructable from the log after an incident. */
483
+ * original's declined-to-change behaviour. Non-preemptive launch PRIORITY
484
+ * layers on top of this same "one at a time" guard, never replacing it, and
485
+ * the anti-pattern it names -- killing or relaunching preemptively to serve
486
+ * a newer request -- is likewise unaffected by the floor's removal: this
487
+ * function still only ever refuses a second concurrent caller
488
+ * (`launch_in_flight`), and never kills or preempts whichever caller
489
+ * already holds the slot. Among multiple QUEUED acquires, which one wins
490
+ * this slot NEXT, once it frees, falls out of the arrival-ordered
491
+ * pending-acquire structure (broker-control.mts's own mechanism) that
492
+ * requeues a refused acquire for the next pass -- not from anything in this
493
+ * function. The refusal below logs which reason currently holds the slot
494
+ * and which reason is waiting, so the decision is reconstructable from the
495
+ * log after an incident. */
508
496
  export async function acquirePortAndLaunch(reason, deps) {
509
497
  const log = deps.log ?? defaultLog;
510
498
  if (inFlight) {
511
- log(`vice-broker: launch-slot decision -- ${inFlightReason ?? "unknown"} holds the slot; ${reason} waits (D-07)`);
499
+ log(`vice-broker: launch-slot decision -- ${inFlightReason ?? "unknown"} holds the slot; ${reason} waits`);
512
500
  return { ok: false, reason: "launch_in_flight" };
513
501
  }
514
502
  inFlight = true;
@@ -522,14 +510,14 @@ export async function acquirePortAndLaunch(reason, deps) {
522
510
  const supervisorDir = join(deps.stateDir, String(port));
523
511
  const epochFile = join(supervisorDir, "epoch.json");
524
512
  const spawn = deps.spawnFactory ? deps.spawnFactory(port) : deps.spawn;
525
- // Plan 03-04 (DIRECT-06, D-13): the second (`-remotemonitor`) port is
526
- // resolved HERE, still inside the single in_flight owner's own
527
- // try-block, immediately after the primary allocation succeeds -- both
528
- // awaits stay inside this SAME try, after the guard's synchronous
529
- // check-and-set above; neither is moved, duplicated, or awaited around
530
- // that guard. Only ever attempted for `backend === "stock"`, and only
531
- // when the caller actually provided the allocator -- every fork launch
532
- // and every pre-Phase-3 caller never reaches this branch at all.
513
+ // The second (`-remotemonitor`) port is resolved HERE, still inside the
514
+ // single in_flight owner's own try-block, immediately after the primary
515
+ // allocation succeeds -- both awaits stay inside this SAME try, after
516
+ // the guard's synchronous check-and-set above; neither is moved,
517
+ // duplicated, or awaited around that guard. Only ever attempted for
518
+ // `backend === "stock"`, and only when the caller actually provided the
519
+ // allocator -- every fork launch and every caller before this feature
520
+ // existed never reaches this branch at all.
533
521
  let remoteMonitorPort;
534
522
  if (deps.backend === "stock" && deps.allocateRemoteMonitorPort) {
535
523
  const remoteResult = await deps.allocateRemoteMonitorPort(deps.state, new Set([port]));
@@ -548,12 +536,12 @@ export async function acquirePortAndLaunch(reason, deps) {
548
536
  remoteMonitorPort = remoteResult.port;
549
537
  }
550
538
  else {
551
- // Plan 41-05 (D-16): FAIL, never degrade. Owner direction, verbatim:
552
- // "it should not be possible, vice must be started witht the text
553
- // channel." A stock launch that cannot bind a text-monitor port
554
- // fails the whole acquire -- no process is spawned. The PRIMARY port
555
- // allocated moments earlier is not yet in `state.instances` and was
556
- // never added to `state.blockedPorts` by this function (only
539
+ // FAIL, never degrade. Owner direction, verbatim: "it should not be
540
+ // possible, vice must be started witht the text channel." A stock
541
+ // launch that cannot bind a text-monitor port fails the whole
542
+ // acquire -- no process is spawned. The PRIMARY port allocated
543
+ // moments earlier is not yet in `state.instances` and was never
544
+ // added to `state.blockedPorts` by this function (only
557
545
  // `nextFreePort()`'s own in-use probe blocks a candidate, and that
558
546
  // never ran against the winning candidate) -- so it is already
559
547
  // allocatable again on the very next call with no further release
@@ -561,7 +549,7 @@ export async function acquirePortAndLaunch(reason, deps) {
561
549
  // it. This failure arm must NEVER call spawnAndRecordInstance() or
562
550
  // otherwise leave a port "spoken for" on the caller's behalf.
563
551
  log(`vice-broker: second (-remotemonitor) port allocation failed (${remoteResult.reason}) -- ` +
564
- `abandoning the stock launch; the text-monitor port is mandatory on every stock launch (D-16) and the acquire fails`);
552
+ `abandoning the stock launch; the text-monitor port is mandatory on every stock launch and the acquire fails`);
565
553
  return { ok: false, reason: "no_free_text_port" };
566
554
  }
567
555
  }
@@ -589,17 +577,17 @@ export async function acquirePortAndLaunch(reason, deps) {
589
577
  * deleting the record AND handing its second (`-remotemonitor`) port back to
590
578
  * the allocator in the same step.
591
579
  *
592
- * CR-02 (03-REVIEW.md): `acquirePortAndLaunch()` above adds every allocated
593
- * remote-monitor port to `state.blockedPorts`, and until this function existed
594
- * NOTHING ever removed one. `nextFreePort()` never reconsiders a blocked
595
- * candidate for the lifetime of the process, so every teardown of a stock
596
- * instance permanently consumed one more port out of the fixed
597
- * PORT_SCAN_CEILING window even though the OS port was free again the instant
598
- * the owning process exited -- a long-running broker (the explicit design goal
599
- * of an on-demand pool with crash supervision, per plan 41-05 launched
600
- * strictly on demand rather than kept warm) eventually exhausts its band and
601
- * answers `no_free_port` to ordinary launches purely
602
- * from routine churn, with no operator recourse short of a broker restart.
580
+ * `acquirePortAndLaunch()` above adds every allocated remote-monitor port to
581
+ * `state.blockedPorts`, and until this function existed NOTHING ever removed
582
+ * one. `nextFreePort()` never reconsiders a blocked candidate for the
583
+ * lifetime of the process, so every teardown of a stock instance permanently
584
+ * consumed one more port out of the fixed PORT_SCAN_CEILING window even
585
+ * though the OS port was free again the instant the owning process exited
586
+ * -- a long-running broker (the explicit design goal of an on-demand pool
587
+ * with crash supervision, launched strictly on demand rather than kept
588
+ * warm) eventually exhausts its band and answers `no_free_port` to ordinary
589
+ * launches purely from routine churn, with no operator recourse short of a
590
+ * broker restart.
603
591
  *
604
592
  * A RESPAWN is deliberately NOT a call site: the replacement instance keeps
605
593
  * BOTH the primary port and the remote-monitor port of the instance it
@@ -660,7 +648,7 @@ async function defaultHttpProbe(port, timeoutMs) {
660
648
  }
661
649
  }
662
650
  // ---------------------------------------------------------------------------
663
- // WR-01: the STOCK readiness route.
651
+ // The STOCK readiness route.
664
652
  //
665
653
  // probeReady() below used to POST http://127.0.0.1:<port>/mcp unconditionally
666
654
  // and require both "version" and "machine" in the body. On the stock backend
@@ -681,8 +669,9 @@ async function defaultHttpProbe(port, timeoutMs) {
681
669
  // probe-binmon.mjs. What is written here is the minimum a READINESS check needs:
682
670
  // one request header out, one response header in, four bytes checked.
683
671
  // ---------------------------------------------------------------------------
684
- /** Hand-copied from docs/phase0-binmon-findings.md §5 -- see the block comment
685
- * above for why these are not imported from stock-protocol.ts. */
672
+ /** Hand-copied from this project's own measured binary-monitor wire format
673
+ * (the same constants stock-protocol.ts defines) -- see the block comment
674
+ * above for why these are not imported from stock-protocol.ts directly. */
686
675
  const BINMON_STX = 0x02;
687
676
  const BINMON_API_VERSION = 0x02;
688
677
  const BINMON_REQUEST_HEADER_LEN = 11;
@@ -708,15 +697,15 @@ function binmonRequest(commandType, requestId) {
708
697
  * collides between the two, which is exactly why demux must key on it. */
709
698
  const BINMON_UNSOLICITED_REQUEST_ID = 0xffffffff;
710
699
  /**
711
- * WR-01: one PING (0x81) over the binary monitor, requiring a WELL-FORMED 0x81
700
+ * One PING (0x81) over the binary monitor, requiring a WELL-FORMED 0x81
712
701
  * reply -- STX, the expected api_version, response type 0x81, error code 0x00,
713
702
  * and this probe's own request id. A bare TCP accept is explicitly insufficient
714
703
  * here for exactly the reason probeReady()'s own comment gives for the HTTP
715
704
  * route: a C64 can accept a connection before it has finished booting.
716
705
  *
717
- * quick task 260818-obc (live-discovered): a NEW binmon connection ALWAYS
718
- * emits an unsolicited REGISTER_INFO (0x31) frame at request-id 0xffffffff
719
- * the instant it opens (CLAUDE.md's own Protocol constraint) -- BEFORE this
706
+ * A live-discovered defect: a NEW binmon connection ALWAYS emits an
707
+ * unsolicited REGISTER_INFO (0x31) frame at request-id 0xffffffff the
708
+ * instant it opens (CLAUDE.md's own Protocol constraint) -- BEFORE this
720
709
  * probe's own PING reply ever arrives. The naive "the first 12 bytes ARE the
721
710
  * reply" read this code used to do treated that event frame's OWN response-
722
711
  * type byte (0x31) as a malformed PING reply and answered `false` forever,
@@ -732,8 +721,8 @@ const BINMON_UNSOLICITED_REQUEST_ID = 0xffffffff;
732
721
  * ViceMonitorClient chief among them).
733
722
  *
734
723
  * Then EXIT (0xaa), unconditionally, before closing -- because the PING ITSELF
735
- * HALTS THE MACHINE. Any inbound byte does (docs/phase0-binmon-findings.md §4,
736
- * and CR-02, which fixed the same omission in the connect handshake). A
724
+ * HALTS THE MACHINE. Any inbound byte does, and this project's own connect
725
+ * handshake had the same omission and was fixed for the same reason. A
737
726
  * readiness probe that left every warm instance frozen would be a worse defect
738
727
  * than the one it fixes: the emulator would be "ready" and stopped.
739
728
  *
@@ -821,32 +810,29 @@ async function defaultBinmonProbe(port, timeoutMs) {
821
810
  });
822
811
  });
823
812
  }
824
- /** D-05, AS AMENDED BY P-05 -- this comment is the amendment's record, kept
825
- * in the exact place a three-branch description used to sit, per this
826
- * plan's own instruction that a code reader must meet the amendment here,
827
- * not merely in the plan text (`01.6.2.1-02-PLAN.md`) or the validation
828
- * ledger (`01.6.2-VALIDATION.md`, consolidated by plan 06).
813
+ /** This comment records why the readiness probe below looks the way it
814
+ * does, kept in the exact place a longer, three-branch description used to
815
+ * sit.
829
816
  *
830
- * D-05 (locked, `01.6.2-CONTEXT.md`) originally specified the probe as a
831
- * bare in-process TCP connect to the instance's own monitor port, with a
832
- * short timeout. The code that landed instead argued against that wording,
833
- * in its OWN comment: "a bare TCP accept is explicitly not sufficient (a
834
- * C64 can accept a connection before it has finished booting)" -- a
835
- * booting emulator promoted to ready on nothing more than an accepted
836
- * connection is exactly Defect 3's failure shape reappearing, here on the
837
- * plan-01 acquire hot path.
817
+ * The probe was originally specified as a bare in-process TCP connect to
818
+ * the instance's own monitor port, with a short timeout. The code that
819
+ * landed instead argued against that wording, in its OWN comment: "a bare
820
+ * TCP accept is explicitly not sufficient (a C64 can accept a connection
821
+ * before it has finished booting)" -- a booting emulator promoted to ready
822
+ * on nothing more than an accepted connection is exactly the kind of false
823
+ * positive this probe exists to prevent.
838
824
  *
839
- * P-05 amends D-05: the ping-shaped request body stays -- it is what
840
- * proves the emulator ANSWERS, not merely that a port is bound, which is
841
- * the whole difference between a liveness check and a readiness check. The
842
- * two OTHER branches the landed code carried (an external-command
843
- * mechanism, and a "neither mechanism available -> report ready
844
- * unconditionally" fallback) retire outright, per P-06: with no second
845
- * mechanism to prefer and no "no mechanism" state left to report, there is
846
- * no longer a pair of indistinguishable states (a deliberately-zero warm
847
- * floor and a broken host) for an operator to confuse in the logs. D-05's
848
- * own intent -- exactly one check, no external command, no ambiguity -- is
849
- * fully honoured by this collapse, not reversed by it.
825
+ * The ping-shaped request body stays -- it is what proves the emulator
826
+ * ANSWERS, not merely that a port is bound, which is the whole difference
827
+ * between a liveness check and a readiness check. Two other mechanisms the
828
+ * landed code originally carried (an external-command mechanism, and a
829
+ * "neither mechanism available -> report ready unconditionally" fallback)
830
+ * were retired outright: with no second mechanism to prefer and no "no
831
+ * mechanism" state left to report, there is no longer a pair of
832
+ * indistinguishable states (a deliberately-zero warm floor and a broken
833
+ * host) for an operator to confuse in the logs. The original intent --
834
+ * exactly one check, no external command, no ambiguity -- is fully
835
+ * honoured by this collapse, not reversed by it.
850
836
  *
851
837
  * No retry loop, deliberately: a still-booting instance simply fails THIS
852
838
  * pass and is re-probed on the next one (promoteLaunchingInstances()'s own
@@ -858,7 +844,7 @@ async function defaultBinmonProbe(port, timeoutMs) {
858
844
  export async function probeReady(port, deps = {}) {
859
845
  const timeoutS = Number(deps.probeTimeoutSEnv ?? process.env.VICE_BROKER_PROBE_TIMEOUT_S) || DEFAULT_PROBE_TIMEOUT_S;
860
846
  const timeoutMs = timeoutS * 1000;
861
- // WR-01: the route is chosen by the backend, exactly like buildViceArgs()'s
847
+ // The route is chosen by the backend, exactly like buildViceArgs()'s
862
848
  // own argv choice, and from the SAME threaded-down verdict. The fork arm below
863
849
  // is byte-identical to what this function always did, including the
864
850
  // omitted-backend default -- a fork deployment sees no behaviour change.
@@ -880,7 +866,7 @@ export async function probeReady(port, deps = {}) {
880
866
  export async function promoteLaunchingInstances(deps) {
881
867
  const log = deps.log ?? defaultLog;
882
868
  const now = deps.now ?? (() => Date.now());
883
- // WR-01: the DEFAULT probe follows this call's own backend, so a caller that
869
+ // The DEFAULT probe follows this call's own backend, so a caller that
884
870
  // threads `backend` and omits `probe` gets a matching readiness route
885
871
  // rather than an HTTP POST at a binary-monitor port. An explicitly
886
872
  // injected `probe` still wins, unchanged.
@@ -902,22 +888,21 @@ export async function promoteLaunchingInstances(deps) {
902
888
  * comment names the ordering as load-bearing: "the spare invariant is
903
889
  * always re-evaluated against the freshest possible grant/teardown
904
890
  * state"). The bash version's third concern, the grant sweep, does NOT
905
- * appear here -- it is one of criterion F's six retiring file-lease
906
- * mechanisms; the TCP connection itself is the lease (D-12). The
907
- * broker-instances.json projection write does not appear either, per D-24
908
- * (see broker-state.mts's own FINDING 2 comment). Takes plain callbacks
909
- * rather than the full BrokerState/deps shape so a test can inject two
910
- * instrumented no-op functions and assert call ORDER without needing a
911
- * real broker, a real port or a real launch.
891
+ * appear here -- it is one of several retiring file-lease mechanisms; the
892
+ * TCP connection itself is the lease. The broker-instances.json projection
893
+ * write does not appear either (see broker-state.mts's own FINDING 2
894
+ * comment). Takes plain callbacks rather than the full BrokerState/deps
895
+ * shape so a test can inject two instrumented no-op functions and assert
896
+ * call ORDER without needing a real broker, a real port or a real launch.
912
897
  *
913
- * Plan 41-05 (folded todo): the warm floor that D-07 (01.6.2.1-03-PLAN.md)
914
- * originally reasoned about here is GONE -- `promoteLaunching` never calls
898
+ * The warm floor that non-preemptive launch priority originally reasoned
899
+ * about here is GONE -- `promoteLaunching` never calls
915
900
  * acquirePortAndLaunch() and so never competes for the single in-flight
916
901
  * launch slot the way a warm-floor spare launch used to. `serveAcquires()`
917
902
  * (via its own drainPendingAcquires()) is now the ONLY caller in this pass
918
- * that ever launches anything, so D-07's original "which reason wins a
919
- * freed slot" question has nothing left to decide BETWEEN these two steps --
920
- * that reasoning still applies WITHIN the acquire arm itself (two overlapping
903
+ * that ever launches anything, so the original "which reason wins a freed
904
+ * slot" question has nothing left to decide BETWEEN these two steps -- that
905
+ * reasoning still applies WITHIN the acquire arm itself (two overlapping
921
906
  * acquires still resolve through the single in-flight owner
922
907
  * (acquirePortAndLaunch()'s own invariant comment), which this reordering
923
908
  * never weakens). What the fixed order still buys: promoting AFTER serving
@@ -932,14 +917,14 @@ export async function runBrokerPass(deps) {
932
917
  await deps.promoteLaunching();
933
918
  }
934
919
  // ===========================================================================
935
- // Per-child supervision (Plan 03, Task 2 -- C2/D-23): absorbs
936
- // resources/vice-supervisor.sh WHOLESALE. The respawn loop becomes an
937
- // exit-event handler installed on the spawned child; the backoff shape
938
- // (initial delay, doubling, ceiling), the crash-loop give-up (too many
939
- // crashes inside a window), and the per-instance boot/crash log are ported
940
- // exactly, per D-1's own configuration knobs -- VICE_RESTART_BACKOFF_S,
941
- // VICE_RESTART_BACKOFF_MAX_S, VICE_MAX_RESTARTS, VICE_CRASH_WINDOW_S all
942
- // keep their exact names and semantics.
920
+ // Per-child supervision: absorbs resources/vice-supervisor.sh WHOLESALE. The
921
+ // respawn loop becomes an exit-event handler installed on the spawned
922
+ // child; the backoff shape (initial delay, doubling, ceiling), the
923
+ // crash-loop give-up (too many crashes inside a window), and the
924
+ // per-instance boot/crash log are ported exactly, keeping the same
925
+ // configuration knobs -- VICE_RESTART_BACKOFF_S, VICE_RESTART_BACKOFF_MAX_S,
926
+ // VICE_MAX_RESTARTS, VICE_CRASH_WINDOW_S all keep their exact names and
927
+ // semantics.
943
928
  // ===========================================================================
944
929
  function resolveMs(envVar, defaultSeconds, override) {
945
930
  if (typeof override === "number")
@@ -996,12 +981,11 @@ async function handleExit(reason, port, deps) {
996
981
  return;
997
982
  }
998
983
  const log = deps.log ?? defaultLog;
999
- // Plan 05 (BROK-02/PROTO-08), promoted to a per-channel map by plan 41-03
1000
- // (D-14): the process behind this instance's monitor sockets has just
1001
- // exited, by every path this function can take (crash, recycle, or a
1002
- // deliberate teardown) -- clear EVERY channel's ownership record HERE,
1003
- // once, before any of those paths branch, so a client that died without
1004
- // releasing can never hold this lock forever on any channel. Redundant
984
+ // The process behind this instance's monitor sockets has just exited, by
985
+ // every path this function can take (crash, recycle, or a deliberate
986
+ // teardown) -- clear EVERY channel's ownership record HERE, once, before
987
+ // any of those paths branch, so a client that died without releasing can
988
+ // never hold this lock forever on any channel. Redundant
1005
989
  // with the respawn/delete paths below (a fresh InstanceRecord never
1006
990
  // carries this forward; a deleted one has no field to carry), but
1007
991
  // explicit for the same reason broker-state.mts's own header comment
@@ -1024,20 +1008,19 @@ async function handleExit(reason, port, deps) {
1024
1008
  const preKillState = record.state;
1025
1009
  const preKillCrashTimes = record.crashTimes ?? [];
1026
1010
  const preKillBackoffMs = record.backoffMs ?? resolveMs("VICE_RESTART_BACKOFF_S", 3, deps.initialBackoffMs);
1027
- // CR-02 (03-REVIEW.md): the second (`-remotemonitor`) port is carried
1028
- // forward across the replacement exactly like the primary port is --
1029
- // captured BEFORE launchSupervised() overwrites this port's map entry
1030
- // with a brand new record, for the same reason the three values above
1031
- // are.
1011
+ // The second (`-remotemonitor`) port is carried forward across the
1012
+ // replacement exactly like the primary port is -- captured BEFORE
1013
+ // launchSupervised() overwrites this port's map entry with a brand
1014
+ // new record, for the same reason the three values above are.
1032
1015
  const preKillRemoteMonitorPort = record.remoteMonitorPort;
1033
- // Phase 33, plan 33-06 (D-16, T-33-24): the launch PROFILE is carried
1034
- // forward for exactly the reason CR-02 carries the remote-monitor port
1035
- // forward, and the failure it prevents is sharper. Without this, a
1036
- // recycled `{warp:true}` instance would come back UNWARPED while its
1037
- // fresh record still claimed `profile:{warp:true}` -- after which
1038
- // profileEligible() (vice-broker.mts) would happily hand that instance
1039
- // to the next warp request. That is precisely the undetectable lie
1040
- // D-16 exists to structurally exclude, reintroduced one respawn later.
1016
+ // The launch PROFILE is carried forward for exactly the reason the
1017
+ // remote-monitor port is carried forward above, and the failure it
1018
+ // prevents is sharper. Without this, a recycled `{warp:true}`
1019
+ // instance would come back UNWARPED while its fresh record still
1020
+ // claimed `profile:{warp:true}` -- after which profileEligible()
1021
+ // (vice-broker.mts) would happily hand that instance to the next warp
1022
+ // request. That is precisely the undetectable lie this carry-forward
1023
+ // exists to structurally exclude, reintroduced one respawn later.
1041
1024
  // Captured BEFORE launchSupervised() overwrites this port's map entry
1042
1025
  // with a brand new record, same as the four values above.
1043
1026
  const preKillProfile = record.profile;
@@ -1061,7 +1044,7 @@ async function handleExit(reason, port, deps) {
1061
1044
  deps.onOutcome?.("recycled", port);
1062
1045
  return;
1063
1046
  }
1064
- // CR-02: a deliberate teardown is the END of this instance -- its
1047
+ // A deliberate teardown is the END of this instance -- its
1065
1048
  // remote-monitor port must go back to the allocator with it.
1066
1049
  deleteInstanceRecord(deps.state, port);
1067
1050
  deps.onOutcome?.("deliberate_teardown", port);
@@ -1075,7 +1058,7 @@ async function handleExit(reason, port, deps) {
1075
1058
  if (crashTimes.length >= maxRestarts) {
1076
1059
  log(`vice-broker: giving up on port ${port} after ${crashTimes.length} crashes within ${crashWindowMs}ms -- ` +
1077
1060
  `this is not a transient crash; check VICE_ARGS and whether the port is already bound`);
1078
- // CR-02: giving up is likewise terminal for this instance -- release its
1061
+ // Giving up is likewise terminal for this instance -- release its
1079
1062
  // remote-monitor port rather than leaking it out of the allocation band.
1080
1063
  deleteInstanceRecord(deps.state, port);
1081
1064
  deps.onOutcome?.("given_up", port);
@@ -1086,13 +1069,13 @@ async function handleExit(reason, port, deps) {
1086
1069
  await sleepMs(currentBackoffMs);
1087
1070
  const maxBackoffMs = resolveMs("VICE_RESTART_BACKOFF_MAX_S", 30, deps.maxBackoffMs);
1088
1071
  const nextBackoffMs = Math.min(currentBackoffMs * 2, maxBackoffMs);
1089
- // CR-02: same carry-forward as the recycle branch above -- a crash must not
1072
+ // Same carry-forward as the recycle branch above -- a crash must not
1090
1073
  // silently strip `-remotemonitor` (and its InstanceRecord field) off the
1091
- // replacement, which is what made D-13's "the instance record carries it"
1092
- // stop being true the first time an instance was replaced.
1093
- // Phase 33, plan 33-06: same carry-forward as the recycle branch above --
1094
- // an unexplained crash must not silently strip `-warp`/`-console` off the
1095
- // replacement while leaving the record claiming them (D-16, T-33-24).
1074
+ // replacement, which would otherwise make the instance record's claim to
1075
+ // carry that field stop being true the first time an instance was
1076
+ // replaced. Same reasoning applies to the launch profile -- an
1077
+ // unexplained crash must not silently strip `-warp`/`-console` off the
1078
+ // replacement while leaving the record claiming them.
1096
1079
  const respawned = launchSupervised(reason, port, deps, crashTimes, nextBackoffMs, record.remoteMonitorPort, record.profile);
1097
1080
  deps.onOutcome?.(respawned ? "respawned" : "given_up", port);
1098
1081
  }
@@ -1118,12 +1101,11 @@ async function handleExit(reason, port, deps) {
1118
1101
  * a second inline listener, is what keeps the "exactly one installation
1119
1102
  * point" invariant a structural gate (broker-launch.test.ts) can hold. */
1120
1103
  export function withCrashSupervision(reason, port, baseSpawn, deps) {
1121
- // I-1 rider (08.2-06-PLAN.md, Task 1): forwards a third options argument
1122
- // in the BODY, not just the type -- this is the hop that matters most,
1123
- // because it wraps every real launch path (cold acquire and every
1124
- // respawn -- plan 41-05 retires the warm floor, the third path this
1125
- // comment used to name). A type-only widening would still silently drop a
1126
- // caller's options at this call site.
1104
+ // Forwards a third options argument in the BODY, not just the type --
1105
+ // this is the hop that matters most, because it wraps every real launch
1106
+ // path (cold acquire and every respawn -- a warm floor used to be a
1107
+ // third path here and has since been retired). A type-only widening
1108
+ // would still silently drop a caller's options at this call site.
1127
1109
  return (cmd, args, options) => {
1128
1110
  const child = baseSpawn(cmd, args, options);
1129
1111
  child.once("exit", () => {
@@ -1133,7 +1115,7 @@ export function withCrashSupervision(reason, port, baseSpawn, deps) {
1133
1115
  };
1134
1116
  }
1135
1117
  /** Launches (or relaunches) a supervised instance: spawns through
1136
- * tryLaunchOne() (the SAME single guarded primitive plan 02 established --
1118
+ * tryLaunchOne() (the SAME single guarded primitive established above --
1137
1119
  * "spawn again through the SAME single guarded launch function", never a
1138
1120
  * second, parallel spawn path), writes the per-instance boot/crash log at
1139
1121
  * the path shape the retiring supervisor used (a `logs/` directory under
@@ -1148,26 +1130,26 @@ export function withCrashSupervision(reason, port, baseSpawn, deps) {
1148
1130
  * fact that spawnAndRecordInstance() creates a BRAND NEW InstanceRecord
1149
1131
  * object on every launch, replacing the old one at the same port key.
1150
1132
  *
1151
- * CR-02 (03-REVIEW.md): `remoteMonitorPort` is threaded the SAME way and for
1152
- * the same reason -- it belongs to the instance, not to a single spawn of it.
1153
- * The replacement reuses the port the crashed/recycled process just vacated
1154
- * (already reserved in `state.blockedPorts`, so nothing else can have taken it
1133
+ * `remoteMonitorPort` is threaded the SAME way and for the same reason --
1134
+ * it belongs to the instance, not to a single spawn of it. The replacement
1135
+ * reuses the port the crashed/recycled process just vacated (already
1136
+ * reserved in `state.blockedPorts`, so nothing else can have taken it
1155
1137
  * meanwhile), exactly as it reuses the primary `port` argument; this function
1156
1138
  * stays fully synchronous and never allocates. `undefined` is the correct
1157
1139
  * value for a FIRST launch through superviseChild() and for every fork launch,
1158
1140
  * which is why the parameter is optional.
1159
1141
  *
1160
- * Phase 33, plan 33-06 (D-16): `profile` is threaded the SAME way and for a
1161
- * sharper version of the same reason -- it belongs to the instance, not to a
1162
- * single spawn of it, and warp is fixed at spawn (there is no runtime
1163
- * `WarpMode` resource on stock at all). A replacement that dropped it would
1164
- * come back unwarped while its record still claimed warp, which is exactly the
1165
- * mismatch-between-grant-and-request that D-16's eligibility rule exists to
1142
+ * `profile` is threaded the SAME way and for a sharper version of the same
1143
+ * reason -- it belongs to the instance, not to a single spawn of it, and
1144
+ * warp is fixed at spawn (there is no runtime `WarpMode` resource on stock
1145
+ * at all). A replacement that dropped it would come back unwarped while
1146
+ * its record still claimed warp, which is exactly the
1147
+ * mismatch-between-grant-and-request that this carry-forward exists to
1166
1148
  * make impossible. `undefined` is correct for a FIRST launch through
1167
1149
  * superviseChild() -- production has no profile-less first-launch call site
1168
- * of its own left after plan 41-05 retires the warm floor, but this
1169
- * module's own unit tests still drive one directly -- and for every fork
1170
- * launch, which is why this parameter is optional too. */
1150
+ * of its own left now that the warm floor is retired, but this module's
1151
+ * own unit tests still drive one directly -- and for every fork launch,
1152
+ * which is why this parameter is optional too. */
1171
1153
  function launchSupervised(reason, port, deps, crashTimes, backoffMs, remoteMonitorPort, profile) {
1172
1154
  const supervisorDir = join(deps.stateDir, String(port));
1173
1155
  const epochFile = deps.epoch.epochPathFor(deps.stateDir, port);
@@ -1184,15 +1166,15 @@ function launchSupervised(reason, port, deps, crashTimes, backoffMs, remoteMonit
1184
1166
  const logFileName = `${basename(viceBin)}-${Date.now()}-e${epoch}.log`;
1185
1167
  const logPath = join(logDir, logFileName);
1186
1168
  const logRelPath = `logs/${logFileName}`;
1187
- // I-1 rider (08.2-06-PLAN.md, Task 1): forwards a third options argument
1188
- // and MERGES it with the per-instance log stdio -- caller options
1189
- // spread FIRST, `stdio` set LAST, so the per-instance log fd always
1190
- // wins. Never the other order: a caller-supplied `stdio` would silently
1191
- // redirect a crash-respawn's output away from the log file the epoch
1192
- // record names, and the forensic per-instance log (D-23) would point at
1193
- // a file that received nothing. Without this fix, a stock instance that
1194
- // crashes and respawns comes back reading the operator's real `vicerc`
1195
- // even though its original launch was isolated.
1169
+ // Forwards a third options argument and MERGES it with the per-instance
1170
+ // log stdio -- caller options spread FIRST, `stdio` set LAST, so the
1171
+ // per-instance log fd always wins. Never the other order: a
1172
+ // caller-supplied `stdio` would silently redirect a crash-respawn's
1173
+ // output away from the log file the epoch record names, and the
1174
+ // forensic per-instance log would point at a file that received nothing.
1175
+ // Without this fix, a stock instance that crashes and respawns comes back
1176
+ // reading the operator's real `vicerc` even though its original launch
1177
+ // was isolated.
1196
1178
  const defaultRealSpawn = (cmd, args, options) => {
1197
1179
  const fd = openSync(logPath, "a");
1198
1180
  return nodeSpawn(cmd, args, { ...options, stdio: ["ignore", fd, fd] });
@@ -1249,16 +1231,16 @@ function launchSupervised(reason, port, deps, crashTimes, backoffMs, remoteMonit
1249
1231
  * mirroring resources/vice-supervisor.sh's own respawn loop but expressed
1250
1232
  * as an event-loop exit handler instead of a `while true` poll.
1251
1233
  *
1252
- * Plan 41-05 (D-16): `remoteMonitorPort` is an OPTIONAL fourth parameter,
1253
- * threaded straight through to launchSupervised() exactly like every other
1254
- * optional trailing parameter in this file. A
1255
- * `backend: "stock"` caller MUST supply it: spawnAndRecordInstance()'s own
1256
- * construction-site assertion (D-16) throws otherwise, since this function
1257
- * is a genuine first-launch call site, not merely a respawn. This is not a
1258
- * production stock first-launch path today (only acquirePortAndLaunch() is)
1259
- * -- it exists for this module's own unit tests to drive a supervised first
1260
- * launch directly, and the parameter exists so a stock test case can do so
1261
- * without violating the same guarantee production code enforces. */
1234
+ * `remoteMonitorPort` is an OPTIONAL fourth parameter, threaded straight
1235
+ * through to launchSupervised() exactly like every other optional trailing
1236
+ * parameter in this file. A `backend: "stock"` caller MUST supply it:
1237
+ * spawnAndRecordInstance()'s own construction-site assertion throws
1238
+ * otherwise, since this function is a genuine first-launch call site, not
1239
+ * merely a respawn. This is not a production stock first-launch path today
1240
+ * (only acquirePortAndLaunch() is) -- it exists for this module's own unit
1241
+ * tests to drive a supervised first launch directly, and the parameter
1242
+ * exists so a stock test case can do so without violating the same
1243
+ * guarantee production code enforces. */
1262
1244
  export function superviseChild(reason, port, deps, remoteMonitorPort) {
1263
1245
  const initialBackoffMs = resolveMs("VICE_RESTART_BACKOFF_S", 3, deps.initialBackoffMs);
1264
1246
  return launchSupervised(reason, port, deps, [], initialBackoffMs, remoteMonitorPort);