@henols/vice-mcp 0.2.2 → 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.
Files changed (84) hide show
  1. package/README.md +2 -2
  2. package/THIRD-PARTY-NOTICES.md +422 -1
  3. package/anno-bank.ts +171 -0
  4. package/anno-cli.ts +1736 -163
  5. package/anno-confidence.ts +2 -2
  6. package/anno-derive.ts +6 -6
  7. package/anno-details.ts +4 -4
  8. package/anno-enum-gen.ts +416 -30
  9. package/anno-export-asm.ts +1211 -126
  10. package/anno-graphics.ts +338 -0
  11. package/anno-hazard-report.ts +1367 -0
  12. package/anno-import.ts +495 -0
  13. package/anno-index.ts +8 -8
  14. package/anno-join.ts +480 -0
  15. package/anno-memmap-render.ts +22 -21
  16. package/anno-provenance-ledger.ts +472 -0
  17. package/anno-regbits-gen.ts +13 -13
  18. package/anno-register.ts +159 -0
  19. package/anno-store-export.ts +661 -0
  20. package/anno-store.ts +635 -124
  21. package/anno-symbols.ts +7 -7
  22. package/anno-tools.ts +1169 -16
  23. package/anno-types.ts +313 -40
  24. package/backend-detect.mts +124 -312
  25. package/build.ts +3 -1
  26. package/capture-predicate.ts +597 -0
  27. package/channel-lock.ts +349 -0
  28. package/evid-ingest.ts +217 -0
  29. package/evid-reconcile.ts +316 -0
  30. package/host-tool-client.ts +430 -0
  31. package/incident-record.ts +23 -12
  32. package/install-resources.ts +29 -13
  33. package/memmap-lookup.ts +285 -0
  34. package/package.json +27 -8
  35. package/prg-image.ts +1 -2
  36. package/repo-root.ts +87 -3
  37. package/resources/backend-detect.mjs +98 -236
  38. package/resources/broker-control.mjs +220 -54
  39. package/resources/broker-epoch.mjs +7 -8
  40. package/resources/broker-kill.mjs +36 -31
  41. package/resources/broker-launch.mjs +511 -374
  42. package/resources/broker-state.mjs +69 -24
  43. package/resources/container-guard.mjs +1 -1
  44. package/resources/ghidra-project.mjs +790 -0
  45. package/resources/host-tool.mjs +2533 -0
  46. package/resources/vice-broker.mjs +434 -290
  47. package/resources/vice-launcher.sh +127 -9
  48. package/stock-address.ts +1 -1
  49. package/stock-condition.ts +1 -1
  50. package/stock-connect.ts +9 -5
  51. package/stock-derived.ts +29 -37
  52. package/stock-diagnose.ts +200 -36
  53. package/stock-dispatch.ts +179 -77
  54. package/stock-handler.ts +1 -1
  55. package/stock-paths.ts +18 -14
  56. package/stock-petscii.ts +1 -1
  57. package/stock-protocol.ts +1 -1
  58. package/stock-recycle.ts +83 -2
  59. package/stock-reproducible-run.ts +811 -0
  60. package/stock-run-until.ts +100 -1
  61. package/stock-symbols.ts +4 -4
  62. package/stock-timing.ts +1 -1
  63. package/stop-oracle.ts +167 -0
  64. package/text-capability-probe.ts +660 -0
  65. package/text-connect.ts +157 -0
  66. package/text-protocol.ts +810 -0
  67. package/text-tools.ts +778 -0
  68. package/textmon-backtrace.ts +385 -0
  69. package/textmon-cpuhistory.ts +335 -0
  70. package/textmon-memmap.ts +494 -0
  71. package/textmon-profile.ts +458 -0
  72. package/textmon-registers.ts +748 -0
  73. package/tools-manifest.stock.json +864 -3
  74. package/vice-broker-client.ts +253 -108
  75. package/vice-errors.ts +268 -0
  76. package/vice-proxy.ts +339 -2144
  77. package/vsf-slice.ts +640 -0
  78. package/anno-d64.ts +0 -310
  79. package/capability-registry.ts +0 -390
  80. package/refresh-manifest.ts +0 -124
  81. package/tools-manifest.json +0 -1223
  82. package/vice-probe.ts +0 -278
  83. package/vice-sync.ts +0 -336
  84. package/vice.ts +0 -772
@@ -2,126 +2,53 @@
2
2
  // Compiled by `tsc` from backend-detect.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
  // backend-detect.mts
8
8
  //
9
- // The ONE place that decides which VICE build a binary is, and the ONE
10
- // reader of VICE_BACKEND anywhere in this tree (D-01). Everything else that
11
- // needs to know "fork or stock" -- broker-launch.mts's buildViceArgs(),
12
- // vice-broker.mts's startup wiring, and plan 02-08's later connect handshake
13
- // -- calls resolvedBackend() below and threads its answer down, exactly like
14
- // vice.ts's mcpHost() documents for its own container-versus-host question:
15
- // a SECOND independent reader of the same signal is a bug waiting to
16
- // happen the moment one copy is updated and the other is not (mcpHost()'s
17
- // own "three inlined copies" incident is the exact regression this file
18
- // exists to keep from recurring here).
9
+ // FORKRM-01 (plan 52-06): this file used to be the ONE place that decided
10
+ // which VICE build a binary is (the fork's `-mcpserver` flag versus stock's
11
+ // `-binarymonitor`-only surface) and the ONE reader of an environment
12
+ // override naming a backend, anywhere in this tree. There is only one
13
+ // backend now, so there is nothing left to detect between: the `--help`
14
+ // probe (its own spawn, its fork/stock/unknown classifier) and the
15
+ // environment override are both deleted outright, not merely unreachable.
19
16
  //
20
- // WHAT NOT TO DO:
21
- // - Do not call resolvedBackend()/probeBackend() per acquire, per launch,
22
- // or per connect. The broker resolves the backend exactly ONCE, at
23
- // process startup (vice-broker.mts's run()), and passes the resolved
24
- // value down through every real launch call site -- see this module's
25
- // own module-level memo below, which exists as a second line of defence
26
- // against an accidental extra call, not as the PRIMARY mechanism (that
27
- // is the caller only ever invoking this once).
28
- // - Do not call this from inside broker-launch.mts's `inFlight`
29
- // single-owner launch guard. This is a possibly-blocking child-process
30
- // spawn (probeBackend()'s --help probe); anything that can block inside
31
- // that synchronous check-and-set window is the exact failure class the
32
- // 2026-08-01 triple-launch outage came from (D-03/T-02-25).
33
- // - Do not add a trial-launch fallback (launch the binary for real and
34
- // watch what happens) as a second detection mechanism. The `--help`
35
- // probe below runs entirely outside any launch-guarded critical section
36
- // BY CONSTRUCTION -- a trial launch would not. It would also depend on
37
- // RESEARCH.md's assumption A1 (stock's argument parser rejects an
38
- // unknown flag rather than ignoring it), which is UNVERIFIED against a
39
- // real stock binary; see docs/phase2-backend-probe-evidence.md.
17
+ // What SURVIVES, and why:
18
+ // - Binary-path resolution (`resolveBinPath`/`binPathFields`, WR-05) --
19
+ // `vice_ping`'s `resolvedBinaryPath` field and host-tool.mts's sibling
20
+ // binary resolution (`c1541`/`petcat`) both still need "which file did
21
+ // you actually mean", independent of any backend concept.
22
+ // - The on-disk identity/capability cache (BACK-04) -- it still records
23
+ // something worth caching once the backend verdict is gone: the version
24
+ // quad and CPU-history capability a live connect handshake (plan 02-08,
25
+ // stock-connect.ts) attaches to a resolved binary's identity
26
+ // (`resolvedPath`/`mtimeMs`/`sizeBytes`). `readCapabilityRecord()`/
27
+ // `writeCapabilityRecord()` are unchanged in shape; only the cache
28
+ // record's `backend` field is gone, since it never meant anything once
29
+ // there was nothing to distinguish.
30
+ //
31
+ // `ViceBackend` is NARROWED to a single literal (`"stock"`), not deleted:
32
+ // text-capability-probe.ts and stock-connect.ts (both out of this plan's
33
+ // scope -- plan 52-07 owns text-capability-probe.ts's own registry-shaped
34
+ // fork references) still import it as a type-only import, and deleting it
35
+ // outright would break their compile for no benefit this plan is scoped to
36
+ // deliver.
40
37
  //
41
- // ENVIRONMENT CONSTRAINT (2026-08-13, explicit user scope override): no real
42
- // stock or fork VICE binary is reachable from the environment this plan
43
- // executed in, and the user's own ruling for this plan is "we can't do
44
- // tests with deciding what vice is". Every test in backend-detect.test.ts
45
- // therefore drives this module's OVERRIDE path, its on-disk CACHE lifecycle,
46
- // and classifyHelpOutput()'s STRING-PARSING logic against fixture strings
47
- // authored in the test file -- never a real spawned binary. The `--help`
48
- // discriminator itself (does a real stock build's --help output actually
49
- // contain "-binarymonitor" and omit "-mcpserver", the way classifyHelpOutput()
50
- // below assumes) is recorded as an OPEN, not a VERIFIED, question in
51
- // docs/phase2-backend-probe-evidence.md section 2 -- that document's verdict
52
- // is deliberately left standing; nothing in this file's own tests attempts to
53
- // resolve it, and no fixture string anywhere in this tree should ever be
54
- // presented as real captured output from either build. See the follow-up
55
- // todo tracked under .planning/todos/pending/ for what a real-hardware run
56
- // must still confirm.
57
- import { spawnSync } from "node:child_process";
38
+ // WHAT NOT TO DO:
39
+ // - Do not reintroduce an environment-variable backend override, a
40
+ // --help probe, or any other backend-discrimination mechanism. There is
41
+ // one binary shape now; a second detection mechanism would be solving a
42
+ // problem that no longer exists.
43
+ // - Do not call resolvedBackend() from inside broker-launch.mts's
44
+ // `inFlight` single-owner launch guard. This still performs filesystem
45
+ // I/O (a stat call and, on a fresh or changed identity, a cache write);
46
+ // anything that can block inside that synchronous check-and-set window
47
+ // is the exact failure class the 2026-08-01 triple-launch outage came
48
+ // from (D-03/T-02-25) -- unchanged reasoning from before this plan, only
49
+ // the mechanism inside resolvedBackend() changed.
58
50
  import { existsSync, readFileSync, writeFileSync, chmodSync, renameSync, mkdirSync, statSync, } from "node:fs";
59
51
  import { join, resolve as resolvePath } from "node:path";
60
- // ---------------------------------------------------------------------------
61
- // classifyHelpOutput() -- pure string classification, no I/O at all.
62
- // ---------------------------------------------------------------------------
63
- /** Matches on the literal flag tokens D-02 names as the discriminator: the
64
- * fork's `-mcpserver` flag versus stock's `-binarymonitor`-only surface.
65
- * `"fork"` wins when BOTH tokens appear (the fork's own VICE tree is a 3.10
66
- * checkout and accepts both flags) -- checked FIRST, deliberately, so a
67
- * build that advertises both is classified by the flag that actually makes
68
- * it the fork, not merely "also has stock's flag too". `"unknown"` when
69
- * NEITHER token appears -- a real --help transcript that does not match
70
- * either shape, a probe that spawned nothing at all (empty text), or
71
- * anything else this function was never taught to recognise. Never throws;
72
- * pure function of the text it is given. */
73
- export function classifyHelpOutput(text) {
74
- const hasFork = text.includes("-mcpserver");
75
- const hasStock = text.includes("-binarymonitor");
76
- if (hasFork)
77
- return "fork";
78
- if (hasStock)
79
- return "stock";
80
- return "unknown";
81
- }
82
- // ---------------------------------------------------------------------------
83
- // probeBackend() -- the --help probe. spawnSync only, argv array, shell:
84
- // false, never a shell string and never string interpolation of binPath
85
- // into a command line (T-02-03's mitigation). Bounded by a 5000ms timeout
86
- // with kill-on-timeout so a hostile or hung binary cannot stall broker
87
- // startup (T-02-25's second mitigation half).
88
- // ---------------------------------------------------------------------------
89
- const PROBE_TIMEOUT_MS = 5000;
90
- /** `--help` first, falling back to `-help` then `-?` ONLY when a run exits
91
- * non-zero with EMPTY combined output -- a run that exits non-zero but
92
- * still printed something (some builds write usage to stderr and exit 1) is
93
- * already usable and is not retried further. */
94
- const HELP_FLAG_CANDIDATES = ["--help", "-help", "-?"];
95
- function defaultSpawnHelp(binPath, flag) {
96
- try {
97
- const result = spawnSync(binPath, [flag], {
98
- encoding: "utf8",
99
- timeout: PROBE_TIMEOUT_MS,
100
- killSignal: "SIGKILL",
101
- });
102
- const text = `${result.stdout ?? ""}${result.stderr ?? ""}`;
103
- return { text, exitedZero: result.status === 0 };
104
- }
105
- catch {
106
- return { text: "", exitedZero: false };
107
- }
108
- }
109
- /** Runs the fallback ladder above against `binPath` and classifies whatever
110
- * text the LAST attempted flag produced. Never throws -- every failure mode
111
- * (spawn error, timeout, empty output, an exit code the caller does not
112
- * recognise) flows through to classifyHelpOutput() as ordinary text, which
113
- * itself never throws either. */
114
- export function probeBackend(binPath, deps = {}) {
115
- const spawnHelp = deps.spawnHelp ?? defaultSpawnHelp;
116
- let text = "";
117
- for (const flag of HELP_FLAG_CANDIDATES) {
118
- const outcome = spawnHelp(binPath, flag);
119
- text = outcome.text;
120
- if (outcome.exitedZero || text.trim() !== "")
121
- break;
122
- }
123
- return classifyHelpOutput(text);
124
- }
125
52
  /** The client-side capability-decision schema version stamped into every
126
53
  * capability record this module writes (see BackendCacheRecord's own field
127
54
  * comment). BUMP THIS whenever the client's capability probing or the
@@ -165,8 +92,7 @@ function readCacheRecord(supervisorDir) {
165
92
  return null;
166
93
  if (typeof parsed.resolvedPath !== "string" ||
167
94
  typeof parsed.mtimeMs !== "number" ||
168
- typeof parsed.sizeBytes !== "number" ||
169
- (parsed.backend !== "fork" && parsed.backend !== "stock")) {
95
+ typeof parsed.sizeBytes !== "number") {
170
96
  return null;
171
97
  }
172
98
  const record = {
@@ -174,7 +100,6 @@ function readCacheRecord(supervisorDir) {
174
100
  resolvedPath: parsed.resolvedPath,
175
101
  mtimeMs: parsed.mtimeMs,
176
102
  sizeBytes: parsed.sizeBytes,
177
- backend: parsed.backend,
178
103
  probedAt: typeof parsed.probedAt === "string" ? parsed.probedAt : "",
179
104
  };
180
105
  if (typeof parsed.versionQuad === "string")
@@ -186,10 +111,10 @@ function readCacheRecord(supervisorDir) {
186
111
  return record;
187
112
  }
188
113
  /** Tmp-sibling -> chmod 0600 -> content -> rename, the SAME atomic-write
189
- * discipline refresh-manifest.ts's writeManifestAtomic() and vice-broker.mts's
190
- * writeBrokerRecordFile() both already use -- a crash mid-write can only ever
191
- * leave a stray tmp sibling behind, never a truncated or empty file at the
192
- * real cache path that a later read would wrongly accept. */
114
+ * discipline vice-broker.mts's writeBrokerRecordFile() already uses -- a
115
+ * crash mid-write can only ever leave a stray tmp sibling behind, never a
116
+ * truncated or empty file at the real cache path that a later read would
117
+ * wrongly accept. */
193
118
  function writeCacheRecordAtomic(supervisorDir, record) {
194
119
  mkdirSync(supervisorDir, { recursive: true });
195
120
  const finalPath = cachePathFor(supervisorDir);
@@ -223,133 +148,73 @@ function defaultStat(resolvedPath) {
223
148
  return null;
224
149
  }
225
150
  }
226
- function defaultLog(line) {
227
- process.stderr.write(`${line}\n`);
228
- }
229
- // Memoised answer for the probe/cache path ONLY -- the override path
230
- // (VICE_BACKEND set) is always answered fresh, on every call, straight from
231
- // the environment, and never touches this memo (an explicit override can
232
- // legitimately differ from call to call within, e.g., a test process driving
233
- // many scenarios; the detected-backend answer for a fixed binary cannot).
234
- // This is what makes resolvedBackend() answer "once per long-running
235
- // process" for the case that actually spawns something, while never
236
- // requiring a caller to somehow signal "this is a fresh scenario" the way
237
- // container-guard.mts's isInsideContainer() asks callers to pass explicit
238
- // deps to bypass ITS OWN memo -- here, the override/no-override distinction
239
- // already IS that signal.
151
+ // Memoised answer -- a long-running process (the real broker) resolves once
152
+ // per process lifetime; a test suite driving many scenarios calls
153
+ // resetResolvedBackendForTests() between them (see that function's own
154
+ // comment).
240
155
  let memoisedResult = null;
241
- // D-06: gates the "detected backend X for binary Y" stderr note so a
242
- // long-running broker (or a test suite driving resolvedBackend() many times)
243
- // emits it at most once per process -- repo-root.ts's warnedEnvOutsideFrom/
244
- // warnedNoMarkerFound pattern, reused here verbatim.
245
- let warnedBackendUnset = false;
246
- function emitDetectedNote(result, viceBin, log) {
247
- if (warnedBackendUnset)
248
- return;
249
- warnedBackendUnset = true;
250
- log(`vice-broker: detected backend "${result.backend}" for ${viceBin} (source: ${result.source}) -- ` +
251
- `set VICE_BACKEND=stock or VICE_BACKEND=fork to override this detection explicitly`);
252
- }
253
- /** Test-only escape hatch: clears the in-process memo and the D-06
254
- * one-time-note gate. Never called by any real production code path --
255
- * vice-broker.mts calls resolvedBackend() exactly once per real process
256
- * lifetime and has no reason to ever reset it; this exists solely so
257
- * backend-detect.test.ts can drive many distinct scenarios (cache hit, cache
258
- * miss, indeterminate, ...) in one shared test process without one
259
- * scenario's memoised answer contaminating the next -- mirroring
260
- * broker-launch.test.ts's own discipline of restoring module-level state
261
- * between test cases, made explicit here rather than left to careful test
262
- * ordering, since this module's memo (unlike buildViceArgs()'s one-time
263
- * note) has no natural "always widens the same way" ordering to exploit. */
156
+ /** Test-only escape hatch: clears the in-process memo. Never called by any
157
+ * real production code path -- vice-broker.mts calls resolvedBackend()
158
+ * exactly once per real process lifetime and has no reason to ever reset it;
159
+ * this exists solely so backend-detect.test.ts can drive many distinct
160
+ * scenarios (fresh identity, matching cached identity, changed identity, ...)
161
+ * in one shared test process without one scenario's memoised answer
162
+ * contaminating the next. */
264
163
  export function resetResolvedBackendForTests() {
265
164
  memoisedResult = null;
266
- warnedBackendUnset = false;
267
165
  }
268
- /** Honours VICE_BACKEND FIRST, returning immediately without spawning
269
- * anything when it names `stock` or `fork` (BACK-01: one optional config
270
- * value switches backends, no code edit). Otherwise consults the on-disk
271
- * cache (when `supervisorDir` is given and the binary's current
272
- * `{ resolvedPath, mtimeMs, sizeBytes }` all match the stored record); on a
273
- * miss, probes via probeBackend() and writes the cache. Memoises the
274
- * probe/cache answer in a module-level variable so a long-running process
275
- * resolves once (see the memo's own comment above for what "once" means
276
- * here). Never throws: a probe that classifies "unknown" (including a
277
- * spawn failure or a timeout, both of which probeBackend() already reduces
278
- * to "unknown") returns a defined `{ backend: "fork", source:
279
- * "indeterminate", ... }` outcome instead -- "fork" because that is the
280
- * pre-Phase-2 behaviour every existing install already has, so an
281
- * undetectable binary degrades to what already worked rather than to
282
- * nothing. */
283
- /** WR-05: the ONE place `binPath`/`binPathResolved` are derived, so the four
284
- * return paths below cannot disagree about what "the binary" means. A resolved
285
- * absolute path when there is one; the configured name, flagged as unresolved,
286
- * when there is not. */
166
+ /** WR-05: the ONE place `binPath`/`binPathResolved` are derived, so a
167
+ * resolved absolute path when there is one, or the configured name flagged
168
+ * as unresolved when there is not, is computed identically everywhere this
169
+ * file returns it. */
287
170
  function binPathFields(resolvedPath, viceBin) {
288
171
  return resolvedPath !== null ? { binPath: resolvedPath, binPathResolved: true } : { binPath: viceBin, binPathResolved: false };
289
172
  }
173
+ /** Resolves the emulator binary's identity and (re)initialises the on-disk
174
+ * identity/capability cache record for it when a `supervisorDir` is given.
175
+ * Memoises the answer in a module-level variable so a long-running process
176
+ * resolves once (see the memo's own comment above for what "once" means
177
+ * here). Never throws.
178
+ *
179
+ * FORKRM-01: there is nothing left to detect -- `backend` is always
180
+ * `"stock"`. What this function still does is identity resolution (WR-05's
181
+ * `binPath`/`binPathResolved`) and cache bookkeeping for BACK-04's capability
182
+ * record: when the resolved binary's identity (`resolvedPath`/`mtimeMs`/
183
+ * `sizeBytes`) does not match whatever is already on file -- no record at
184
+ * all, or a record describing a DIFFERENT binary (replaced in place, or a
185
+ * fresh install) -- a fresh identity record is written, with no stale
186
+ * capability fields carried over from a different binary's answer. A
187
+ * matching identity is left untouched, so a capability answer already
188
+ * recorded for THIS binary survives. */
290
189
  export function resolvedBackend(deps = {}) {
190
+ if (memoisedResult !== null)
191
+ return memoisedResult;
291
192
  const env = deps.env ?? process.env;
292
193
  const viceBin = deps.viceBin ?? env.VICE_BIN ?? "x64sc";
293
- // A direct read of the real environment on the right of this ternary
294
- // (rather than the generic `env` local above) is deliberate: this file is
295
- // grep-gated, tree-wide, as the ONE place that ever names this variable
296
- // directly against the real environment -- `deps.env` (the test-injection
297
- // seam) still takes precedence when supplied, exactly like every other
298
- // field on this options object.
299
- const override = deps.env ? deps.env.VICE_BACKEND : process.env.VICE_BACKEND;
300
194
  const resolveBinPath = deps.resolveBinPath ?? defaultResolveBinPath;
301
- if (override === "stock" || override === "fork") {
302
- // WR-05: an explicit backend override still resolves the PATH, so
303
- // `vice_ping` reports a real file rather than the bare name. This is a
304
- // filesystem lookup only -- existsSync per PATH entry -- and NEVER a spawn,
305
- // so the override path keeps its "answered fresh, straight from the
306
- // environment, spawns nothing" property.
307
- return { backend: override, source: "override", ...binPathFields(resolveBinPath(viceBin, env), viceBin) };
308
- }
309
- if (memoisedResult !== null)
310
- return memoisedResult;
311
- const log = deps.log ?? defaultLog;
312
195
  const stat = deps.stat ?? defaultStat;
313
- const probe = deps.probe ?? ((bin) => probeBackend(bin));
314
196
  const now = deps.now ?? (() => Date.now());
315
197
  const resolvedPath = resolveBinPath(viceBin, env);
316
198
  const identity = resolvedPath ? stat(resolvedPath) : null;
317
199
  const cacheEligible = resolvedPath !== null && identity !== null && typeof deps.supervisorDir === "string";
318
200
  if (cacheEligible) {
319
- const cached = readCacheRecord(deps.supervisorDir);
320
- if (cached &&
321
- cached.resolvedPath === resolvedPath &&
322
- cached.mtimeMs === identity.mtimeMs &&
323
- cached.sizeBytes === identity.sizeBytes) {
324
- const result = { backend: cached.backend, source: "cache", ...binPathFields(resolvedPath, viceBin) };
325
- memoisedResult = result;
326
- emitDetectedNote(result, viceBin, log);
327
- return result;
201
+ const existing = readCacheRecord(deps.supervisorDir);
202
+ const identityMatches = existing !== null &&
203
+ existing.resolvedPath === resolvedPath &&
204
+ existing.mtimeMs === identity.mtimeMs &&
205
+ existing.sizeBytes === identity.sizeBytes;
206
+ if (!identityMatches) {
207
+ writeCacheRecordAtomic(deps.supervisorDir, {
208
+ version: 1,
209
+ resolvedPath: resolvedPath,
210
+ mtimeMs: identity.mtimeMs,
211
+ sizeBytes: identity.sizeBytes,
212
+ probedAt: new Date(now()).toISOString(),
213
+ });
328
214
  }
329
215
  }
330
- const verdict = probe(viceBin);
331
- if (verdict === "unknown") {
332
- const note = `vice-broker: could not determine whether ${viceBin} is the stock or fork VICE build -- ` +
333
- `its --help output matched neither the -mcpserver nor the -binarymonitor discriminator. ` +
334
- `Set VICE_BACKEND=stock or VICE_BACKEND=fork explicitly.`;
335
- log(note);
336
- const result = { backend: "fork", source: "indeterminate", ...binPathFields(resolvedPath, viceBin), note };
337
- memoisedResult = result;
338
- return result;
339
- }
340
- if (cacheEligible) {
341
- writeCacheRecordAtomic(deps.supervisorDir, {
342
- version: 1,
343
- resolvedPath: resolvedPath,
344
- mtimeMs: identity.mtimeMs,
345
- sizeBytes: identity.sizeBytes,
346
- backend: verdict,
347
- probedAt: new Date(now()).toISOString(),
348
- });
349
- }
350
- const result = { backend: verdict, source: "probe", ...binPathFields(resolvedPath, viceBin) };
216
+ const result = { backend: "stock", ...binPathFields(resolvedPath, viceBin) };
351
217
  memoisedResult = result;
352
- emitDetectedNote(result, viceBin, log);
353
218
  return result;
354
219
  }
355
220
  /** Reads whatever capability answers (BACK-04) are on record for `binPath`
@@ -383,15 +248,13 @@ export function readCapabilityRecord(binPath, deps = {}) {
383
248
  result.capabilitySchema = existing.capabilitySchema;
384
249
  return result;
385
250
  }
386
- /** Attaches `{ versionQuad, cpuHistoryAvailable }` to the EXISTING backend
387
- * verdict already on record for `binPath`'s resolved identity -- a no-op,
388
- * never a throw, when there is no such matching record yet (no supervisorDir
251
+ /** Attaches `{ versionQuad, cpuHistoryAvailable }` to the EXISTING identity
252
+ * record already on file for `binPath`'s resolved identity -- a no-op, never
253
+ * a throw, when there is no such matching record yet (no supervisorDir
389
254
  * given, the binary cannot be resolved or stat'd, or the cache names a
390
- * different binary or has no verdict at all). This function never invents a
391
- * backend verdict of its own: it can only EXTEND a record resolvedBackend()
392
- * already wrote, since a `--help` probe has no way to observe a version
393
- * quad and this function must not silently fabricate the field it did not
394
- * observe either. */
255
+ * different binary or has no record at all). This function never invents an
256
+ * identity record of its own: it can only EXTEND a record resolvedBackend()
257
+ * already wrote. */
395
258
  export function writeCapabilityRecord(binPath, capability, deps = {}) {
396
259
  if (typeof deps.supervisorDir !== "string")
397
260
  return;
@@ -412,7 +275,6 @@ export function writeCapabilityRecord(binPath, capability, deps = {}) {
412
275
  resolvedPath,
413
276
  mtimeMs: identity.mtimeMs,
414
277
  sizeBytes: identity.sizeBytes,
415
- backend: existing.backend,
416
278
  probedAt: existing.probedAt,
417
279
  versionQuad: capability.versionQuad,
418
280
  cpuHistoryAvailable: capability.cpuHistoryAvailable,