@henols/vice-mcp 0.2.1 → 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 +4 -3
- package/THIRD-PARTY-NOTICES.md +423 -25
- package/{r2000-acme-ident.ts → anno-acme-ident.ts} +13 -13
- package/anno-bank.ts +171 -0
- package/anno-cli.ts +3040 -0
- package/{r2000-confidence.ts → anno-confidence.ts} +22 -22
- package/anno-coverage.ts +2465 -0
- package/anno-derive.ts +590 -0
- package/anno-details.ts +169 -0
- package/anno-enum-gen.ts +919 -0
- package/anno-export-asm.ts +2396 -0
- package/anno-graphics.ts +338 -0
- package/anno-hazard-report.ts +1367 -0
- package/anno-import.ts +495 -0
- package/anno-index.ts +150 -0
- package/anno-join.ts +480 -0
- package/{r2000-memmap-render.ts → anno-memmap-render.ts} +236 -95
- package/anno-provenance-ledger.ts +472 -0
- package/{r2000-regbits-gen.ts → anno-regbits-gen.ts} +20 -15
- package/{r2000-regbits.json → anno-regbits.json} +2 -2
- package/anno-register.ts +399 -0
- package/anno-store-export.ts +661 -0
- package/anno-store.ts +4002 -0
- package/anno-symbols.ts +266 -0
- package/anno-tools.ts +3264 -0
- package/anno-types.ts +1909 -0
- package/backend-detect.mts +124 -312
- package/block-class.ts +201 -0
- package/build.ts +4 -2
- package/capture-predicate.ts +597 -0
- package/channel-lock.ts +349 -0
- package/disasm-decoder.ts +14 -14
- package/disasm-opcodes.ts +4 -4
- package/disasm-renderer.ts +2 -2
- package/evid-ingest.ts +217 -0
- package/evid-reconcile.ts +316 -0
- package/host-tool-client.ts +430 -0
- package/hostpath.ts +1 -1
- package/incident-record.ts +23 -12
- package/install-resources.ts +30 -14
- package/memmap-lookup.ts +285 -0
- package/package.json +48 -23
- package/prg-image.ts +118 -0
- package/repo-root.ts +107 -8
- 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 +373 -214
- 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 +130 -12
- package/stock-address.ts +6 -6
- package/stock-cia.ts +2 -2
- package/stock-condition.ts +8 -8
- package/stock-connect.ts +10 -6
- package/stock-derived.ts +29 -37
- package/stock-diagnose.ts +200 -36
- package/stock-dispatch.ts +200 -68
- package/stock-execution.ts +5 -3
- package/stock-handler.ts +1 -1
- package/stock-input.ts +9 -9
- package/stock-machine.ts +17 -6
- package/stock-paths.ts +18 -14
- package/stock-petscii.ts +1 -1
- package/stock-protocol.ts +17 -12
- package/stock-recycle.ts +83 -2
- package/stock-registers.ts +54 -29
- package/stock-reproducible-run.ts +811 -0
- package/stock-run-until.ts +100 -1
- package/stock-sprites.ts +3 -3
- package/stock-symbols.ts +13 -13
- package/stock-timing.ts +2 -2
- package/stock-vicii.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/version.ts +1 -1
- package/vice-broker-client.ts +189 -42
- package/vice-errors.ts +268 -0
- package/vice-proxy.ts +392 -2175
- package/vsf-slice.ts +640 -0
- package/capability-registry.ts +0 -388
- package/r2000-cli.ts +0 -1103
- package/r2000-d64.ts +0 -310
- package/r2000-enum-gen.ts +0 -574
- package/r2000-launch.ts +0 -357
- package/r2000-mcp-client.ts +0 -596
- package/r2000-project.ts +0 -190
- package/r2000-symbols.ts +0 -388
- package/r2000-tools.ts +0 -914
- package/r2000-verify.ts +0 -184
- 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
package/stock-protocol.ts
CHANGED
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
import { EventEmitter } from "node:events";
|
|
42
42
|
import net from "node:net";
|
|
43
43
|
|
|
44
|
-
import { ViceError } from "./vice.ts";
|
|
44
|
+
import { ViceError } from "./vice-errors.ts";
|
|
45
45
|
|
|
46
46
|
// ---------------------------------------------------------------------------
|
|
47
47
|
// Wire constants (hand-copied, not imported -- see header comment above)
|
|
@@ -720,12 +720,15 @@ export function registersSetBody({ memspace, items }: RegistersSetBodyOptions):
|
|
|
720
720
|
|
|
721
721
|
// ---------------------------------------------------------------------------
|
|
722
722
|
// Execution and machine-control body encoders (Phase 3, Task 2). Every
|
|
723
|
-
// body layout below is [CITED] against the official VICE manual (§13)
|
|
724
|
-
//
|
|
725
|
-
//
|
|
726
|
-
//
|
|
727
|
-
//
|
|
728
|
-
//
|
|
723
|
+
// body layout below is [CITED] against the official VICE manual (§13);
|
|
724
|
+
// each encoder's own JSDoc states whether its behavioural (not wire-shape)
|
|
725
|
+
// claim has since been live-probed. RESEARCH.md Assumptions Log row A2 was
|
|
726
|
+
// closed CONFIRMED by 13-PROBE-RESULTS.md. Two rows below are still
|
|
727
|
+
// unconfirmed -- A3 stayed INCONCLUSIVE (no observable signal either way)
|
|
728
|
+
// and A5 was CONTRADICTED (still pending its correction) -- and each of
|
|
729
|
+
// those two encoders' own JSDoc, below, still carries its own [ASSUMED]
|
|
730
|
+
// naming that row and pointing at .planning/todos/pending/ for the
|
|
731
|
+
// remaining probe debt.
|
|
729
732
|
// ---------------------------------------------------------------------------
|
|
730
733
|
|
|
731
734
|
export interface AdvanceInstructionsBodyOptions {
|
|
@@ -739,9 +742,11 @@ export interface AdvanceInstructionsBodyOptions {
|
|
|
739
742
|
* exercised, with stepOver=0 only, in probe-binmon.mjs's async-events check]
|
|
740
743
|
*
|
|
741
744
|
* `stepOver = true`'s runtime meaning (skip a `JSR`'s subroutine as one
|
|
742
|
-
* step, matching the fork's own `stepOver` field name)
|
|
743
|
-
*
|
|
744
|
-
*
|
|
745
|
+
* step, matching the fork's own `stepOver` field name) was live-probed
|
|
746
|
+
* against a real `JSR` on fork VICE 3.10 and CONFIRMED (RESEARCH.md
|
|
747
|
+
* Assumptions Log row A2, closed by 13-PROBE-RESULTS.md §A2): the post-step
|
|
748
|
+
* PC landed at JSR+3, reproduced identically across two independent live
|
|
749
|
+
* sessions.
|
|
745
750
|
*/
|
|
746
751
|
export function advanceInstructionsBody({ stepOver = false, count = 1 }: AdvanceInstructionsBodyOptions = {}): Buffer {
|
|
747
752
|
if (!Number.isInteger(count) || count < 1 || count > 0xffff) {
|
|
@@ -816,8 +821,8 @@ export interface ResetBodyOptions {
|
|
|
816
821
|
* [CITED docs/phase0-binmon-findings.md §5]
|
|
817
822
|
*
|
|
818
823
|
* NOT the RESOURCE_SET (0x52) power-cycle hazard CLAUDE.md warns about
|
|
819
|
-
* (`MachineVideoStandard`/`VICIIModel`/`MachinePowerFrequency`,
|
|
820
|
-
*
|
|
824
|
+
* (`MachineVideoStandard`/`VICIIModel`/`MachinePowerFrequency`, the CUT
|
|
825
|
+
* vice_machine_config_get/set pair's resources) -- a distinct opcode, and an agent-requested hard
|
|
821
826
|
* reset via RESET is exactly what DIRECT-06 asks for. It needs no
|
|
822
827
|
* deny-list. This is RESEARCH.md's Pitfall 1; this comment is what stops a
|
|
823
828
|
* later reviewer from "fixing" it by adding one.
|
package/stock-recycle.ts
CHANGED
|
@@ -64,7 +64,7 @@ import { handleRegistersGet } from "./stock-registers.ts";
|
|
|
64
64
|
import { stockAnswer, isErrorText, type StockSessionHandler, type StockToolResult } from "./stock-handler.ts";
|
|
65
65
|
import { stockDisconnect, type StockConnectSession } from "./stock-connect.ts";
|
|
66
66
|
import type { StockDispatchDeps } from "./stock-dispatch.ts";
|
|
67
|
-
import { readEpoch } from "./vice.ts";
|
|
67
|
+
import { readEpoch } from "./vice-errors.ts";
|
|
68
68
|
|
|
69
69
|
function describeError(err: unknown): string {
|
|
70
70
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -307,6 +307,81 @@ export async function gatherStockWedgeEvidence(session: StockConnectSession, dep
|
|
|
307
307
|
* per-outcome vocabulary is the same one. Redeclared locally rather than
|
|
308
308
|
* imported: importing it would mean importing vice-proxy.ts, which this
|
|
309
309
|
* module must never do. */
|
|
310
|
+
const DEFAULT_RECYCLE_EPOCH_POLL_TIMEOUT_MS = 3000;
|
|
311
|
+
const RECYCLE_EPOCH_POLL_INTERVAL_MS = 50;
|
|
312
|
+
|
|
313
|
+
/** Read fresh on EVERY call -- same load-time-vs-call-time reasoning as
|
|
314
|
+
* stockCaptureStepTimeoutMs() above: a static `import` is hoisted ahead of
|
|
315
|
+
* any top-level statement in the importing file, so a module-level constant
|
|
316
|
+
* computed once at load time could never be retuned by a test that sets
|
|
317
|
+
* `process.env` afterwards. Deliberately its OWN environment variable,
|
|
318
|
+
* distinct from `VICE_RECYCLE_CAPTURE_TIMEOUT_MS` -- that knob bounds one
|
|
319
|
+
* evidence-gathering step before the kill; this one bounds the epoch poll
|
|
320
|
+
* after it, and a single shared knob would let retuning one silently retune
|
|
321
|
+
* the other. Exported so the test file can assert the default directly. */
|
|
322
|
+
export function stockRecycleEpochPollTimeoutMs(): number {
|
|
323
|
+
const raw = process.env.VICE_RECYCLE_EPOCH_POLL_TIMEOUT_MS;
|
|
324
|
+
if (raw === undefined || raw === "") return DEFAULT_RECYCLE_EPOCH_POLL_TIMEOUT_MS;
|
|
325
|
+
const parsed = Number(raw);
|
|
326
|
+
// A non-positive deadline would make the poll finish before it ever reads,
|
|
327
|
+
// so every confirmed kill would record a null epoch_after -- and the field
|
|
328
|
+
// would look present (the producer ran) while carrying no information at
|
|
329
|
+
// all, which is worse than the missing producer this poll exists to fix.
|
|
330
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
331
|
+
console.error(
|
|
332
|
+
`VICE_RECYCLE_EPOCH_POLL_TIMEOUT_MS=${JSON.stringify(raw)} is not a positive number of milliseconds -- ignoring it and using the ` +
|
|
333
|
+
`default ${DEFAULT_RECYCLE_EPOCH_POLL_TIMEOUT_MS}ms. A value of 0 would end the post-kill epoch poll before its first read, so a ` +
|
|
334
|
+
"confirmed kill would always record a null epoch_after, indistinguishable from a genuine stall.",
|
|
335
|
+
);
|
|
336
|
+
return DEFAULT_RECYCLE_EPOCH_POLL_TIMEOUT_MS;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function sleep(ms: number): Promise<void> {
|
|
340
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** The ONE predicate that may promote a post-kill read into the record. A
|
|
344
|
+
* present-but-unchanged value must stay out: a pair of equal before/after
|
|
345
|
+
* numbers reads to a future investigator as a confirmed no-turnover, which is
|
|
346
|
+
* a false claim for a kill the guard above this call already established
|
|
347
|
+
* succeeded. Absence of a pre-kill epoch (a lease that never had one) makes
|
|
348
|
+
* the first present read count as an advance -- there is nothing higher than
|
|
349
|
+
* "nothing" to compare against. */
|
|
350
|
+
function epochAdvanced(before: number | null, after: ReturnType<typeof readEpoch>): boolean {
|
|
351
|
+
if (!after.present || after.epoch === null) return false;
|
|
352
|
+
return before === null || after.epoch > before;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Bounded post-kill epoch poll. Reuses the SAME dependency-injected reader
|
|
357
|
+
* and file path the pre-kill read above already used, so the before and
|
|
358
|
+
* after values come from one source. Loops until either the epoch has
|
|
359
|
+
* advanced (epochAdvanced() above) or its own wall-clock deadline passes,
|
|
360
|
+
* clamping the final sleep so the loop cannot overshoot that deadline. A
|
|
361
|
+
* lease with no epoch file polls nothing and resolves null immediately --
|
|
362
|
+
* there is nothing to read. A throw from the reader is treated as a read
|
|
363
|
+
* that did not advance, never as a fatal error on a path that runs after a
|
|
364
|
+
* destructive action has already happened.
|
|
365
|
+
*/
|
|
366
|
+
async function pollEpochAfter(readEpochFn: typeof readEpoch, epochFile: string, epochBefore: number | null): Promise<number | null> {
|
|
367
|
+
if (!epochFile) return null;
|
|
368
|
+
const deadline = Date.now() + stockRecycleEpochPollTimeoutMs();
|
|
369
|
+
for (;;) {
|
|
370
|
+
let result: ReturnType<typeof readEpoch> | null;
|
|
371
|
+
try {
|
|
372
|
+
result = readEpochFn(epochFile);
|
|
373
|
+
} catch {
|
|
374
|
+
result = null;
|
|
375
|
+
}
|
|
376
|
+
if (result && epochAdvanced(epochBefore, result)) {
|
|
377
|
+
return result.epoch;
|
|
378
|
+
}
|
|
379
|
+
const remaining = deadline - Date.now();
|
|
380
|
+
if (remaining <= 0) return null;
|
|
381
|
+
await sleep(Math.min(RECYCLE_EPOCH_POLL_INTERVAL_MS, remaining));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
310
385
|
function recycleAckOutcomeMessage(ack: { outcome: string; kill_stage: string; reason: string }): string {
|
|
311
386
|
const stage = ack.kill_stage || "unknown";
|
|
312
387
|
const reasonSuffix = ack.reason ? ` (${ack.reason})` : "";
|
|
@@ -472,7 +547,13 @@ export async function handleRecycleStock(args: Record<string, unknown>, session:
|
|
|
472
547
|
return isErrorText(`vice_recycle: ${recycleAckOutcomeMessage(ack)} Incident record: ${recordPath}.`);
|
|
473
548
|
}
|
|
474
549
|
|
|
475
|
-
|
|
550
|
+
// Post-kill epoch poll -- the record's own `epoch_after` producer. Only
|
|
551
|
+
// reached on a confirmed kill: a refusal, a timeout or a broker-gone
|
|
552
|
+
// outcome each leave the machine's state unknown, and polling for an
|
|
553
|
+
// epoch advance on any of those would invent a fact this handler has no
|
|
554
|
+
// basis for.
|
|
555
|
+
const epochAfter = await pollEpochAfter(readEpochFn, lease.epochFile, epochBefore);
|
|
556
|
+
finaliseIncidentRecord(recordPath, { outcome: "ok", kill_stage: killStage, epoch_after: epochAfter });
|
|
476
557
|
|
|
477
558
|
// stockAnswer() stamps runState from session.client -- read BEFORE the
|
|
478
559
|
// teardown below disconnects it, so the answer reports the machine's
|
package/stock-registers.ts
CHANGED
|
@@ -68,12 +68,19 @@ export interface RegisterCatalog {
|
|
|
68
68
|
/** The one module-level catalog map, keyed on the session object itself --
|
|
69
69
|
* NOT on session.client -- so a fresh stockReconnect() (which returns a
|
|
70
70
|
* brand-new session) is indistinguishable from "never fetched" and simply
|
|
71
|
-
* fetches again, with no manual invalidation path required anywhere.
|
|
71
|
+
* fetches again, with no manual invalidation path required anywhere.
|
|
72
|
+
*
|
|
73
|
+
* Caches the in-flight PROMISE, not the resolved catalog (15-04, IN-02):
|
|
74
|
+
* two handlers racing on a fresh session both read `catalogs.get(session)`
|
|
75
|
+
* before either write lands, so caching only the resolved value let both
|
|
76
|
+
* send their own REGISTERS_AVAILABLE. Caching the promise means the second
|
|
77
|
+
* caller awaits the SAME in-flight request. A rejected promise is evicted
|
|
78
|
+
* (see registerCatalogFor()) so a failed fetch is retried, never memoised. */
|
|
72
79
|
// Single-line by design: the ONLY line in this file naming the garbage-
|
|
73
80
|
// collectable, session-keyed map primitive directly (grep-gated -- see
|
|
74
81
|
// this plan's own acceptance criteria). Every other reference goes
|
|
75
82
|
// through this factory, never a second construction call site.
|
|
76
|
-
function freshCatalogMap(): WeakMap<StockConnectSession, RegisterCatalog
|
|
83
|
+
function freshCatalogMap(): WeakMap<StockConnectSession, Promise<RegisterCatalog>> { return new WeakMap<StockConnectSession, Promise<RegisterCatalog>>(); }
|
|
77
84
|
|
|
78
85
|
let catalogs = freshCatalogMap();
|
|
79
86
|
|
|
@@ -86,15 +93,22 @@ export function resetRegisterCatalogsForTest(): void {
|
|
|
86
93
|
|
|
87
94
|
/**
|
|
88
95
|
* Resolves `session`'s register catalog, fetching it through
|
|
89
|
-
* REGISTERS_AVAILABLE (0x83) exactly once and caching the
|
|
90
|
-
* session object
|
|
91
|
-
*
|
|
96
|
+
* REGISTERS_AVAILABLE (0x83) exactly once and caching the IN-FLIGHT
|
|
97
|
+
* PROMISE on the session object -- not just the resolved value, so two
|
|
98
|
+
* concurrent callers on a fresh session (before either fetch has
|
|
99
|
+
* resolved) share the SAME REGISTERS_AVAILABLE round trip rather than
|
|
100
|
+
* each sending their own (15-04, IN-02). Every subsequent call for the
|
|
101
|
+
* SAME session object returns the same promise (and, once it settles,
|
|
102
|
+
* the same resolved catalog) with no further wire traffic.
|
|
92
103
|
*
|
|
93
104
|
* Refuses (throws a plain Error, converted by the caller through
|
|
94
105
|
* convertWireError()) an empty enumeration rather than caching it: a
|
|
95
106
|
* build that enumerates zero registers cannot support
|
|
96
107
|
* vice_registers_set, and that failure must be visible on every call,
|
|
97
|
-
* never silently cached as "zero registers, nothing to resolve".
|
|
108
|
+
* never silently cached as "zero registers, nothing to resolve". A
|
|
109
|
+
* rejected fetch, whether from an empty enumeration or a wire error, is
|
|
110
|
+
* evicted from the cache before this function returns, so the NEXT call retries instead
|
|
111
|
+
* of permanently memoising the failure.
|
|
98
112
|
*/
|
|
99
113
|
export async function registerCatalogFor(session: StockConnectSession): Promise<RegisterCatalog> {
|
|
100
114
|
const existing = catalogs.get(session);
|
|
@@ -102,30 +116,41 @@ export async function registerCatalogFor(session: StockConnectSession): Promise<
|
|
|
102
116
|
return existing;
|
|
103
117
|
}
|
|
104
118
|
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
"
|
|
113
|
-
|
|
114
|
-
|
|
119
|
+
const pending = (async (): Promise<RegisterCatalog> => {
|
|
120
|
+
const response = await session.client.send(CommandType.RegistersAvailable, memspaceBody({ memspace: 0x00 }));
|
|
121
|
+
if (response.type !== "registers_available") {
|
|
122
|
+
throw new Error(`registerCatalogFor: expected a registers_available reply, got "${response.type}"`);
|
|
123
|
+
}
|
|
124
|
+
if (response.registers.length === 0) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
"registerCatalogFor: the connected VICE build enumerated zero registers via REGISTERS_AVAILABLE -- " +
|
|
127
|
+
"it cannot support vice_registers_set, and this failure must be named rather than cached as an empty catalog",
|
|
128
|
+
);
|
|
129
|
+
}
|
|
115
130
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
131
|
+
const byName = new Map<string, { id: number; sizeBits: number; name: string }>();
|
|
132
|
+
const byId = new Map<number, { sizeBits: number; name: string }>();
|
|
133
|
+
for (const reg of response.registers) {
|
|
134
|
+
// reg.size is stock-protocol.ts's own field name for the wire's size
|
|
135
|
+
// byte (its parser is unchanged by this plan); this module renames it
|
|
136
|
+
// to sizeBits at the point it enters the catalog so every downstream
|
|
137
|
+
// reader sees the unit named in the type.
|
|
138
|
+
byName.set(reg.name.toUpperCase(), { id: reg.id, sizeBits: reg.size, name: reg.name });
|
|
139
|
+
byId.set(reg.id, { sizeBits: reg.size, name: reg.name });
|
|
140
|
+
}
|
|
141
|
+
return { byName, byId };
|
|
142
|
+
})();
|
|
143
|
+
|
|
144
|
+
catalogs.set(session, pending);
|
|
145
|
+
// Evict on rejection so a failed fetch is retried by the next call
|
|
146
|
+
// rather than memoised forever -- only if no newer promise has already
|
|
147
|
+
// replaced this one for the same session.
|
|
148
|
+
pending.catch(() => {
|
|
149
|
+
if (catalogs.get(session) === pending) {
|
|
150
|
+
catalogs.delete(session);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
return pending;
|
|
129
154
|
}
|
|
130
155
|
|
|
131
156
|
// ---------------------------------------------------------------------------
|