@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,8 +6,8 @@
6
6
  // rebuild.
7
7
  // vice-broker.mts
8
8
  //
9
- // The long-lived host broker entry point (Phase 01.6.2). Extends the Phase
10
- // 01.6 tracer in place rather than replacing it: parseArgs(),
9
+ // The long-lived host broker entry point. Extends an earlier write-once
10
+ // tracer script in place rather than replacing it: parseArgs(),
11
11
  // readBrokerRecordMaybe() and the atomic tmp-sibling-then-rename write
12
12
  // discipline all survive; main() grows a real control listener, a
13
13
  // heartbeat and a real acquire/release path spawning a real child.
@@ -29,23 +29,26 @@ import { join, basename, resolve as resolvePath } from "node:path";
29
29
  import { fileURLToPath } from "node:url";
30
30
  import { spawn as nodeSpawn } from "node:child_process";
31
31
  import { containerGuardReport, containerGuardEnforce } from "./container-guard.mjs";
32
- // Plan 41-05 (folded todo): countReady/countTotal/countLaunching are DROPPED
33
- // from this import -- they were used only as maintainWarmFloorForRealBroker()'s
34
- // own deps for the now-retired maintainWarmFloor(), passed through by
35
- // shorthand property (`countReady,` etc.), never called directly in this
36
- // file. atCapacity() is the one survivor actually called here (its own
37
- // cold-launch-arm gate, below).
32
+ // countReady/countTotal/countLaunching are DROPPED from this import -- they
33
+ // were used only as maintainWarmFloorForRealBroker()'s own deps for the
34
+ // now-retired maintainWarmFloor() (the warm floor itself was retired once
35
+ // the connection became the lease, with no separate expiry left to
36
+ // maintain), passed through by shorthand property (`countReady,` etc.),
37
+ // never called directly in this file. atCapacity() is the one survivor
38
+ // actually called here (its own cold-launch-arm gate, below).
38
39
  import { createBrokerState, nextFreePort, atCapacity, resolveBasePort, clearMonitorClient, } from "./broker-state.mjs";
39
40
  import { acquirePortAndLaunch, deleteInstanceRecord,
40
- // Plan 41-05 (folded todo): replaces maintainWarmFloor -- the warm floor
41
- // itself is retired; this is ONLY the launching -> ready promotion sweep
42
- // the floor used to carry as its own step 1.
41
+ // Replaces maintainWarmFloor -- the warm floor itself is retired (the
42
+ // connection is the lease now, so there is no separate floor left to
43
+ // keep warm); this is ONLY the launching -> ready promotion sweep the
44
+ // floor used to carry as its own step 1.
43
45
  promoteLaunchingInstances, probeReady, runBrokerPass, withCrashSupervision, } from "./broker-launch.mjs";
44
- // Plan 02-07: resolvedBackend() resolves the emulator binary's identity --
45
- // ViceBackend's own definition lives in backend-detect.mts too (narrowed to
46
- // a single literal by FORKRM-01, plan 52-06), so broker-launch.mjs's own
47
- // (type-only) re-import of it and this file's VALUE import both name the
48
- // same one home. A real value import is safe here (unlike inside
46
+ // resolvedBackend() resolves the emulator binary's identity -- ViceBackend's
47
+ // own definition lives in backend-detect.mts too (narrowed to a single
48
+ // literal now that the fork backend has been removed entirely), so
49
+ // broker-launch.mjs's own (type-only) re-import of it and this file's VALUE
50
+ // import both name the same one home. A real value import is safe here
51
+ // (unlike inside
49
52
  // broker-launch.mts) because vice-broker.mts is ALWAYS run from its own
50
53
  // compiled resources/ form -- both modules are compiled together in the
51
54
  // same build.ts pass, so "./backend-detect.mjs" always exists as a real
@@ -53,19 +56,18 @@ promoteLaunchingInstances, probeReady, runBrokerPass, withCrashSupervision, } fr
53
56
  import { resolvedBackend } from "./backend-detect.mjs";
54
57
  import { verifiedKill, registerShutdownHandlers, startupBanner, reapOrphanedInstances } from "./broker-kill.mjs";
55
58
  import { writeEpochRecord, epochPathFor, nextEpochFor, instanceLogDirFor } from "./broker-epoch.mjs";
56
- // Phase 34, plan 34-01 (SEAM-01): a VALUE import of the host-tool executor --
57
- // safe here for the SAME reason every other sibling value import above is:
59
+ // A VALUE import of the host-tool executor -- safe here for the SAME reason
60
+ // every other sibling value import above is:
58
61
  // this file is ALWAYS run from its own compiled resources/ form, and
59
62
  // "./host-tool.mjs" is compiled into that same directory by the same build.ts
60
63
  // pass (host-tool.mts is added to HOST_BOUND_ARTIFACTS/tsconfig.build.json's
61
64
  // include[] in this same commit).
62
65
  import { runHostTool } from "./host-tool.mjs";
63
- // Gap G-40-1, requirement R2 (plan 40-09): a VALUE import of the same
64
- // handle-minting function for the SAME reason as the host-tool.mjs import
65
- // immediately above -- this file is always run from its own compiled
66
- // resources/ form, and "./ghidra-project.mjs" is compiled into that same
67
- // directory by the same build.ts pass (both source and target are already
68
- // listed in HOST_BOUND_ARTIFACTS, since plan 40-08 landed the module).
66
+ // A VALUE import of the same handle-minting function for the SAME reason as
67
+ // the host-tool.mjs import immediately above -- this file is always run
68
+ // from its own compiled resources/ form, and "./ghidra-project.mjs" is
69
+ // compiled into that same directory by the same build.ts pass (both source
70
+ // and target are already listed in HOST_BOUND_ARTIFACTS).
69
71
  import { ensureGhidraRunsHandle } from "./ghidra-project.mjs";
70
72
  import { startControlListener, newControlToken, drainPendingAcquires, resolveControlPort, } from "./broker-control.mjs";
71
73
  const USAGE = "usage: vice-broker.mjs --repo-root <path> [--state-dir <path>] [--check-container] [--dry-run]";
@@ -73,8 +75,8 @@ const USAGE = "usage: vice-broker.mjs --repo-root <path> [--state-dir <path>] [-
73
75
  * container guard needs no paths at all, matching the bash launcher's own
74
76
  * `--check-container` handling (answered before any path resolution).
75
77
  * `--state-dir` defaults to VICE_POOL_DIR from the environment when set,
76
- * otherwise `.c64-re-tools/supervisor` under the repo root (moved 2026-09-08,
77
- * D-33 -- was `.vice-supervisor`; the three-tier chain itself -- explicit
78
+ * otherwise `.c64-re-tools/supervisor` under the repo root (moved 2026-09-08
79
+ * from `.vice-supervisor`; the three-tier chain itself -- explicit
78
80
  * `--state-dir`, then `VICE_POOL_DIR`, then this default -- is unchanged,
79
81
  * only the default's location moved). This module is host-bound and compiled
80
82
  * by `build.ts`, so it must not import the container-side `repo-root.ts`;
@@ -108,10 +110,9 @@ export function parseArgs(argv) {
108
110
  (repoRoot ? join(repoRoot, ".c64-re-tools", "supervisor") : join(".c64-re-tools", "supervisor"));
109
111
  return { repoRoot: repoRoot ?? "", stateDir: resolvedStateDir, checkContainer, dryRun };
110
112
  }
111
- /** The deployed JavaScript broker artifact's own name -- D-26's entire
112
- * point: this field used to read "vice-broker.sh" (the retiring bash
113
- * daemon), which was false the moment a real TypeScript broker existed.
114
- * It now names itself. */
113
+ /** The deployed JavaScript broker artifact's own name. This field used to
114
+ * read "vice-broker.sh" (the retiring bash daemon), which was false the
115
+ * moment a real TypeScript broker existed. It now names itself. */
115
116
  export const WRITTEN_BY = "vice-broker.mjs";
116
117
  // ---------------------------------------------------------------------------
117
118
  // Small, locally-duplicated env-var reader (plan 05) -- the SAME pattern
@@ -123,11 +124,11 @@ export const WRITTEN_BY = "vice-broker.mjs";
123
124
  // env-var read this file can duplicate exactly as cheaply). Mirrors
124
125
  // broker-launch.mts's own default precisely (VICE_BROKER_MAX/16) so
125
126
  // broker.json's config echo and host_state's own answer can never disagree
126
- // with what atCapacity() itself actually enforces. Plan 41-05 (folded todo):
127
- // this used to be a PAIR with resolveWarmFloorForRecord() (VICE_BROKER_WARM_
128
- // FLOOR/1), kept in lockstep with broker-launch.mts's own matching pair
129
- // (01.6.2.1-03-PLAN.md, D-06) so the two numbers could never disagree. The
130
- // warm-floor half of that pair is RETIRED along with the floor itself -- the
127
+ // with what atCapacity() itself actually enforces. This used to be a PAIR
128
+ // with resolveWarmFloorForRecord() (VICE_BROKER_WARM_FLOOR/1), kept in
129
+ // lockstep with broker-launch.mts's own matching pair so the two numbers
130
+ // could never disagree. The warm-floor half of that pair is RETIRED along
131
+ // with the floor itself -- the
131
132
  // ceiling's own default (16) is untouched, since it is a separate concern
132
133
  // (VICE_BROKER_MAX / atCapacity()) this plan does not touch.
133
134
  // ---------------------------------------------------------------------------
@@ -203,21 +204,21 @@ function writeBrokerRecordFile(stateDir, record) {
203
204
  return finalPath;
204
205
  }
205
206
  /** Builds a spawn function that redirects the child's stdout/stderr into a
206
- * FRESH per-launch log file under logDir (D-23: per-instance boot/crash
207
- * logs survive under .c64-re-tools/supervisor/<port>/logs/, same paths, same
207
+ * FRESH per-launch log file under logDir (so per-instance boot/crash logs
208
+ * survive under .c64-re-tools/supervisor/<port>/logs/, same paths, same
208
209
  * format as the retiring bash supervisor), returning both the spawn
209
210
  * closure and the log's path relative to supervisorDir (the epoch
210
211
  * record's own `log` field). Shared by both launch paths -- a cold
211
212
  * acquire and warm-floor maintenance -- so there is exactly one place that
212
213
  * opens a launch log fd.
213
214
  *
214
- * I-1 rider (08.2-06-PLAN.md, Task 2): the returned `spawn` now also
215
- * forwards a caller options object (audit item I-1), MERGING it into the
216
- * object handed to nodeSpawn() -- caller options spread FIRST, `stdio` set
217
- * LAST, so the launch log fd always wins over any caller-supplied `stdio`.
218
- * Merging in the other order would silently redirect a launch's output
219
- * away from the per-instance log file the epoch record names, breaking
220
- * D-23's forensic logs while appearing to work. */
215
+ * The returned `spawn` now also forwards a caller options object, MERGING
216
+ * it into the object handed to nodeSpawn() -- caller options spread FIRST,
217
+ * `stdio` set LAST, so the launch log fd always wins over any
218
+ * caller-supplied `stdio`. Merging in the other order would silently
219
+ * redirect a launch's output away from the per-instance log file the
220
+ * epoch record names, breaking the per-instance forensic logs while
221
+ * appearing to work. */
221
222
  function makeLoggingSpawn(logDir) {
222
223
  mkdirSync(logDir, { recursive: true });
223
224
  const viceBinForLog = basename(process.env.VICE_BIN ?? "x64sc");
@@ -229,8 +230,9 @@ function makeLoggingSpawn(logDir) {
229
230
  };
230
231
  }
231
232
  /** Writes the epoch record for a just-launched instance -- shared by both
232
- * launch paths so D-04's contract (format, location, atomic-write
233
- * discipline, all unchanged -- only the writer moves) is discharged from
233
+ * launch paths so the epoch record's own contract (format, location,
234
+ * atomic-write discipline, all unchanged -- only the writer moves) is
235
+ * discharged from
234
236
  * exactly one place regardless of WHY the instance was launched. A
235
237
  * granted instance and a still-warm instance are equally real processes; both
236
238
  * need a real epoch.json the moment they exist, or plan 04's grant-time
@@ -259,19 +261,20 @@ function writeEpochForLaunch(record, logRelPath) {
259
261
  }
260
262
  /** Builds the supervision dependency object for withCrashSupervision(),
261
263
  * once per launch, so the real launch path (handleAcquire's own cold arm,
262
- * here -- plan 41-05 retires the second real launch path this comment used
263
- * to name, the warm floor) passes a structurally identical SuperviseChildDeps
264
- * object into the shared wrapper. Deliberately does
265
- * NOT set spawnFactory: on a respawn, launchSupervised() (broker-launch.mts)
266
- * derives its own per-instance log path from instanceLogDirFor and names
267
- * that same path in the epoch record it writes -- supplying a competing
268
- * spawn factory here would produce two log files per respawn with the
269
- * epoch record naming the wrong one. Leaving it unset means a respawn's
270
- * output lands in the supervision module's own log file under the same
271
- * per-instance logs directory D-23 requires, and the epoch record names
272
- * the file that actually received the output.
264
+ * here -- the second real launch path this comment used to name, the warm
265
+ * floor, was retired once the connection became the lease) passes a
266
+ * structurally identical SuperviseChildDeps object into the shared wrapper.
267
+ * Deliberately does NOT set spawnFactory: on a respawn, launchSupervised()
268
+ * (broker-launch.mts) derives its own per-instance log path from
269
+ * instanceLogDirFor and names that same path in the epoch record it writes
270
+ * -- supplying a competing spawn factory here would produce two log files
271
+ * per respawn with the epoch record naming the wrong one. Leaving it unset
272
+ * means a respawn's output lands in the supervision module's own log file
273
+ * under the same per-instance logs directory the epoch record already
274
+ * requires, and the epoch record names the file that actually received the
275
+ * output.
273
276
  *
274
- * CR-01 (03-REVIEW.md): `backend` is a REQUIRED positional parameter, not an
277
+ * `backend` is a REQUIRED positional parameter, not an
275
278
  * optional field a call site may quietly omit. Before this, both real call
276
279
  * sites built their deps here WITHOUT it, so `spawnAndRecordInstance()`'s own
277
280
  * unset-parameter default silently took over the moment crash supervision
@@ -301,8 +304,8 @@ function superviseDepsFor(stateDir, state, backend, binmonHost) {
301
304
  /** Exported ONLY so a test can install withCrashSupervision() through the
302
305
  * REAL deps object this module actually uses in production, rather than a
303
306
  * hand-built SuperviseChildDeps that can (and did) diverge from it -- the
304
- * exact blind spot CR-01 (03-REVIEW.md) lived in: broker-launch.test.ts's own
305
- * respawn/recycle tests each construct their deps inline and therefore pass
307
+ * exact blind spot the backend-argv bug above lived in: broker-launch.test.ts's
308
+ * own respawn/recycle tests each construct their deps inline and therefore pass
306
309
  * `backend: "stock"` directly, so the production builder's missing field was
307
310
  * invisible to the whole suite. Same discipline as broker-kill.mts's
308
311
  * `_HANDLED_SIGNALS`: an underscore-prefixed alias, never called by any
@@ -321,8 +324,7 @@ function markDeliberateDeath(instance, respawnAfterKill) {
321
324
  instance.respawnAfterKill = respawnAfterKill;
322
325
  }
323
326
  // ---------------------------------------------------------------------------
324
- // Phase 33, plan 33-06 (REPRO-05, D-16, T-33-23/T-33-24): the warm-instance
325
- // PROFILE-ELIGIBILITY rule.
327
+ // THE WARM-INSTANCE PROFILE-ELIGIBILITY RULE.
326
328
  //
327
329
  // THE DECISION, stated out loud because two of the three available answers
328
330
  // are wrong in ways the CALLER CANNOT DETECT:
@@ -332,7 +334,7 @@ function markDeliberateDeath(instance, respawnAfterKill) {
332
334
  // - Serve the request with the mismatched instance -> the caller asked for
333
335
  // warp, got an unwarped machine, and received a confident grant. The knob
334
336
  // is a lie and nothing in the response says so.
335
- // - D-16, what this implements: the mismatched instance is INELIGIBLE. The
337
+ // - What this implements: the mismatched instance is INELIGIBLE. The
336
338
  // walk skips it and the acquire falls through to the cold arm, which
337
339
  // launches a DEDICATED instance for that grant.
338
340
  //
@@ -364,34 +366,32 @@ export function profileEligible(record, requested) {
364
366
  /** Walks `state.instances` for probe-live `ready` candidates, in iteration
365
367
  * order, and returns the first that answers a grant-time re-probe (P-02) --
366
368
  * or `null` once every candidate has been tried and none answered, letting
367
- * the caller fall through to a cold launch (P-03). Regardless of
368
- * `record.reason`: per D-07, a waiting request takes an instance whichever
369
- * reason booted it, so a warm-floor instance and a not-yet-granted instance are
370
- * equally eligible. Kill-never-recycle needs no separate guard here --
369
+ * the caller fall through to a cold launch. Regardless of `record.reason`:
370
+ * a waiting request takes an instance whichever reason booted it, so a
371
+ * warm-floor instance and a not-yet-granted instance are equally eligible.
372
+ * Kill-never-recycle needs no separate guard here --
371
373
  * handleRelease() below already deletes a released instance's record
372
374
  * outright, so a released instance is structurally absent from
373
375
  * `state.instances` and can never be a candidate.
374
376
  *
375
377
  * A candidate whose grant-time probe FAILS is dropped -- de-registered from
376
378
  * `state.instances` -- and identity-verified-killed BEFORE the walk
377
- * continues to the next candidate, but per WR-02
378
- * (`.planning/todos/pending/2026-08-05-wr-02-*`, decision: fix now rather
379
- * than defer further) the kill itself is fire-and-forget, matching
380
- * handleRelease()'s own posture a few hundred lines below
381
- * (`verifiedKill(...).catch(...)`, never awaited by that call site either):
382
- * the acquiring request must not wait up to `VICE_BROKER_KILL_WAIT_S`
383
- * (default 5s) of SIGTERM-then-poll-then-SIGKILL PER DEAD CANDIDATE before
384
- * the walk can move on -- that wait is exactly what turns a warm floor's
385
- * fast, in-memory grant into a multi-second serial teardown on a single
386
- * request's hot path once the warm floor is configured above its default
387
- * of 1 (WR-02's own bounding condition). The drop -- `markDeliberateDeath()`
379
+ * continues to the next candidate, but the kill itself is deliberately
380
+ * fire-and-forget, matching handleRelease()'s own posture a few hundred
381
+ * lines below (`verifiedKill(...).catch(...)`, never awaited by that call
382
+ * site either): the acquiring request must not wait up to
383
+ * `VICE_BROKER_KILL_WAIT_S` (default 5s) of SIGTERM-then-poll-then-SIGKILL
384
+ * PER DEAD CANDIDATE before the walk can move on -- that wait is exactly
385
+ * what turns a warm floor's fast, in-memory grant into a multi-second
386
+ * serial teardown on a single request's hot path once the warm floor is
387
+ * configured above its default of 1. The drop -- `markDeliberateDeath()`
388
388
  * plus `state.instances.delete()` -- still happens SYNCHRONOUSLY, in the
389
389
  * same tick as the probe failure, before `deps.kill(...)` is even invoked;
390
- * only the kill's own SETTLEMENT is decoupled from this walk. This is
391
- * WR-02's fix option 1, not option 2 (capping how many failed candidates a
392
- * single acquire will wait through): option 1 matches an idiom the file
393
- * already uses elsewhere rather than inventing a new bound, and removes the
394
- * wait entirely rather than merely capping it. The grant-time-probe-failure
390
+ * only the kill's own SETTLEMENT is decoupled from this walk. This matches
391
+ * an idiom the file already uses elsewhere rather than inventing a new
392
+ * bound, and removes the wait entirely rather than merely capping it, by
393
+ * design: capping how many failed candidates a single acquire will wait
394
+ * through was the alternative considered and rejected. The grant-time-probe-failure
395
395
  * log line's own ordering is decoupled accordingly (see below) -- it can no
396
396
  * longer name the kill's resolved stage synchronously, since nothing here
397
397
  * waits for it to resolve. The marker is set BEFORE any signal reaches the
@@ -404,24 +404,23 @@ export function profileEligible(record, requested) {
404
404
  * immediately after every `await` (the probe call itself) and BEFORE ever
405
405
  * treating a probe-live candidate as the winner -- this is what makes the
406
406
  * caller's own "no await between selection and the grant-recording step"
407
- * property (T-01.6.2.1-03) actually hold under two concurrent acquires. A
408
- * candidate's own probe response cannot change because a sibling acquire
409
- * granted it first, but its RECORDED state does, the instant that sibling's
410
- * synchronous grant step runs -- recorded state alone catches that case.
411
- * It does NOT catch a sibling that has already DROPPED this exact candidate
412
- * (a failed grant-time probe: markDeliberateDeath() + state.instances.delete(),
413
- * which never touches record.state -- the drop path a few lines below) --
414
- * 01.6.2.1-VERIFICATION.md's CR-01 finding, re-confirmed here: a state-only
415
- * recheck is blind to a concurrent drop, letting a second caller's stale
416
- * object reference win a grant for a record that is no longer in
417
- * state.instances at all, orphaning the grant. Rechecking
407
+ * property actually hold under two concurrent acquires. A candidate's own
408
+ * probe response cannot change because a sibling acquire granted it first,
409
+ * but its RECORDED state does, the instant that sibling's synchronous grant
410
+ * step runs -- recorded state alone catches that case. It does NOT catch a
411
+ * sibling that has already DROPPED this exact candidate (a failed
412
+ * grant-time probe: markDeliberateDeath() + state.instances.delete(), which
413
+ * never touches record.state -- the drop path a few lines below) -- a
414
+ * state-only recheck is blind to a concurrent drop, letting a second
415
+ * caller's stale object reference win a grant for a record that is no
416
+ * longer in state.instances at all, orphaning the grant. Rechecking
418
417
  * `state.instances.get(record.port) === record` (identity, not merely a
419
418
  * port-number lookup) closes that case too. */
420
419
  async function selectWarmInstance(state, deps) {
421
420
  for (const record of Array.from(state.instances.values())) {
422
421
  if (record.state !== "ready")
423
422
  continue;
424
- // Phase 33, plan 33-06 (D-16, T-33-23): a SYNCHRONOUS `continue`, sitting
423
+ // A SYNCHRONOUS `continue`, sitting
425
424
  // immediately beside the `record.state !== "ready"` filter directly
426
425
  // above and BEFORE the readiness probe below. That placement is
427
426
  // load-bearing twice over, and neither reason is stylistic:
@@ -448,7 +447,7 @@ async function selectWarmInstance(state, deps) {
448
447
  // while this probe was in flight. "Granted" changes record.state;
449
448
  // "dropped" removes the record from state.instances outright and never
450
449
  // touches record.state -- so map membership must be rechecked too, not
451
- // merely the state field (CR-01, 01.6.2.1-REVIEW.md/01.6.2.1-VERIFICATION.md).
450
+ // merely the state field.
452
451
  if (record.state !== "ready" || state.instances.get(record.port) !== record) {
453
452
  continue;
454
453
  }
@@ -456,27 +455,27 @@ async function selectWarmInstance(state, deps) {
456
455
  return record;
457
456
  }
458
457
  // Drop and de-register FIRST, synchronously, before the kill is even
459
- // invoked -- this is what CR-01's identity recheck above depends on:
460
- // the record must already be gone from state.instances by the time a
461
- // concurrent sibling's own probe on this same candidate resolves.
462
- // WR-02 only changes what happens to the kill's own PROMISE next, never
463
- // this ordering.
458
+ // invoked -- this is what the identity recheck above depends on: the
459
+ // record must already be gone from state.instances by the time a
460
+ // concurrent sibling's own probe on this same candidate resolves. The
461
+ // fire-and-forget kill below only changes what happens to the kill's
462
+ // own PROMISE next, never this ordering.
464
463
  markDeliberateDeath(record, false);
465
- // CR-02 (03-REVIEW.md): dropping a record is also where its second
464
+ // Dropping a record is also where its second
466
465
  // (`-remotemonitor`) port stops being spoken for -- deleteInstanceRecord()
467
466
  // is the ONE place both mutations happen together, so a drop can never
468
467
  // leak a port out of the fixed allocation band.
469
468
  deleteInstanceRecord(state, record.port);
470
469
  // Distinct wording from shutdown()'s own "shutdown complete" line
471
470
  // (broker-kill.mts) and from handleRecycleForRealBroker's own log-free
472
- // path -- D-07's standing constraint that a lifecycle decision must be
471
+ // path -- the standing constraint that a lifecycle decision must be
473
472
  // reconstructable from the log after an incident (both 2026-08-01 and
474
473
  // 2026-08-02 were diagnosed from broker log lines). Logged BEFORE the
475
- // kill settles (WR-02): the walk does not wait for deps.kill(...) to
474
+ // kill settles: the walk does not wait for deps.kill(...) to
476
475
  // resolve, so this line can no longer name the kill's resolved stage --
477
476
  // that gets its own, separately-logged line once the kill settles,
478
477
  // below.
479
- deps.log(`vice-broker: grant-time probe failed for port ${record.port} (pid ${record.pid ?? "null"}) -- dropped the record and kicked off an identity-verified kill of the pid (not awaited by the acquire walk, WR-02)`);
478
+ deps.log(`vice-broker: grant-time probe failed for port ${record.port} (pid ${record.pid ?? "null"}) -- dropped the record and kicked off an identity-verified kill of the pid (not awaited by the acquire walk)`);
480
479
  // Fire-and-forget, matching handleRelease()'s own posture
481
480
  // (`verifiedKill(...).catch(...)`, a few hundred lines below in this
482
481
  // same file) -- the acquire walk moves on to the next candidate (or
@@ -503,8 +502,8 @@ async function selectWarmInstance(state, deps) {
503
502
  * The warm-instance selection arm (selectWarmInstance(), P-01) runs BEFORE
504
503
  * the cold-launch arm; `atCapacity()` gates ONLY the cold-launch arm --
505
504
  * checked only once selectWarmInstance() has already answered `null` (no
506
- * probe-live candidate available) -- NOT before either arm (WR-01,
507
- * 01.6.2.1-REVIEW.md). A full host still refuses a fresh cold launch before
505
+ * probe-live candidate available) -- NOT before either arm. A full host
506
+ * still refuses a fresh cold launch before
508
507
  * ever touching the port allocator, but a ready, probe-live warm candidate
509
508
  * is grantable even when the ceiling is already reached: granting it
510
509
  * creates no NEW instance and does not raise `countTotal()`, so refusing to
@@ -523,14 +522,14 @@ async function selectWarmInstance(state, deps) {
523
522
  * control.mts's own attemptAcquire()/enqueueAcquire() queue the request and
524
523
  * retry it later rather than refusing it. */
525
524
  export async function handleAcquire(requestId, stateDir, state, deps = {}) {
526
- // WR-01: the readiness probe is backend-aware, from the SAME threaded-down
525
+ // The readiness probe is backend-aware, from the SAME threaded-down
527
526
  // verdict handleAcquire already uses for buildViceArgs() -- on stock the port
528
527
  // speaks the binary monitor, so an HTTP POST there can never succeed.
529
528
  const backend = deps.backend ?? "stock";
530
529
  const probe = deps.probe ?? ((port) => probeReady(port, { backend }));
531
530
  // Textually a verifiedKill( call site, not merely a reference -- reused
532
- // UNCHANGED from broker-kill.mts (Phase 01.6.2 criterion 6), never
533
- // re-derived, and never replaced by a bare process.kill().
531
+ // UNCHANGED from broker-kill.mts, never re-derived, and never replaced by
532
+ // a bare process.kill().
534
533
  const kill = deps.kill ?? ((opts) => verifiedKill(opts));
535
534
  const log = deps.log ?? ((line) => process.stderr.write(`${line}\n`));
536
535
  const winner = await selectWarmInstance(state, { probe, kill, log, requestedProfile: deps.profile });
@@ -556,13 +555,13 @@ export async function handleAcquire(requestId, stateDir, state, deps = {}) {
556
555
  state,
557
556
  stateDir,
558
557
  allocatePort: nextFreePort,
559
- // CR-01 (03-REVIEW.md): the SAME local `backend` const resolved at the
558
+ // The SAME local `backend` const resolved at the
560
559
  // top of this function feeds BOTH the initial argv (here) and the
561
560
  // supervision deps below, so a crash-respawn of this instance can never
562
561
  // build a different backend's argv than the launch it replaces.
563
562
  backend,
564
563
  allocateRemoteMonitorPort: deps.allocateRemoteMonitorPort,
565
- // Phase 33, plan 33-06 (D-15/D-16): the profile the warm arm just
564
+ // The profile the warm arm just
566
565
  // refused to compromise on reaches buildViceArgs() here, and is
567
566
  // mirrored onto the fresh InstanceRecord by spawnAndRecordInstance()
568
567
  // in the SAME step -- so this instance's recorded profile and its real
@@ -579,7 +578,7 @@ export async function handleAcquire(requestId, stateDir, state, deps = {}) {
579
578
  }),
580
579
  });
581
580
  if (!result.ok) {
582
- // Plan 41-05 (D-16, checkpoint option B): `result.reason` passes
581
+ // `result.reason` passes
583
582
  // straight through -- `AcquireLaunchResult`'s reason union
584
583
  // ("launch_in_flight" | "no_free_port" | "no_free_text_port") is a
585
584
  // subset of `AcquireOutcome`'s, so a failed text-port allocation's own
@@ -589,30 +588,29 @@ export async function handleAcquire(requestId, stateDir, state, deps = {}) {
589
588
  return { ok: false, reason: result.reason };
590
589
  }
591
590
  if (result.record.pid === null) {
592
- // WR-03 (01.6.2.1-REVIEW.md): the spawn never forked a real process
593
- // (e.g. a bad VICE_BIN path), so there is nothing to signal -- the
594
- // fix is deleting the just-created broken record alone. Without this,
595
- // a configuration failure would silently occupy a port slot and count
596
- // toward countTotal()/atCapacity() until crash supervision's own
597
- // delayed respawn/give-up machinery eventually noticed and freed it,
598
- // even though the caller was already told "internal" right now.
599
- // CR-02: deleteInstanceRecord(), not a bare map delete -- a stock launch
600
- // that failed this way already had its second port allocated and
601
- // blocked by acquirePortAndLaunch(), and deleteInstanceRecord() hands
602
- // that second port back to the allocator (via state.blockedPorts) in
603
- // the SAME step as it removes the broken record -- confirmed still
604
- // true after plan 41-05 (D-16): this branch is reached only once a
605
- // record already exists, i.e. only once BOTH allocations already
606
- // succeeded (a failed second allocation now fails the acquire before
607
- // any record -- and before this `pid === null` check -- is ever
608
- // reached at all).
591
+ // The spawn never forked a real process (e.g. a bad VICE_BIN path),
592
+ // so there is nothing to signal -- the fix is deleting the
593
+ // just-created broken record alone. Without this, a configuration
594
+ // failure would silently occupy a port slot and count toward
595
+ // countTotal()/atCapacity() until crash supervision's own delayed
596
+ // respawn/give-up machinery eventually noticed and freed it, even
597
+ // though the caller was already told "internal" right now.
598
+ // deleteInstanceRecord(), not a bare map delete -- a stock launch that
599
+ // failed this way already had its second port allocated and blocked
600
+ // by acquirePortAndLaunch(), and deleteInstanceRecord() hands that
601
+ // second port back to the allocator (via state.blockedPorts) in the
602
+ // SAME step as it removes the broken record -- this branch is reached
603
+ // only once a record already exists, i.e. only once BOTH allocations
604
+ // already succeeded (a failed second allocation now fails the acquire
605
+ // before any record -- and before this `pid === null` check -- is
606
+ // ever reached at all).
609
607
  deleteInstanceRecord(state, result.record.port);
610
608
  return { ok: false, reason: "internal" };
611
609
  }
612
610
  record = result.record;
613
611
  // Only the cold-launch arm ever writes a FRESH epoch record here --
614
- // selectWarmInstance()'s own winner already has one. Plan 41-05 (folded
615
- // todo) changes WHY that is true without changing that it IS true: a
612
+ // selectWarmInstance()'s own winner already has one. WHY that is true
613
+ // changed without changing that it IS true: a
616
614
  // ready, ungranted candidate no longer comes from a warm-floor pass's
617
615
  // own onLaunched hook (retired along with the floor) -- it comes from
618
616
  // broker-launch.mts's own crash-supervision respawn path
@@ -626,7 +624,7 @@ export async function handleAcquire(requestId, stateDir, state, deps = {}) {
626
624
  // THE single grant-recording step, fed by both arms above -- no `await`
627
625
  // between resolving `record` (whichever arm produced it) and this
628
626
  // synchronous pair, so two concurrent acquires can never both grant the
629
- // SAME record (T-01.6.2.1-03; see selectWarmInstance()'s own re-check for
627
+ // SAME record (see selectWarmInstance()'s own re-check for
630
628
  // the other half of that guarantee).
631
629
  state.grants.set(requestId, { id: requestId, port: record.port, grantedAt: Date.now(), pid: record.pid });
632
630
  record.state = "granted";
@@ -637,7 +635,7 @@ export async function handleAcquire(requestId, stateDir, state, deps = {}) {
637
635
  url: record.url,
638
636
  epochFile: record.epochFile,
639
637
  supervisorDir: record.supervisorDir,
640
- // Plan 41-01 (D-15): key omitted entirely when the record has none --
638
+ // Key omitted entirely when the record has none --
641
639
  // the fork case, and (until a later plan closes the port-allocation
642
640
  // degrade path) a stock instance whose second port allocation itself
643
641
  // failed. Same key-omitted-when-undefined idiom
@@ -649,7 +647,7 @@ export async function handleAcquire(requestId, stateDir, state, deps = {}) {
649
647
  /** Answers the `status` control-plane request: one entry per instance,
650
648
  * computed on demand from the SAME in-memory map every other count reads --
651
649
  * strictly better than the dropped broker-instances.json projection, which
652
- * could go stale between passes (D-24). */
650
+ * could go stale between passes. */
653
651
  function handleStatus(state) {
654
652
  return Array.from(state.instances.values()).map((r) => ({
655
653
  port: r.port,
@@ -657,9 +655,9 @@ function handleStatus(state) {
657
655
  state: r.state,
658
656
  reason: r.reason,
659
657
  epoch: typeof r.epoch === "number" ? r.epoch : null,
660
- // Plan 41-03 (D-14): "at least one channel is claimed" -- promoted from
658
+ // "at least one channel is claimed" -- promoted from
661
659
  // a single-field check, byte-identical wire shape, meaning stated
662
- // explicitly (D-15).
660
+ // explicitly.
663
661
  hasMonitorClient: Object.keys(r.monitorClients).length > 0,
664
662
  }));
665
663
  }
@@ -667,17 +665,16 @@ function handleStatus(state) {
667
665
  * handleRelease() and handleRecycleForRealBroker() already resolve theirs:
668
666
  * `targetId` is a grant id, looked up in state.grants for its port, then
669
667
  * the instance at that port. Returns `null` for an unknown target_id/port
670
- * so callers answer `bad_request`, never `internal` (plan 05's own
671
- * acceptance criterion). */
668
+ * so callers answer `bad_request`, never `internal`. */
672
669
  function resolveInstanceForMonitorTarget(targetId, state) {
673
670
  const grant = state.grants.get(targetId);
674
671
  if (!grant)
675
672
  return null;
676
673
  return state.instances.get(grant.port) ?? null;
677
674
  }
678
- /** Answers `monitor_claim` (plan 05, BROK-02/PROTO-08, D-13; per-channel
679
- * since plan 41-03, D-14): exclusive monitor-socket ownership enforced
680
- * HERE, broker-side, PER CHANNEL, so a conflicting claim is refused by name
675
+ /** Answers `monitor_claim` (per-channel): exclusive monitor-socket
676
+ * ownership enforced HERE, broker-side, PER CHANNEL, so a conflicting
677
+ * claim is refused by name
681
678
  * before any second `connect()` is ever attempted -- the one state stock
682
679
  * VICE cannot report and no client-side heuristic can diagnose. `targetId`
683
680
  * doubles as both "which instance" (resolved via the SAME grant lookup
@@ -705,11 +702,11 @@ export function handleMonitorClaim(requestId, targetId, channel, state) {
705
702
  }
706
703
  return { ok: false, code: "monitor_owned", holder: { grantId: existing.grantId, claimedAt: existing.claimedAt, pid: existing.pid, channel } };
707
704
  }
708
- /** Answers `monitor_release` (plan 05, T-02-01; per-channel since plan
709
- * 41-03, D-14): clears ONLY the named channel's entry, ONLY when `targetId`
710
- * names that channel's CURRENT holder -- a non-holder is refused, not
711
- * silently accepted (spoofing a release is exactly T-02-01's own
712
- * disposition). A channel with no current holder at all tolerates the
705
+ /** Answers `monitor_release` (per-channel): clears ONLY the named
706
+ * channel's entry, ONLY when `targetId` names that channel's CURRENT
707
+ * holder -- a non-holder is refused, not silently accepted (spoofing a
708
+ * release is a deliberately refused case). A channel with no current
709
+ * holder at all tolerates the
713
710
  * release as a success, matching the container-side client's own documented
714
711
  * tolerance for releasing a socket the broker already cleared. */
715
712
  export function handleMonitorRelease(requestId, targetId, channel, state) {
@@ -801,10 +798,11 @@ async function handleRecycleForRealBroker(targetId, state) {
801
798
  const reason = killStage === "identity_refused" ? "process identity did not match the recorded emulator binary -- the target was NOT signalled and is still running" : "";
802
799
  return { port: instance.port, pid: instance.pid, viceBin: instance.viceBin, killStage, epochBefore, outcome, reason };
803
800
  }
804
- /** Plan 41-05 (folded todo): the second concern of the fixed-order
805
- * evaluation pass, RENAMED from the retired warm-floor maintenance function
806
- * this replaces (D-24 drops the projection write; the grant sweep does not
807
- * appear -- D-12's connection-is-the-lease). Unlike the function it
801
+ /** The second concern of the fixed-order evaluation pass, RENAMED from the
802
+ * retired warm-floor maintenance function this replaces (the projection
803
+ * write is dropped, and the grant sweep does not appear -- the connection
804
+ * is the lease now, so there is nothing left to sweep for expiry). Unlike
805
+ * the function it
808
806
  * replaces, this one never launches anything -- it wires only
809
807
  * broker-launch.mjs's real promoteLaunchingInstances() against this
810
808
  * broker's own state and the backend-aware readiness probe, so a
@@ -815,7 +813,7 @@ function promoteLaunchingForRealBroker(state, backend) {
815
813
  return promoteLaunchingInstances({
816
814
  state,
817
815
  backend,
818
- // WR-01: same backend-aware probe route as handleAcquire's, from the
816
+ // Same backend-aware probe route as handleAcquire's, from the
819
817
  // SAME resolved verdict this function already receives.
820
818
  probe: (port) => probeReady(port, { backend }),
821
819
  log: (line) => process.stderr.write(`${line}\n`),
@@ -825,9 +823,9 @@ function promoteLaunchingForRealBroker(state, backend) {
825
823
  * when the port's CURRENT occupant is proven to be the SAME process this
826
824
  * grant was actually issued for (its own recorded `pid`, set at grant time
827
825
  * by handleAcquire()'s single state.grants.set() call site), not merely
828
- * "whatever now holds this port number." This is Task 2's own closure of
829
- * CR-01's cross-session-kill blast radius (T-01.6.2.1-28): even after Task
830
- * 1 closes the specific concurrent-acquire race, this lookup was ALREADY
826
+ * "whatever now holds this port number." This closes a cross-session-kill
827
+ * blast radius: even after the specific concurrent-acquire race above is
828
+ * closed, this lookup was ALREADY
831
829
  * unsafe against any OTHER event that swaps a port's occupant without also
832
830
  * clearing the grant -- the clearest independent example being an ordinary
833
831
  * (non-deliberate) crash of a GRANTED instance that hits the give-up
@@ -851,7 +849,7 @@ function promoteLaunchingForRealBroker(state, backend) {
851
849
  * grant's own recorded pid, and the current occupant's pid (or "none" when
852
850
  * the port is empty), worded distinctly from both the shutdown-complete
853
851
  * line (broker-kill.mts) and the grant-time-probe-failure line this same
854
- * file already emits (D-07's standing constraint that a lifecycle decision
852
+ * file already emits (the standing constraint that a lifecycle decision
855
853
  * must be reconstructable from the log after an incident).
856
854
  *
857
855
  * A legitimate recycle (broker-launch.mts's handleExit() recycle branch)
@@ -874,7 +872,7 @@ export function handleRelease(requestId, state) {
874
872
  // depend on silently continuing to hold.
875
873
  clearMonitorClient(instance);
876
874
  state.grants.delete(requestId);
877
- // CR-02: kill-never-recycle means this instance is gone for good, so its
875
+ // Kill-never-recycle means this instance is gone for good, so its
878
876
  // second (`-remotemonitor`) port must go back to the allocator with it.
879
877
  deleteInstanceRecord(state, grant.port);
880
878
  verifiedKill({ pid: instance.pid, expectedIdentity: instance.expectedIdentity }).catch(() => {
@@ -892,7 +890,7 @@ export function handleRelease(requestId, state) {
892
890
  }
893
891
  async function run(args) {
894
892
  const finalPath = join(args.stateDir, "broker.json");
895
- // Plan 05 (criterion K, D-17): the tracer/plan-04-era "refuse to overwrite
893
+ // An early tracer-era "refuse to overwrite
896
894
  // a record naming a currently-live pid" pre-check is GONE -- REPLACED by
897
895
  // the bind-before-write singleton guard below, not merely extended
898
896
  // alongside it (this phase's own plan-time note is explicit: the
@@ -905,7 +903,7 @@ async function run(args) {
905
903
  // port itself already held" -- and broker.json becomes a pure ARBITER of
906
904
  // that question's two possible causes, never a gate in its own right.
907
905
  //
908
- // D-25: the mandatory start-time banner, printed unconditionally and
906
+ // The mandatory start-time banner, printed unconditionally and
909
907
  // BEFORE anything else in this function runs -- an operator must be told
910
908
  // what a Ctrl-C costs before there is anything running for them to Ctrl-C.
911
909
  process.stderr.write(`${startupBanner()}\n`);
@@ -915,17 +913,17 @@ async function run(args) {
915
913
  const startedAt = new Date().toISOString(); // FIXED across every heartbeat refresh -- see writeBrokerRecordFile()'s callers below
916
914
  const pollMs = Number(process.env.VICE_BROKER_POLL_MS) || 500;
917
915
  const controlPort = resolveControlPort();
918
- // Criterion I / D-15: the unconditional startup reap runs BEFORE the
916
+ // The unconditional startup reap runs BEFORE the
919
917
  // control listener accepts and before anything is launched. A SIGKILLed
920
918
  // prior broker never ran a shutdown path, so this is the only place the
921
919
  // "every emulator this project's port band could be squatting is either
922
920
  // ours or a human's own work" guarantee can be enforced -- no marker file
923
921
  // is consulted, per this reap's own header comment in broker-kill.mts.
924
922
  //
925
- // NOTE (plan 05): this reap runs UNCONDITIONALLY, before the bind attempt
923
+ // NOTE: this reap runs UNCONDITIONALLY, before the bind attempt
926
924
  // below -- including for a process that goes on to LOSE the singleton
927
925
  // race a moment later (see the EADDRINUSE handling below). That ordering
928
- // is D-15's own, already established and tested by plan 04
926
+ // is deliberate and already established and tested
929
927
  // (broker-kill.test.ts's own structural source-order check); this task
930
928
  // does not change it. A losing second broker's own reap pass is an
931
929
  // accepted, pre-existing consequence of "the reap is unconditional" --
@@ -936,7 +934,7 @@ async function run(args) {
936
934
  nextEpochFor,
937
935
  writeEpochRecord,
938
936
  });
939
- // Plan 02-07 (D-01, D-03): resolved ONCE here, after the unconditional
937
+ // Resolved ONCE here, after the unconditional
940
938
  // startup reap and BEFORE the control listener binds -- never re-read per
941
939
  // launch, and never called from inside broker-launch.mts's `inFlight`
942
940
  // single-owner guard (this call sits entirely outside it; no launch is
@@ -946,14 +944,14 @@ async function run(args) {
946
944
  // directory repo-root.ts's supervisorDir() would resolve to, without this
947
945
  // host-bound module ever importing that container-side resolver directly
948
946
  // (backend-detect.mts's own header comment explains why it cannot).
949
- // FORKRM-01 (plan 52-06): there is nothing left to detect -- the resolved
947
+ // There is nothing left to detect -- the resolved
950
948
  // `backend` is always `"stock"`; what this call still does is resolve the
951
949
  // binary's own identity for the log line below and initialise the
952
- // capability cache backend-detect.mts's own BACK-04 record depends on.
950
+ // capability cache backend-detect.mts's own record depends on.
953
951
  const backendResult = resolvedBackend({ supervisorDir: args.stateDir });
954
952
  const backend = backendResult.backend;
955
953
  process.stderr.write(`vice-broker: backend "${backend}" (binary: ${backendResult.binPath})\n`);
956
- // Gap G-40-1, requirement R2 (plan 40-09): THE BROKER mints/verifies the
954
+ // THE BROKER mints/verifies the
957
955
  // Ghidra runs-root handle here -- after the unconditional startup reap
958
956
  // above, and BEFORE the control listener below accepts a single
959
957
  // connection -- so a container-side MCP server with no host tooling of
@@ -982,7 +980,7 @@ async function run(args) {
982
980
  else {
983
981
  process.stderr.write(`vice-broker: ghidra runs handle refused: ${ghidraHandleResult.message} -- ghidra.analyze will refuse by name until this is fixed by hand; every other tool id is unaffected\n`);
984
982
  }
985
- // D-18: the singleton guarantee holds only while the control port keeps its default -- two brokers deliberately configured onto different ports are two brokers, and no code prevents that.
983
+ // The singleton guarantee holds only while the control port keeps its default -- two brokers deliberately configured onto different ports are two brokers, and no code prevents that.
986
984
  let listener;
987
985
  try {
988
986
  listener = await startControlListener({
@@ -991,11 +989,11 @@ async function run(args) {
991
989
  token,
992
990
  onAcquire: (requestId, profile) => handleAcquire(requestId, args.stateDir, state, {
993
991
  backend,
994
- // Plan 03-04 (DIRECT-06, D-13): threaded down to
992
+ // Threaded down to
995
993
  // acquirePortAndLaunch()'s own gate (backend === "stock"); this
996
994
  // callback does not re-read any environment variable itself.
997
995
  allocateRemoteMonitorPort: (s, exclude) => nextFreePort(s, { exclude }),
998
- // Phase 33, plan 33-06 (REPRO-05, D-15): the ALREADY-NARROWED
996
+ // The ALREADY-NARROWED
999
997
  // profile broker-control.mts handed this callback. Nothing here
1000
998
  // re-validates it and nothing here reads a raw wire field --
1001
999
  // normaliseLaunchProfile() is the single narrowing site, and it ran
@@ -1005,11 +1003,11 @@ async function run(args) {
1005
1003
  onRelease: (requestId) => handleRelease(requestId, state),
1006
1004
  onRecycle: (targetId) => handleRecycleForRealBroker(targetId, state),
1007
1005
  onStatus: () => handleStatus(state),
1008
- // Phase 34, plan 34-01 (SEAM-01): its OWN callback, wired alongside
1006
+ // Its OWN callback, wired alongside
1009
1007
  // (never derived from) the other six above -- handed only
1010
1008
  // `args.repoRoot` and a stderr logger, never this broker's `state` map,
1011
1009
  // so it structurally cannot reach lease state through this closure.
1012
- // 34-09 (CR-04): deliberately supplies no timeout, and that is
1010
+ // Deliberately supplies no timeout, and that is
1013
1011
  // authoritative here, not an omission -- the per-tool budget table
1014
1012
  // inside runHostTool()/hostToolTimeoutMs() (host-tool.mts) is the ONE
1015
1013
  // place a budget is decided, and no wire field carries one across the
@@ -1031,7 +1029,7 @@ async function run(args) {
1031
1029
  viceBin: resolveViceBinForHostState(),
1032
1030
  maxInstances: resolveCeilingForRecord(),
1033
1031
  basePort: resolveBasePort(),
1034
- // FORKRM-01 (plan 52-06): the verdict THIS process resolved once, at
1032
+ // The verdict THIS process resolved once, at
1035
1033
  // startup, above -- kept on the wire because text-tools.ts's own
1036
1034
  // broker-identity cross-check (out of this plan's scope) still reads
1037
1035
  // it. Never a second resolvedBackend() call.
@@ -1040,7 +1038,7 @@ async function run(args) {
1040
1038
  });
1041
1039
  }
1042
1040
  catch (e) {
1043
- // Criterion K / D-17 / D-18: CR-01 closes here. A well-known TCP port
1041
+ // The singleton race closes here. A well-known TCP port
1044
1042
  // cannot be bound twice, so EADDRINUSE is the kernel enforcing the
1045
1043
  // singleton -- but the guarantee holds only while the control port
1046
1044
  // keeps its default (two brokers deliberately configured onto
@@ -1075,7 +1073,7 @@ async function run(args) {
1075
1073
  process.exitCode = 1;
1076
1074
  return;
1077
1075
  }
1078
- // C5: every catchable shutdown path (SIGTERM/SIGINT/SIGHUP, an uncaught
1076
+ // Every catchable shutdown path (SIGTERM/SIGINT/SIGHUP, an uncaught
1079
1077
  // exception, an unhandled rejection, normal exit) converges on ONE
1080
1078
  // re-entrant-safe teardown that identity-verified-kills every instance
1081
1079
  // this broker launched and clears the map unconditionally
@@ -1083,13 +1081,13 @@ async function run(args) {
1083
1081
  // nothing to tear down before that point.
1084
1082
  registerShutdownHandlers({ state });
1085
1083
  // A successful bind writes the record UNCONDITIONALLY, overwriting
1086
- // whatever was there -- the bind itself is the proof of singleton status
1087
- // (D-17). The fourteen-field set (D-27, criterion G; narrowed from
1088
- // fourteen to thirteen by plan 41-05, then widened back to fourteen to add
1089
- // node_exec_path): the lease time-to-live field the bash original carried
1090
- // is gone -- the connection is the lease now (D-12) -- `warm_floor` is
1091
- // likewise gone (plan 41-05: there is no warm floor left to echo a
1092
- // configured value for) -- and every other config-echo field survives
1084
+ // whatever was there -- the bind itself is the proof of singleton status.
1085
+ // The fourteen-field set (narrowed to thirteen once the warm floor was
1086
+ // retired, then widened back to fourteen to add node_exec_path): the
1087
+ // lease time-to-live field the bash original carried is gone -- the
1088
+ // connection is the lease now, so there is no separate expiry left to
1089
+ // track -- `warm_floor` is likewise gone (there is no warm floor left to
1090
+ // echo a configured value for) -- and every other config-echo field survives
1093
1091
  // even though no consumer parses it beyond a status message, because a
1094
1092
  // human reading this file by hand benefits from the full echo.
1095
1093
  //
@@ -1128,15 +1126,15 @@ async function run(args) {
1128
1126
  }, heartbeatMs);
1129
1127
  // The fixed-order evaluation pass (runBrokerPass, broker-launch.mts):
1130
1128
  // serve pending acquires, then promote launching -> ready -- mirroring
1131
- // vice-broker.sh's own broker_once() ordering (plan 41-05, folded todo:
1132
- // the warm floor this pass used to maintain as its second concern is
1133
- // RETIRED; see runBrokerPass()'s own comment in broker-launch.mts for what
1134
- // the fixed order still buys now that only serveAcquires() ever launches
1135
- // anything). Ticks on VICE_BROKER_POLL_MS (default 500, the SAME env var
1136
- // name and semantics the bash daemon used). serveAcquires now drains the
1129
+ // the retiring bash daemon's own broker_once() ordering (the warm floor
1130
+ // this pass used to maintain as its second concern is RETIRED; see
1131
+ // runBrokerPass()'s own comment in broker-launch.mts for what the fixed
1132
+ // order still buys now that only serveAcquires() ever launches anything).
1133
+ // Ticks on VICE_BROKER_POLL_MS (default 500, the SAME env var name and
1134
+ // semantics the bash daemon used). serveAcquires now drains the
1137
1135
  // arrival-ordered pending-acquire structure this listener instance owns
1138
- // (D-08's mechanism; plan 02's own `serveAcquires: () => {}` comment
1139
- // reserved exactly this room) -- an acquire queued because a launch was
1136
+ // (an early stubbed `serveAcquires: () => {}` comment reserved exactly
1137
+ // this room) -- an acquire queued because a launch was
1140
1138
  // already in flight is retried here, on the SAME pass that also promotes
1141
1139
  // any newly-ready instance, so a stalled pass shows up as a stale record
1142
1140
  // rather than a silently wrong one. Re-entrancy guarded: a pass that is
@@ -1160,8 +1158,8 @@ async function run(args) {
1160
1158
  }, pollMs);
1161
1159
  }
1162
1160
  /** Parses argv, evaluates the container guard FIRST -- before any state
1163
- * directory is read or written and before anything is spawned (PD-03) --
1164
- * then runs the long-lived broker. Never calls process.exit(); always sets
1161
+ * directory is read or written and before anything is spawned -- then
1162
+ * runs the long-lived broker. Never calls process.exit(); always sets
1165
1163
  * process.exitCode so pending I/O flushes first. */
1166
1164
  export function main(argv = process.argv.slice(2)) {
1167
1165
  let args;