@henols/vice-mcp 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/THIRD-PARTY-NOTICES.md +422 -1
- package/anno-bank.ts +171 -0
- package/anno-cli.ts +1674 -99
- package/anno-enum-gen.ts +416 -30
- package/anno-export-asm.ts +1175 -89
- package/anno-graphics.ts +338 -0
- package/anno-hazard-report.ts +1367 -0
- package/anno-import.ts +495 -0
- package/anno-join.ts +480 -0
- package/anno-provenance-ledger.ts +472 -0
- package/anno-register.ts +159 -0
- package/anno-store-export.ts +661 -0
- package/anno-store.ts +518 -2
- package/anno-tools.ts +1169 -16
- package/anno-types.ts +275 -2
- package/backend-detect.mts +124 -312
- package/build.ts +3 -1
- package/capture-predicate.ts +597 -0
- package/channel-lock.ts +349 -0
- package/evid-ingest.ts +217 -0
- package/evid-reconcile.ts +316 -0
- package/host-tool-client.ts +430 -0
- package/incident-record.ts +23 -12
- package/install-resources.ts +29 -13
- package/memmap-lookup.ts +285 -0
- package/package.json +27 -8
- package/prg-image.ts +1 -2
- package/repo-root.ts +87 -3
- package/resources/backend-detect.mjs +98 -236
- package/resources/broker-control.mjs +189 -16
- package/resources/broker-epoch.mjs +1 -1
- package/resources/broker-kill.mjs +8 -2
- package/resources/broker-launch.mjs +365 -210
- package/resources/broker-state.mjs +64 -18
- package/resources/container-guard.mjs +1 -1
- package/resources/ghidra-project.mjs +790 -0
- package/resources/host-tool.mjs +2561 -0
- package/resources/vice-broker.mjs +330 -184
- package/resources/vice-launcher.sh +127 -9
- package/stock-address.ts +1 -1
- package/stock-condition.ts +1 -1
- package/stock-connect.ts +9 -5
- package/stock-derived.ts +29 -37
- package/stock-diagnose.ts +200 -36
- package/stock-dispatch.ts +179 -77
- package/stock-handler.ts +1 -1
- package/stock-paths.ts +18 -14
- package/stock-petscii.ts +1 -1
- package/stock-protocol.ts +1 -1
- package/stock-recycle.ts +83 -2
- package/stock-reproducible-run.ts +811 -0
- package/stock-run-until.ts +100 -1
- package/stock-symbols.ts +4 -4
- package/stock-timing.ts +1 -1
- package/stop-oracle.ts +167 -0
- package/text-capability-probe.ts +660 -0
- package/text-connect.ts +157 -0
- package/text-protocol.ts +810 -0
- package/text-tools.ts +778 -0
- package/textmon-backtrace.ts +385 -0
- package/textmon-cpuhistory.ts +335 -0
- package/textmon-memmap.ts +494 -0
- package/textmon-profile.ts +458 -0
- package/textmon-registers.ts +748 -0
- package/tools-manifest.stock.json +864 -3
- package/vice-broker-client.ts +189 -42
- package/vice-errors.ts +268 -0
- package/vice-proxy.ts +339 -2144
- package/vsf-slice.ts +640 -0
- package/anno-d64.ts +0 -310
- package/capability-registry.ts +0 -390
- package/refresh-manifest.ts +0 -124
- package/tools-manifest.json +0 -1223
- package/vice-probe.ts +0 -278
- package/vice-sync.ts +0 -336
- package/vice.ts +0 -772
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Compiled by `tsc` from broker-launch.mts. Edit the TypeScript source and rebuild;
|
|
3
3
|
// changes made directly to this file are silently overwritten by the next build, and are never
|
|
4
4
|
// deployed to the host on their own -- install-resources.mjs copies THIS file's on-disk contents
|
|
5
|
-
// verbatim to tools/, so an edit made only here reaches the host but is lost on the very next
|
|
5
|
+
// verbatim to .c64-re-tools/bin/, so an edit made only here reaches the host but is lost on the very next
|
|
6
6
|
// rebuild.
|
|
7
7
|
// broker-launch.mts
|
|
8
8
|
//
|
|
@@ -14,8 +14,12 @@
|
|
|
14
14
|
// in-process mechanism (collapsed from a three-way branch by Phase
|
|
15
15
|
// 01.6.2.1's own plan 02 -- D-05 as amended by P-05/P-06/P-07; see
|
|
16
16
|
// probeReady()'s own header comment below for the amendment's record),
|
|
17
|
-
//
|
|
18
|
-
//
|
|
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.
|
|
19
23
|
//
|
|
20
24
|
// Plan 03, Task 2 grows this module into a real per-child supervisor
|
|
21
25
|
// (C2/D-23), absorbing resources/vice-supervisor.sh wholesale: superviseChild()
|
|
@@ -71,6 +75,37 @@ let warnedBinmonBindWidened = false;
|
|
|
71
75
|
// caller could widen one host override and not the other, though in
|
|
72
76
|
// practice both resolve from the same `binmonHost` value below).
|
|
73
77
|
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. */
|
|
86
|
+
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.
|
|
93
|
+
*
|
|
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
|
|
97
|
+
* intent. */
|
|
98
|
+
export const STOCK_DETERMINISM_FLAGS = Object.freeze([
|
|
99
|
+
"-seed",
|
|
100
|
+
String(STOCK_DETERMINISM_SEED),
|
|
101
|
+
"-raminitstartrandom",
|
|
102
|
+
"0",
|
|
103
|
+
"-raminitrepeatrandom",
|
|
104
|
+
"0",
|
|
105
|
+
"-raminitrandomchance",
|
|
106
|
+
"0",
|
|
107
|
+
"+autostart-delay-random",
|
|
108
|
+
]);
|
|
74
109
|
/** Resolves the emulator's own argument vector for the given `backend`. The
|
|
75
110
|
* `VICE_ARGS` full-override short-circuit (matching
|
|
76
111
|
* resources/vice-supervisor.sh's own VICE_ARGS convention exactly) is
|
|
@@ -80,15 +115,12 @@ let warnedRemoteMonitorBindWidened = false;
|
|
|
80
115
|
* understands neither `-mcpserver` nor `-binarymonitor` flags, and that need
|
|
81
116
|
* does not depend on which backend is configured.
|
|
82
117
|
*
|
|
83
|
-
* `backend: "
|
|
84
|
-
*
|
|
85
|
-
* VICE_BROKER_MCP_HOST (default `0.0.0.0`), and the MCP server port.
|
|
86
|
-
*
|
|
87
|
-
* `backend: "stock"` returns `-binarymonitor -binarymonitoraddress
|
|
118
|
+
* `backend: "stock"` (FORKRM-01, plan 52-06: the only value `ViceBackend` has
|
|
119
|
+
* left) returns `-binarymonitor -binarymonitoraddress
|
|
88
120
|
* ip4://<host>:<port>` (docs/phase1-probe-results.md's confirmed real-world
|
|
89
121
|
* command line). The host resolves from `binmonHost` or
|
|
90
122
|
* VICE_BROKER_BINMON_HOST, defaulting to `127.0.0.1` -- deliberately
|
|
91
|
-
*
|
|
123
|
+
* narrow, because VICE's binary
|
|
92
124
|
* monitor is unauthenticated by design and grants full read/write over the
|
|
93
125
|
* emulated machine plus process control to anything that can reach it
|
|
94
126
|
* (planner decision, `02-03-PLAN.md`). Widening the bind away from loopback
|
|
@@ -111,11 +143,29 @@ let warnedRemoteMonitorBindWidened = false;
|
|
|
111
143
|
* than implied stock-3.9 coverage. Widening THIS bind away from loopback emits
|
|
112
144
|
* its own one-time stderr note (`warnedRemoteMonitorBindWidened`), naming
|
|
113
145
|
* the resolved address and stating that VICE's TEXT monitor accepts
|
|
114
|
-
* arbitrary monitor commands and is unauthenticated
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
|
|
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).
|
|
153
|
+
*
|
|
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. */
|
|
168
|
+
export function buildViceArgs(port, { backend, mcpHost, binmonHost, viceArgsEnv, remoteMonitorPort, profile, }) {
|
|
119
169
|
const rawViceArgs = viceArgsEnv ?? process.env.VICE_ARGS;
|
|
120
170
|
if (typeof rawViceArgs === "string" && rawViceArgs.trim() !== "") {
|
|
121
171
|
return rawViceArgs.trim().split(/\s+/);
|
|
@@ -138,11 +188,31 @@ export function buildViceArgs(port, { backend, mcpHost, binmonHost, viceArgsEnv,
|
|
|
138
188
|
// first element: it is VICE's reset-to-compiled-in-defaults instruction,
|
|
139
189
|
// not an inert "these are the baselines" no-op, so any flag emitted
|
|
140
190
|
// before it (including -drive8type) is silently clobbered back to its
|
|
141
|
-
// compiled-in value. `-drive8type 1541` therefore has to come
|
|
142
|
-
//
|
|
143
|
-
// constraint -- `-default` also has to come before
|
|
144
|
-
// the monitor never binds and the subsequent connect
|
|
145
|
-
// backlog looking exactly like a wedge.
|
|
191
|
+
// compiled-in value. `-drive8type 1541` therefore has to come AFTER
|
|
192
|
+
// `-default` -- not necessarily IMMEDIATELY after -- and -- per
|
|
193
|
+
// CLAUDE.md's documented constraint -- `-default` also has to come before
|
|
194
|
+
// `-binarymonitor` or the monitor never binds and the subsequent connect
|
|
195
|
+
// hangs in the backlog looking exactly like a wedge.
|
|
196
|
+
//
|
|
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.
|
|
204
|
+
//
|
|
205
|
+
// 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
|
|
210
|
+
// [VERIFIED: live probe 2026-09-03, genuine unpatched stock
|
|
211
|
+
// /usr/bin/x64sc (VICE 3.9), DISPLAY and WAYLAND_DISPLAY both unset]:
|
|
212
|
+
// [-default -console -drive8type 1541 <determinism> -binarymonitor]
|
|
213
|
+
// alive=yes bound=1 Drive8Type=1541 Drive8TrueEmulation=1
|
|
214
|
+
// read over `RESOURCE_GET` (0x51) with `-console` interposed. So the
|
|
215
|
+
// citation now covers the RESOURCE and not only liveness. Confirmed sufficient live in
|
|
146
216
|
// Phase 8.1's standalone probe (08.1-WALKTHROUGH-EVIDENCE.md §4):
|
|
147
217
|
// `resourceget "Drive8Type"` moved 0 -> 1541 and a `load` over the text
|
|
148
218
|
// monitor succeeded immediately. Deliberately NOT setting
|
|
@@ -153,7 +223,81 @@ export function buildViceArgs(port, { backend, mcpHost, binmonHost, viceArgsEnv,
|
|
|
153
223
|
// might default Drive8TrueEmulation to 0) is read and deliberately not
|
|
154
224
|
// pre-emptively defended against here; plan 03's live test is what would
|
|
155
225
|
// surface it if that assumption is ever wrong on a different build.
|
|
156
|
-
|
|
226
|
+
//
|
|
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
|
|
232
|
+
// [CITED: vice-3.8/src/main.c:184-192, 232-238] -- and `console_mode`
|
|
233
|
+
// gates GTK initialisation at two call sites (`ui_init_with_args`,
|
|
234
|
+
// `ui_init`) that BOTH run before the late command-line parser
|
|
235
|
+
// `initcmdline_check_args()` [CITED: vice-3.8/src/main.c:296-345]. A
|
|
236
|
+
// `-console` seen only by the late parser therefore arrives after GTK has
|
|
237
|
+
// already tried and failed. MEASURED 2026-09-02 with `DISPLAY` and
|
|
238
|
+
// `WAYLAND_DISPLAY` both unset (33-RESEARCH.md P5):
|
|
239
|
+
// [-default -console -binarymonitor] alive=yes bound=1
|
|
240
|
+
// [-default -drive8type 1541 -console -binarymonitor] alive=no bound=0 Gtk-WARNING: cannot open display:
|
|
241
|
+
// [-default -console -drive8type 1541 -binarymonitor] alive=yes bound=1
|
|
242
|
+
// 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
|
|
247
|
+
// the monitor bound. It is pinned by an ordering assertion rather than by
|
|
248
|
+
// this comment -- a bare flag push with no reason is exactly what let the
|
|
249
|
+
// `-default` ordering constraint be rediscovered by a red CI run last
|
|
250
|
+
// time.
|
|
251
|
+
//
|
|
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],
|
|
256
|
+
// `-raminitrandomchance 0` is the LOAD-BEARING one: the factory value is
|
|
257
|
+
// **10**, i.e. 0.1% of all RAM bits randomly flipped at power-up, and it is
|
|
258
|
+
// the dominant term in the divergence this milestone removes. The
|
|
259
|
+
// `-raminitstartrandom 0` / `-raminitrepeatrandom 0` pair already reads 0
|
|
260
|
+
// 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
|
|
262
|
+
// spawnAndRecordInstance() below.
|
|
263
|
+
//
|
|
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`
|
|
267
|
+
// ships at **1** on this build [VERIFIED: same probe], it draws an
|
|
268
|
+
// additional random delay of up to 10 frames
|
|
269
|
+
// [CITED: vice-3.8/src/autostart.c:1432-1436], and -- the effect that is
|
|
270
|
+
// easy to miss -- it also SELECTS WHICH keyboard-buffer feed injects `RUN`
|
|
271
|
+
// (`kbdbuf_feed_runcmd` when set, `kbdbuf_feed` when clear)
|
|
272
|
+
// [CITED: vice-3.8/src/autostart.c:882-886]. Disabling it is therefore a
|
|
273
|
+
// behavioural change and not only a timing one. Pin it once; never toggle
|
|
274
|
+
// it between the two runs of a capture pair.
|
|
275
|
+
//
|
|
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.
|
|
291
|
+
const args = ["-default"];
|
|
292
|
+
if (profile?.headless) {
|
|
293
|
+
args.push("-console");
|
|
294
|
+
}
|
|
295
|
+
args.push("-drive8type", "1541");
|
|
296
|
+
args.push(...STOCK_DETERMINISM_FLAGS);
|
|
297
|
+
if (profile?.warp) {
|
|
298
|
+
args.push("-warp");
|
|
299
|
+
}
|
|
300
|
+
args.push("-binarymonitor", "-binarymonitoraddress", `ip4://${host}:${port}`);
|
|
157
301
|
if (typeof remoteMonitorPort === "number") {
|
|
158
302
|
if (host !== "127.0.0.1" && !warnedRemoteMonitorBindWidened) {
|
|
159
303
|
warnedRemoteMonitorBindWidened = true;
|
|
@@ -186,12 +330,30 @@ function spawnAndRecordInstance(reason, port, deps) {
|
|
|
186
330
|
const spawnFn = deps.spawn ?? ((cmd, args, opts) => nodeSpawn(cmd, args, opts));
|
|
187
331
|
const now = deps.now ?? (() => Date.now());
|
|
188
332
|
const viceBin = deps.viceBin ?? process.env.VICE_BIN ?? "x64sc";
|
|
189
|
-
const backend = deps.backend ?? "
|
|
333
|
+
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.
|
|
343
|
+
// The fork case is real and unaffected: this check is stock-only.
|
|
344
|
+
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");
|
|
346
|
+
}
|
|
190
347
|
const viceArgs = buildViceArgs(port, {
|
|
191
348
|
backend,
|
|
192
349
|
mcpHost: deps.mcpHost,
|
|
193
350
|
binmonHost: deps.binmonHost,
|
|
194
351
|
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.
|
|
356
|
+
profile: deps.profile,
|
|
195
357
|
});
|
|
196
358
|
const log = deps.log ?? defaultLog;
|
|
197
359
|
// I-1 rider (audit §4.4, 08.2-02-PLAN.md Task 2): production stock
|
|
@@ -204,24 +366,23 @@ function spawnAndRecordInstance(reason, port, deps) {
|
|
|
204
366
|
// unreachable) and pass it as a third options argument carrying `env`
|
|
205
367
|
// only. Never `shell: true`: the existing array-form spawn(viceBin,
|
|
206
368
|
// viceArgs) call avoids shell interpretation entirely and that property
|
|
207
|
-
// must survive this widening.
|
|
208
|
-
// with NO third argument at all, so the fork path's observable behaviour
|
|
209
|
-
// stays bit-for-bit what it was (BACK-02 is a standing gate and the fork
|
|
210
|
-
// backend has been the sole production backend across all of v0.1.x).
|
|
369
|
+
// must survive this widening.
|
|
211
370
|
//
|
|
212
371
|
// Scope boundary (do not remove this note): the production broker daemon
|
|
213
372
|
// always supplies its own deps.spawn / deps.spawnFactory, so the widened
|
|
214
373
|
// default wrapper above is dead code on the real launch paths. This
|
|
215
374
|
// function's job is only to COMPUTE the value at the one seam that should
|
|
216
|
-
// own it; the forwarding to nodeSpawn() happens at
|
|
217
|
-
// makeLoggingSpawn() and
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
375
|
+
// own it; the forwarding to nodeSpawn() happens at three further hops --
|
|
376
|
+
// 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()
|
|
382
|
+
// composition test that omits buildColdSpawnFactory so an injected stub
|
|
383
|
+
// cannot fake the proof). If you add another spawn hop, it must forward
|
|
384
|
+
// options too, or production stock launches silently lose their config
|
|
385
|
+
// isolation again.
|
|
225
386
|
//
|
|
226
387
|
// Scratch-dir lifetime: this function deliberately does NOT clean the
|
|
227
388
|
// directory up -- the spawned emulator process outlives this function's
|
|
@@ -254,7 +415,26 @@ function spawnAndRecordInstance(reason, port, deps) {
|
|
|
254
415
|
viceBin,
|
|
255
416
|
viceArgs,
|
|
256
417
|
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.
|
|
422
|
+
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
|
|
425
|
+
// remoteMonitorPort, so a stock record reaching this line always
|
|
426
|
+
// supplies the key. "Absent" means fork, never "stock allocation
|
|
427
|
+
// failed" (that state no longer exists).
|
|
257
428
|
...(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.
|
|
437
|
+
...(deps.profile === undefined ? {} : { profile: { ...deps.profile } }),
|
|
258
438
|
};
|
|
259
439
|
deps.state.instances.set(port, record);
|
|
260
440
|
return record;
|
|
@@ -271,10 +451,10 @@ function spawnAndRecordInstance(reason, port, deps) {
|
|
|
271
451
|
*
|
|
272
452
|
* This is the RIGHT primitive when the port is already decided and fixed
|
|
273
453
|
* (most tests; any future caller with its own allocation scheme). It is
|
|
274
|
-
* deliberately NOT what handleAcquire
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
454
|
+
* deliberately NOT what handleAcquire calls for a FRESH port, because
|
|
455
|
+
* nextFreePort() itself is asynchronous (a real port-in-use probe requires
|
|
456
|
+
* it) -- see acquirePortAndLaunch()'s own header comment for the race that
|
|
457
|
+
* creates and how it is closed. */
|
|
278
458
|
export function tryLaunchOne(reason, port, deps) {
|
|
279
459
|
if (inFlight)
|
|
280
460
|
return null;
|
|
@@ -291,26 +471,37 @@ export function tryLaunchOne(reason, port, deps) {
|
|
|
291
471
|
* instant tryLaunchOne() alone guards. This closes a genuine race window
|
|
292
472
|
* tryLaunchOne() cannot: nextFreePort()'s own port-in-use probe is
|
|
293
473
|
* asynchronous (plan 02, C4 -- a real bind-and-release check), so two
|
|
294
|
-
* overlapping callers
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
* own discipline, so a second concurrent call is refused immediately
|
|
301
|
-
* (`launch_in_flight`) rather than racing on the allocation.
|
|
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.
|
|
302
480
|
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
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
|
|
489
|
+
* concurrent acquires -- e.g. two requests arriving over the TCP control
|
|
490
|
+
* listener at nearly the same moment, or one arriving while an EARLIER
|
|
491
|
+
* acquire's own launch is still resolving -- never a warming pass, which no
|
|
492
|
+
* longer exists. This is also the function that restores vice-broker.sh's
|
|
493
|
+
* own process_requests() throttle (its `in_flight` local): whatever launches
|
|
494
|
+
* this broker ever attempts, they never overlap, matching the bash
|
|
307
495
|
* original's declined-to-change behaviour (RESEARCH.md §A1/§C). D-07
|
|
308
496
|
* (01.6.2.1-03-PLAN.md) layers non-preemptive PRIORITY on top of this same
|
|
309
|
-
* "one at a time" guard, never replacing it
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
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
|
|
314
505
|
* anything in this function. The refusal below logs which reason currently
|
|
315
506
|
* holds the slot and which reason is waiting, so the decision is
|
|
316
507
|
* reconstructable from the log after an incident. */
|
|
@@ -351,16 +542,27 @@ export async function acquirePortAndLaunch(reason, deps) {
|
|
|
351
542
|
// does. `state.blockedPorts` is a plain Set the type import already
|
|
352
543
|
// describes, so mutating it directly needs no value import at all --
|
|
353
544
|
// exactly the same discipline handleExit()'s own
|
|
354
|
-
// `record.
|
|
545
|
+
// `record.monitorClients = {}` uses in place of
|
|
355
546
|
// clearMonitorClient().
|
|
356
547
|
deps.state.blockedPorts.add(remoteResult.port);
|
|
357
548
|
remoteMonitorPort = remoteResult.port;
|
|
358
549
|
}
|
|
359
550
|
else {
|
|
360
|
-
//
|
|
361
|
-
//
|
|
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
|
|
557
|
+
// `nextFreePort()`'s own in-use probe blocks a candidate, and that
|
|
558
|
+
// never ran against the winning candidate) -- so it is already
|
|
559
|
+
// allocatable again on the very next call with no further release
|
|
560
|
+
// step; a discriminating-power test proves this rather than assuming
|
|
561
|
+
// it. This failure arm must NEVER call spawnAndRecordInstance() or
|
|
562
|
+
// otherwise leave a port "spoken for" on the caller's behalf.
|
|
362
563
|
log(`vice-broker: second (-remotemonitor) port allocation failed (${remoteResult.reason}) -- ` +
|
|
363
|
-
`
|
|
564
|
+
`abandoning the stock launch; the text-monitor port is mandatory on every stock launch (D-16) and the acquire fails`);
|
|
565
|
+
return { ok: false, reason: "no_free_text_port" };
|
|
364
566
|
}
|
|
365
567
|
}
|
|
366
568
|
const record = spawnAndRecordInstance(reason, port, {
|
|
@@ -374,6 +576,7 @@ export async function acquirePortAndLaunch(reason, deps) {
|
|
|
374
576
|
backend: deps.backend,
|
|
375
577
|
binmonHost: deps.binmonHost,
|
|
376
578
|
remoteMonitorPort,
|
|
579
|
+
profile: deps.profile,
|
|
377
580
|
});
|
|
378
581
|
return { ok: true, record };
|
|
379
582
|
}
|
|
@@ -393,8 +596,9 @@ export async function acquirePortAndLaunch(reason, deps) {
|
|
|
393
596
|
* instance permanently consumed one more port out of the fixed
|
|
394
597
|
* PORT_SCAN_CEILING window even though the OS port was free again the instant
|
|
395
598
|
* the owning process exited -- a long-running broker (the explicit design goal
|
|
396
|
-
* of an on-demand pool with crash supervision
|
|
397
|
-
*
|
|
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
|
|
398
602
|
* from routine churn, with no operator recourse short of a broker restart.
|
|
399
603
|
*
|
|
400
604
|
* A RESPAWN is deliberately NOT a call site: the replacement instance keeps
|
|
@@ -645,8 +849,8 @@ async function defaultBinmonProbe(port, timeoutMs) {
|
|
|
645
849
|
* fully honoured by this collapse, not reversed by it.
|
|
646
850
|
*
|
|
647
851
|
* No retry loop, deliberately: a still-booting instance simply fails THIS
|
|
648
|
-
* pass and is re-probed on the next one (
|
|
649
|
-
* cadence, or a later grant-time re-probe) -- this is what makes the
|
|
852
|
+
* pass and is re-probed on the next one (promoteLaunchingInstances()'s own
|
|
853
|
+
* per-pass cadence, or a later grant-time re-probe) -- this is what makes the
|
|
650
854
|
* shortened ~1s default below safe rather than reckless: a slow host is
|
|
651
855
|
* re-probed, never starved, and the seconds-valued timeout knob
|
|
652
856
|
* (VICE_BROKER_PROBE_TIMEOUT_S) still lets an operator on a slow host raise
|
|
@@ -665,61 +869,22 @@ export async function probeReady(port, deps = {}) {
|
|
|
665
869
|
const httpProbe = deps.httpProbe ?? defaultHttpProbe;
|
|
666
870
|
return httpProbe(port, timeoutMs);
|
|
667
871
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
const n = Number(raw);
|
|
678
|
-
return Number.isFinite(n) ? n : 1;
|
|
679
|
-
}
|
|
680
|
-
function resolveCeiling(override) {
|
|
681
|
-
if (typeof override === "number")
|
|
682
|
-
return override;
|
|
683
|
-
const raw = process.env.VICE_BROKER_MAX;
|
|
684
|
-
if (raw === undefined || raw === "")
|
|
685
|
-
return 16;
|
|
686
|
-
const n = Number(raw);
|
|
687
|
-
return Number.isFinite(n) ? n : 16;
|
|
688
|
-
}
|
|
689
|
-
/** Promotes launching instances via probe, then -- unless a launch is
|
|
690
|
-
* already in flight -- launches AT MOST ONE instance toward the warm floor
|
|
691
|
-
* and returns. Never loops to reach the floor in one call: reaching
|
|
692
|
-
* VICE_BROKER_WARM_FLOOR this way costs one
|
|
693
|
-
* additional CALL per warm instance instead of one call total, which is the exact
|
|
694
|
-
* trade the 2026-08-01 outage made non-negotiable (three simultaneous
|
|
695
|
-
* x64sc launches: one SEGV, one exit 1, one exit 0 at the identical spawn
|
|
696
|
-
* second). `async`/`await` makes launching everything needed in one go
|
|
697
|
-
* look free and idiomatic; it is actively dangerous here. DO NOT gather
|
|
698
|
-
* several pending launches into a single concurrent await, and do not
|
|
699
|
-
* "helpfully" loop this function internally until the floor is met.
|
|
700
|
-
*
|
|
701
|
-
* D-05's probe-live floor evaluation (count_ready() trusting probe-live
|
|
702
|
-
* instances rather than a recorded `ready` state) is explicitly Phase
|
|
703
|
-
* 01.6.2.1's criterion L, NOT this plan's -- countReady() here still
|
|
704
|
-
* counts by RECORDED state, exactly like the bash original's count_ready()
|
|
705
|
-
* before Decision 5.2. A reviewer must not mistake this for an oversight:
|
|
706
|
-
* it is the declined-for-this-phase choice RESEARCH.md §A1 recommends, and
|
|
707
|
-
* grant_from_spare()'s own live re-probe at GRANT time (broker-kill.mts /
|
|
708
|
-
* plan 04's territory) is a separate, already-correct mechanism this plan
|
|
709
|
-
* does not touch. */
|
|
710
|
-
export async function maintainWarmFloor(deps) {
|
|
872
|
+
/** Promotes every `launching` instance whose readiness probe now succeeds to
|
|
873
|
+
* `ready`, recording its readiness timestamp and logging the elapsed boot
|
|
874
|
+
* time. Runs regardless of whether a launch is in flight -- promotion and a
|
|
875
|
+
* NEW launch starting are independent concerns; an already-launched instance
|
|
876
|
+
* becomes usable the moment it answers, whether or not this same pass goes on
|
|
877
|
+
* to start anything further. No retry loop: a still-booting instance simply
|
|
878
|
+
* fails THIS pass and is re-probed on the next one (runBrokerPass()'s own
|
|
879
|
+
* per-tick cadence). */
|
|
880
|
+
export async function promoteLaunchingInstances(deps) {
|
|
711
881
|
const log = deps.log ?? defaultLog;
|
|
712
882
|
const now = deps.now ?? (() => Date.now());
|
|
713
883
|
// WR-01: the DEFAULT probe follows this call's own backend, so a caller that
|
|
714
|
-
// threads `backend`
|
|
715
|
-
//
|
|
716
|
-
//
|
|
717
|
-
const probe = deps.probe ?? ((port) => probeReady(port, { backend: deps.backend ?? "
|
|
718
|
-
// Step 1: promote every "launching" instance whose probe now succeeds.
|
|
719
|
-
// Runs regardless of whether a launch is in flight -- promotion and
|
|
720
|
-
// speculative warming are independent concerns; an already-launched
|
|
721
|
-
// instance becomes usable the moment it answers, whether or not this
|
|
722
|
-
// pass goes on to warm anything further.
|
|
884
|
+
// threads `backend` and omits `probe` gets a matching readiness route
|
|
885
|
+
// rather than an HTTP POST at a binary-monitor port. An explicitly
|
|
886
|
+
// injected `probe` still wins, unchanged.
|
|
887
|
+
const probe = deps.probe ?? ((port) => probeReady(port, { backend: deps.backend ?? "stock" }));
|
|
723
888
|
for (const record of deps.state.instances.values()) {
|
|
724
889
|
if (record.state !== "launching")
|
|
725
890
|
continue;
|
|
@@ -732,65 +897,6 @@ export async function maintainWarmFloor(deps) {
|
|
|
732
897
|
log(`vice-broker: port ${record.port} launching -> ready (${elapsedMs}ms)`);
|
|
733
898
|
}
|
|
734
899
|
}
|
|
735
|
-
// Step 2 (P-06: the warm-zero "no readiness mechanism" branch that used
|
|
736
|
-
// to sit here is GONE -- the surviving probe mechanism is in-process and
|
|
737
|
-
// always available, so there is no "no mechanism" state left to warm
|
|
738
|
-
// zero against). No new boot starts while one is already under way --
|
|
739
|
-
// THE single in-flight counter (countLaunching) both this function and a
|
|
740
|
-
// cold acquire (vice-broker.mts's handleAcquire) consult.
|
|
741
|
-
if (deps.countLaunching(deps.state) > 0) {
|
|
742
|
-
// D-07's launch-slot decision log line, this decision point's own half:
|
|
743
|
-
// name WHICH reason currently holds the slot (the launching record's
|
|
744
|
-
// own `reason`, whichever call produced it -- cold acquire or an
|
|
745
|
-
// earlier warming pass), not merely that warming is waiting.
|
|
746
|
-
const inFlightRecord = Array.from(deps.state.instances.values()).find((r) => r.state === "launching");
|
|
747
|
-
const winningReason = inFlightRecord?.reason ?? "unknown";
|
|
748
|
-
log(`vice-broker: launch-slot decision -- ${winningReason} holds the slot; spare waits (D-07)`);
|
|
749
|
-
return;
|
|
750
|
-
}
|
|
751
|
-
const ready = deps.countReady(deps.state);
|
|
752
|
-
const total = deps.countTotal(deps.state);
|
|
753
|
-
const warmFloor = resolveWarmFloor(deps.warmFloor);
|
|
754
|
-
const ceiling = resolveCeiling(deps.ceiling);
|
|
755
|
-
if (!(ready < warmFloor && total < ceiling)) {
|
|
756
|
-
return;
|
|
757
|
-
}
|
|
758
|
-
// acquirePortAndLaunch() holds the SAME single in_flight owner across
|
|
759
|
-
// its own async port allocation -- not merely tryLaunchOne()'s
|
|
760
|
-
// synchronous spawn instant. This is what actually closes the race
|
|
761
|
-
// between this warm-floor launch and a cold acquire (vice-broker.mts's
|
|
762
|
-
// handleAcquire) arriving over the TCP control listener at any moment:
|
|
763
|
-
// the countLaunching() check just above is a cheap PRE-check (bails
|
|
764
|
-
// early when a launch is already recorded), but nextFreePort() is
|
|
765
|
-
// itself asynchronous, so without the guard held across the allocation
|
|
766
|
-
// too, two overlapping callers could still both be told the same
|
|
767
|
-
// candidate port is free before either commits it.
|
|
768
|
-
const result = await acquirePortAndLaunch("spare", {
|
|
769
|
-
state: deps.state,
|
|
770
|
-
stateDir: deps.stateDir,
|
|
771
|
-
allocatePort: deps.allocatePort,
|
|
772
|
-
allocateRemoteMonitorPort: deps.allocateRemoteMonitorPort,
|
|
773
|
-
spawn: deps.spawn,
|
|
774
|
-
spawnFactory: deps.spawnFactory,
|
|
775
|
-
now: deps.now,
|
|
776
|
-
viceBin: deps.viceBin,
|
|
777
|
-
mcpHost: deps.mcpHost,
|
|
778
|
-
backend: deps.backend,
|
|
779
|
-
binmonHost: deps.binmonHost,
|
|
780
|
-
});
|
|
781
|
-
if (result.ok) {
|
|
782
|
-
log(`vice-broker: warmed 1 warm instance this pass -- ${ready + 1} of ${warmFloor} ready, remainder warmed on later passes`);
|
|
783
|
-
deps.onLaunched?.(result.record);
|
|
784
|
-
}
|
|
785
|
-
else if (result.reason === "no_free_port") {
|
|
786
|
-
log(`vice-broker: no free port available -- warming no further warm instances; ${ready} of ${warmFloor} ready`);
|
|
787
|
-
}
|
|
788
|
-
else {
|
|
789
|
-
// A launch started (cold or warm) between this function's own
|
|
790
|
-
// countLaunching() check above and this call -- a narrow window
|
|
791
|
-
// closed by the guard rather than assumed impossible.
|
|
792
|
-
log("vice-broker: a warm-floor launch was attempted but a launch was already in flight -- deferring to a later pass");
|
|
793
|
-
}
|
|
794
900
|
}
|
|
795
901
|
/** The fixed pass order (mirrors vice-broker.sh's own broker_once(), whose
|
|
796
902
|
* comment names the ordering as load-bearing: "the spare invariant is
|
|
@@ -804,21 +910,26 @@ export async function maintainWarmFloor(deps) {
|
|
|
804
910
|
* instrumented no-op functions and assert call ORDER without needing a
|
|
805
911
|
* real broker, a real port or a real launch.
|
|
806
912
|
*
|
|
807
|
-
* D-07 (01.6.2.1-03-PLAN.md)
|
|
808
|
-
*
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
* own
|
|
812
|
-
*
|
|
813
|
-
*
|
|
814
|
-
*
|
|
815
|
-
*
|
|
816
|
-
*
|
|
817
|
-
*
|
|
818
|
-
*
|
|
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
|
|
915
|
+
* acquirePortAndLaunch() and so never competes for the single in-flight
|
|
916
|
+
* launch slot the way a warm-floor spare launch used to. `serveAcquires()`
|
|
917
|
+
* (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
|
|
921
|
+
* acquires still resolve through the single in-flight owner
|
|
922
|
+
* (acquirePortAndLaunch()'s own invariant comment), which this reordering
|
|
923
|
+
* never weakens). What the fixed order still buys: promoting AFTER serving
|
|
924
|
+
* means an instance that becomes probe-ready DURING this exact tick is not
|
|
925
|
+
* available to any acquire THIS SAME pass -- selectWarmInstance() sees it on
|
|
926
|
+
* the NEXT pass instead, a bound of one poll interval (VICE_BROKER_POLL_MS),
|
|
927
|
+
* never a correctness gap, since a cold acquire finding no ready candidate
|
|
928
|
+
* falls straight through to its own dedicated cold launch rather than
|
|
929
|
+
* waiting on one. */
|
|
819
930
|
export async function runBrokerPass(deps) {
|
|
820
931
|
await deps.serveAcquires();
|
|
821
|
-
await deps.
|
|
932
|
+
await deps.promoteLaunching();
|
|
822
933
|
}
|
|
823
934
|
// ===========================================================================
|
|
824
935
|
// Per-child supervision (Plan 03, Task 2 -- C2/D-23): absorbs
|
|
@@ -857,9 +968,11 @@ function resolveCount(envVar, defaultValue, override) {
|
|
|
857
968
|
* UNEXPLAINED exit, not this one. The pre-kill crash history and backoff
|
|
858
969
|
* are carried forward UNCHANGED, and a pre-kill "granted" state is
|
|
859
970
|
* restored on the fresh record -- the relaunch primitive always creates a
|
|
860
|
-
* new record in the "launching" state, and leaving it there
|
|
861
|
-
*
|
|
862
|
-
*
|
|
971
|
+
* new record in the "launching" state, and leaving it there (once
|
|
972
|
+
* promoted to "ready" by the next probe pass) would let a LATER,
|
|
973
|
+
* UNRELATED acquire's own selectWarmInstance() walk (vice-broker.mts)
|
|
974
|
+
* mistake a recycled session's own machine for an available candidate to
|
|
975
|
+
* grant out from under the session that already owns it.
|
|
863
976
|
* - deliberateKill set WITHOUT respawnAfterKill -> "deliberate_teardown":
|
|
864
977
|
* drop the instance, no respawn. This is T-01.6.2-21's whole point --
|
|
865
978
|
* without reading this flag, every deliberate teardown would respawn
|
|
@@ -883,22 +996,25 @@ async function handleExit(reason, port, deps) {
|
|
|
883
996
|
return;
|
|
884
997
|
}
|
|
885
998
|
const log = deps.log ?? defaultLog;
|
|
886
|
-
// Plan 05 (BROK-02/PROTO-08)
|
|
887
|
-
//
|
|
888
|
-
//
|
|
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,
|
|
889
1003
|
// once, before any of those paths branch, so a client that died without
|
|
890
|
-
// releasing can never hold this lock forever. Redundant
|
|
891
|
-
// respawn/delete paths below (a fresh InstanceRecord never
|
|
892
|
-
//
|
|
893
|
-
// the same reason broker-state.mts's own header comment
|
|
894
|
-
// of the
|
|
895
|
-
// broker-state.mjs's clearMonitorClient()) -- this module's own
|
|
1004
|
+
// releasing can never hold this lock forever on any channel. Redundant
|
|
1005
|
+
// with the respawn/delete paths below (a fresh InstanceRecord never
|
|
1006
|
+
// carries this forward; a deleted one has no field to carry), but
|
|
1007
|
+
// explicit for the same reason broker-state.mts's own header comment
|
|
1008
|
+
// names this as one of the required clearing sites. Assigned directly
|
|
1009
|
+
// (not via broker-state.mjs's clearMonitorClient()) -- this module's own
|
|
896
1010
|
// type-only import of that sibling (see this file's own header comment a
|
|
897
1011
|
// few lines above) is load-bearing: a VALUE import would turn "./broker-
|
|
898
1012
|
// state.mjs" into a real runtime resolution this file cannot satisfy when
|
|
899
1013
|
// loaded directly (as broker-launch.test.ts does), rather than the
|
|
900
|
-
// compiled resources/ sibling this specifier is actually shaped for.
|
|
901
|
-
|
|
1014
|
+
// compiled resources/ sibling this specifier is actually shaped for. `{}`
|
|
1015
|
+
// (not `undefined`) since `monitorClients` is non-optional -- "no claim
|
|
1016
|
+
// on any channel" is an empty map.
|
|
1017
|
+
record.monitorClients = {};
|
|
902
1018
|
if (record.deliberateKill) {
|
|
903
1019
|
if (record.respawnAfterKill) {
|
|
904
1020
|
// Recycle. Capture the pre-kill state, crash history and backoff
|
|
@@ -914,7 +1030,18 @@ async function handleExit(reason, port, deps) {
|
|
|
914
1030
|
// with a brand new record, for the same reason the three values above
|
|
915
1031
|
// are.
|
|
916
1032
|
const preKillRemoteMonitorPort = record.remoteMonitorPort;
|
|
917
|
-
|
|
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.
|
|
1041
|
+
// Captured BEFORE launchSupervised() overwrites this port's map entry
|
|
1042
|
+
// with a brand new record, same as the four values above.
|
|
1043
|
+
const preKillProfile = record.profile;
|
|
1044
|
+
const respawned = launchSupervised(reason, port, deps, preKillCrashTimes, preKillBackoffMs, preKillRemoteMonitorPort, preKillProfile);
|
|
918
1045
|
if (respawned && preKillState === "granted") {
|
|
919
1046
|
respawned.state = "granted";
|
|
920
1047
|
}
|
|
@@ -963,7 +1090,10 @@ async function handleExit(reason, port, deps) {
|
|
|
963
1090
|
// silently strip `-remotemonitor` (and its InstanceRecord field) off the
|
|
964
1091
|
// replacement, which is what made D-13's "the instance record carries it"
|
|
965
1092
|
// stop being true the first time an instance was replaced.
|
|
966
|
-
|
|
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).
|
|
1096
|
+
const respawned = launchSupervised(reason, port, deps, crashTimes, nextBackoffMs, record.remoteMonitorPort, record.profile);
|
|
967
1097
|
deps.onOutcome?.(respawned ? "respawned" : "given_up", port);
|
|
968
1098
|
}
|
|
969
1099
|
/** The single exit-listener installation point in the whole module tree.
|
|
@@ -990,8 +1120,9 @@ async function handleExit(reason, port, deps) {
|
|
|
990
1120
|
export function withCrashSupervision(reason, port, baseSpawn, deps) {
|
|
991
1121
|
// I-1 rider (08.2-06-PLAN.md, Task 1): forwards a third options argument
|
|
992
1122
|
// in the BODY, not just the type -- this is the hop that matters most,
|
|
993
|
-
// because it wraps every real launch path (cold acquire
|
|
994
|
-
//
|
|
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
|
|
995
1126
|
// caller's options at this call site.
|
|
996
1127
|
return (cmd, args, options) => {
|
|
997
1128
|
const child = baseSpawn(cmd, args, options);
|
|
@@ -1024,8 +1155,20 @@ export function withCrashSupervision(reason, port, baseSpawn, deps) {
|
|
|
1024
1155
|
* meanwhile), exactly as it reuses the primary `port` argument; this function
|
|
1025
1156
|
* stays fully synchronous and never allocates. `undefined` is the correct
|
|
1026
1157
|
* value for a FIRST launch through superviseChild() and for every fork launch,
|
|
1027
|
-
* which is why the parameter is optional.
|
|
1028
|
-
|
|
1158
|
+
* which is why the parameter is optional.
|
|
1159
|
+
*
|
|
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
|
|
1166
|
+
* make impossible. `undefined` is correct for a FIRST launch through
|
|
1167
|
+
* 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. */
|
|
1171
|
+
function launchSupervised(reason, port, deps, crashTimes, backoffMs, remoteMonitorPort, profile) {
|
|
1029
1172
|
const supervisorDir = join(deps.stateDir, String(port));
|
|
1030
1173
|
const epochFile = deps.epoch.epochPathFor(deps.stateDir, port);
|
|
1031
1174
|
const logDir = deps.epoch.instanceLogDirFor(deps.stateDir, port);
|
|
@@ -1067,6 +1210,7 @@ function launchSupervised(reason, port, deps, crashTimes, backoffMs, remoteMonit
|
|
|
1067
1210
|
backend: deps.backend,
|
|
1068
1211
|
binmonHost: deps.binmonHost,
|
|
1069
1212
|
remoteMonitorPort,
|
|
1213
|
+
profile,
|
|
1070
1214
|
log: deps.log,
|
|
1071
1215
|
});
|
|
1072
1216
|
if (!record)
|
|
@@ -1103,8 +1247,19 @@ function launchSupervised(reason, port, deps, crashTimes, backoffMs, remoteMonit
|
|
|
1103
1247
|
* (crash respawn with backoff, crash-loop give-up, kill-never-recycle via
|
|
1104
1248
|
* the deliberate-kill marker, and the per-instance boot/crash log), exactly
|
|
1105
1249
|
* mirroring resources/vice-supervisor.sh's own respawn loop but expressed
|
|
1106
|
-
* as an event-loop exit handler instead of a `while true` poll.
|
|
1107
|
-
|
|
1250
|
+
* as an event-loop exit handler instead of a `while true` poll.
|
|
1251
|
+
*
|
|
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. */
|
|
1262
|
+
export function superviseChild(reason, port, deps, remoteMonitorPort) {
|
|
1108
1263
|
const initialBackoffMs = resolveMs("VICE_RESTART_BACKOFF_S", 3, deps.initialBackoffMs);
|
|
1109
|
-
return launchSupervised(reason, port, deps, [], initialBackoffMs);
|
|
1264
|
+
return launchSupervised(reason, port, deps, [], initialBackoffMs, remoteMonitorPort);
|
|
1110
1265
|
}
|