@henols/c64-re-tools 2.0.0-rc.4 → 2.0.0-rc.5

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 (46) hide show
  1. package/README.md +3 -1
  2. package/distribution/package.ts +1 -16
  3. package/distribution/plugin.ts +5 -3
  4. package/package.json +1 -1
  5. package/skills/c64-basic/SKILL.md +3 -3
  6. package/skills/c64-emulator/SKILL.md +2 -2
  7. package/skills/c64-knowledge/SKILL.md +17 -3
  8. package/skills/c64-knowledge/scripts/knowledge.ts +5 -2
  9. package/skills/c64-memory-map/scripts/memmap.ts +1 -1
  10. package/skills/c64-provenance/SKILL.md +3 -2
  11. package/skills/c64-provenance/scripts/provenance.ts +4 -2
  12. package/skills/c64-provenance/scripts/runtime.ts +14 -2
  13. package/skills/c64-source/SKILL.md +4 -4
  14. package/skills/c64-source/scripts/generate.ts +57 -14
  15. package/skills/c64-source/scripts/loader.ts +5 -1
  16. package/skills/c64-source/scripts/source.ts +3 -1
  17. package/skills/c64-static-analysis/SKILL.md +2 -2
  18. package/skills/c64-static-analysis/scripts/analyze.ts +8 -4
  19. package/skills/c64-unpacker/SKILL.md +5 -5
  20. package/skills/c64-unpacker/scripts/evidence.ts +29 -22
  21. package/skills/c64-unpacker/scripts/loads.ts +30 -7
  22. package/skills/c64-unpacker/scripts/stages.ts +67 -0
  23. package/skills/c64-unpacker/scripts/unpack.ts +167 -47
  24. package/src/c64.ts +37 -12
  25. package/src/cli/main.ts +7 -4
  26. package/src/cli/status.ts +9 -7
  27. package/src/host/main.ts +8 -3
  28. package/src/host/tools/petcat.ts +5 -1
  29. package/src/host/tools/status.ts +8 -2
  30. package/src/host/vice/process.ts +10 -5
  31. package/src/host/vice/session.ts +26 -13
  32. package/src/host-client/connect.ts +2 -1
  33. package/src/host-client/coverage.ts +0 -5
  34. package/src/host-client/vice-session.ts +37 -4
  35. package/src/knowledge/history.ts +8 -0
  36. package/src/knowledge/import.ts +34 -3
  37. package/src/knowledge/write.ts +2 -1
  38. package/src/mcp/main.ts +43 -15
  39. package/src/mcp/server.ts +9 -2
  40. package/src/mcp/tools/execution.ts +3 -2
  41. package/src/mcp/tools/input.ts +6 -4
  42. package/src/mcp/tools/media.ts +4 -3
  43. package/src/native/discover.ts +38 -4
  44. package/src/package.ts +21 -0
  45. package/src/protocol/tools.ts +7 -3
  46. package/src/protocol/vice.ts +14 -6
package/src/c64.ts CHANGED
@@ -128,7 +128,7 @@ export function textToPetsciiName(text: string): Uint8Array {
128
128
  /** BASIC refuses higher line numbers when a line is entered. */
129
129
  const MAX_LINE_NUMBER = 63999;
130
130
 
131
- const TOKEN = { data: 0x83, rem: 0x8f, sys: 0x9e, usr: 0xb7, plus: 0xaa, minus: 0xab, times: 0xac, divide: 0xad } as const;
131
+ const TOKEN = { data: 0x83, rem: 0x8f, sys: 0x9e, usr: 0xb7, plus: 0xaa, minus: 0xab, times: 0xac, divide: 0xad, pi: 0xff } as const;
132
132
  const QUOTE = 0x22;
133
133
  const COLON = 0x3a;
134
134
 
@@ -138,15 +138,27 @@ export interface TokenizedLine {
138
138
  tokens: Uint8Array;
139
139
  }
140
140
 
141
- /** A machine-code handoff in BASIC program text: SYS with a constant address, or a SYS or USR whose target needs run-time values. */
142
- export type BasicHandoff = { kind: "sys"; line: number; address: number } | { kind: "sys" | "usr"; line: number; computed: true };
141
+ /**
142
+ * A machine-code handoff in BASIC program text: SYS with a constant address, a
143
+ * SYS whose argument needs run-time values (with the argument as text), or a USR.
144
+ */
145
+ export type BasicHandoff =
146
+ | { kind: "sys"; line: number; address: number }
147
+ | { kind: "sys"; line: number; computed: true; expression: string }
148
+ | { kind: "usr"; line: number; computed: true };
143
149
 
144
- export type TokenizedProgram = { loadAddress: number; basicEnd: number; lines: TokenizedLine[] } | { reason: string };
150
+ /**
151
+ * `basicEnd` is the first address that holds no BASIC; `textBytes` is the
152
+ * number of file bytes (load address included) before the end of the BASIC text.
153
+ */
154
+ export type TokenizedProgram = { loadAddress: number; basicEnd: number; textBytes: number; lines: TokenizedLine[] } | { reason: string };
145
155
 
146
156
  /**
147
157
  * Splits a PRG into BASIC lines the way BASIC relinks a loaded program: each
148
158
  * line ends at a zero byte, and a link whose high byte is zero ends the
149
- * program. The stored links are not trusted.
159
+ * program. The stored links are not trusted. A real end marker is two zero
160
+ * bytes; when only the high byte is zero, the "marker" is the first machine
161
+ * code after the BASIC text (SYS lines that jump right behind themselves).
150
162
  */
151
163
  export function parseTokenized(program: Uint8Array): TokenizedProgram {
152
164
  if (program.length < 4) return { reason: "The file is too short for a BASIC program." };
@@ -157,12 +169,13 @@ export function parseTokenized(program: Uint8Array): TokenizedProgram {
157
169
  for (;;) {
158
170
  // A file that ends right after a line leaves the end marker to the zeros after it in memory;
159
171
  // basicEnd is the address after that marker, as when the file holds it.
160
- if (at === program.length && lines.length > 0) return { loadAddress, basicEnd: loadAddress + at, lines };
172
+ if (at === program.length && lines.length > 0) return { loadAddress, basicEnd: loadAddress + at, textBytes: at, lines };
161
173
  if (at + 1 >= program.length) return { reason: "The BASIC program has no end marker." };
162
174
  if (program[at + 1] === 0) {
163
175
  // An end marker before any line, with more bytes after it, is how machine code at a non-BASIC address often starts.
164
176
  if (lines.length === 0 && at + 2 < program.length) return { reason: "The file holds no BASIC line before other bytes, so it is no BASIC program." };
165
- return { loadAddress, basicEnd: loadAddress + at, lines };
177
+ const marker = program[at] === 0;
178
+ return { loadAddress, basicEnd: loadAddress + (marker ? at : at - 2), textBytes: at, lines };
166
179
  }
167
180
  if (at + 4 > program.length) return { reason: "The last BASIC line is cut off." };
168
181
  const number = program[at + 2]! | (program[at + 3]! << 8);
@@ -188,13 +201,20 @@ const KEYWORDS = [
188
201
  * as it is, and {$xx} for bytes that are no character. For evidence, not a listing.
189
202
  */
190
203
  export function basicLineText(line: TokenizedLine): string {
191
- let text = `${line.number} `;
204
+ return `${line.number} ${basicText(line.tokens)}`;
205
+ }
206
+
207
+ /** Tokenized BASIC bytes as text, as for basicLineText; pi is π. */
208
+ function basicText(tokens: Uint8Array): string {
209
+ let text = "";
192
210
  let literal = false;
193
- for (const byte of line.tokens) {
211
+ for (const byte of tokens) {
194
212
  if (byte === QUOTE) literal = !literal;
195
213
  if (!literal && byte >= 0x80 && byte < 0x80 + KEYWORDS.length) {
196
214
  text += KEYWORDS[byte - 0x80];
197
215
  if (byte === TOKEN.rem) literal = true;
216
+ } else if (!literal && byte === TOKEN.pi) {
217
+ text += "π";
198
218
  } else {
199
219
  text += petsciiNameToText(Uint8Array.of(byte));
200
220
  }
@@ -203,7 +223,7 @@ export function basicLineText(line: TokenizedLine): string {
203
223
  }
204
224
 
205
225
  /**
206
- * Evaluates a SYS argument that holds only numbers, + - * / and brackets.
226
+ * Evaluates a SYS argument that holds only numbers, π, + - * / and brackets.
207
227
  * Returns undefined for anything that needs run-time values.
208
228
  */
209
229
  export function constantExpression(bytes: Uint8Array): number | undefined {
@@ -242,6 +262,10 @@ export function constantExpression(bytes: Uint8Array): number | undefined {
242
262
  if (value === undefined || tokens[at++] !== 0x29) return undefined;
243
263
  return value;
244
264
  }
265
+ if (peek() === TOKEN.pi) {
266
+ at++;
267
+ return Math.PI;
268
+ }
245
269
  return number();
246
270
  };
247
271
  const product = (): number | undefined => {
@@ -297,8 +321,9 @@ export function findHandoffs(lines: TokenizedLine[]): BasicHandoff[] {
297
321
  // The argument ends at the statement end or at a comma (extra SYS parameters).
298
322
  let end = at + 1;
299
323
  while (end < tokens.length && tokens[end] !== COLON && tokens[end] !== 0x2c) end++;
300
- const value = constantExpression(tokens.subarray(at + 1, end));
301
- if (value === undefined) handoffs.push({ kind: "sys", line: number, computed: true });
324
+ const argument = tokens.subarray(at + 1, end);
325
+ const value = constantExpression(argument);
326
+ if (value === undefined) handoffs.push({ kind: "sys", line: number, computed: true, expression: basicText(argument).trim() });
302
327
  // BASIC drops the fraction; outside 0-65535 SYS stops with ILLEGAL QUANTITY and jumps nowhere.
303
328
  else if (Math.trunc(value) >= 0 && Math.trunc(value) <= 0xffff) handoffs.push({ kind: "sys", line: number, address: Math.trunc(value) });
304
329
  at = end - 1;
package/src/cli/main.ts CHANGED
@@ -7,7 +7,7 @@ import { dirname, join } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { parseArgs } from "node:util";
9
9
 
10
- import { PACKAGE } from "../../distribution/package.ts";
10
+ import { PACKAGE } from "../package.ts";
11
11
  import { localToolContext } from "../native/local.ts";
12
12
  import { localToolStatus } from "../native/status.ts";
13
13
  import { WireFailure } from "../protocol/messages.ts";
@@ -51,9 +51,12 @@ async function status(): Promise<number> {
51
51
  console.log(`Package: ${join(here, "..", "..")}`);
52
52
  console.log(`Node: ${process.version} (${process.execPath})`);
53
53
  // The skill scripts run these tools here, on this machine.
54
- for (const line of toolLines("Tools here:", await localToolStatus(localToolContext()))) console.log(line);
55
- for (const line of await hostStatusLines()) console.log(line);
56
- return 0;
54
+ const local = await localToolStatus(localToolContext());
55
+ for (const line of toolLines("Tools here:", local)) console.log(line);
56
+ const host = await hostStatusLines();
57
+ for (const line of host.lines) console.log(line);
58
+ // A script or a CI check can tell a ready setup from a broken one.
59
+ return host.ready && local.every((tool) => tool.runs) ? 0 : 1;
57
60
  }
58
61
 
59
62
  async function main(argv: string[]): Promise<number> {
package/src/cli/status.ts CHANGED
@@ -5,32 +5,34 @@ import { hostTools, type ToolCallOptions } from "../host-client/tools.ts";
5
5
  import { STOP_AND_ASK } from "../protocol/messages.ts";
6
6
  import type { ToolStatus } from "../protocol/tools.ts";
7
7
 
8
- /** One line per tool under `heading`: its version, or why it does not run. */
8
+ /** One line per tool under `heading`: its version, or why it does not run; then each known flaw of it. */
9
9
  export function toolLines(heading: string, tools: readonly ToolStatus[]): string[] {
10
10
  return [
11
11
  heading,
12
- ...tools.map((tool) =>
12
+ ...tools.flatMap((tool) => [
13
13
  tool.runs
14
14
  ? ` ${tool.name}: ${tool.version} (${tool.path})`
15
15
  : tool.found
16
16
  ? ` ${tool.name}: found at ${tool.path} but it does not run. ${tool.problem}`
17
17
  : ` ${tool.name}: missing. ${tool.problem}`,
18
- ),
18
+ ...(tool.flaws ?? []).map((flaw) => ` Known flaw: ${flaw}`),
19
+ ]),
19
20
  ];
20
21
  }
21
22
 
22
23
  /**
23
24
  * Whether the Host Runtime answers, and the tools it has. VICE and its tools
24
25
  * live on the host; it finds and runs each one. One request shows both that
25
- * the host answers and what it has.
26
+ * the host answers and what it has. `ready` is true when the host answers and
27
+ * every tool on it runs.
26
28
  */
27
- export async function hostStatusLines(options: ToolCallOptions = {}): Promise<string[]> {
29
+ export async function hostStatusLines(options: ToolCallOptions = {}): Promise<{ lines: string[]; ready: boolean }> {
28
30
  let tools: ToolStatus[];
29
31
  try {
30
32
  tools = await hostTools(options);
31
33
  } catch (error) {
32
34
  // status is read by a person: the instruction for an agent (D37) stays out.
33
- return [`Host Runtime: not reachable. ${(error as Error).message.replace(` ${STOP_AND_ASK}`, "")}`];
35
+ return { lines: [`Host Runtime: not reachable. ${(error as Error).message.replace(` ${STOP_AND_ASK}`, "")}`], ready: false };
34
36
  }
35
- return ["Host Runtime: reachable", ...toolLines("Tools on the host:", tools)];
37
+ return { lines: ["Host Runtime: reachable", ...toolLines("Tools on the host:", tools)], ready: tools.every((tool) => tool.runs) };
36
38
  }
package/src/host/main.ts CHANGED
@@ -6,14 +6,16 @@ import { DEFAULT_HOST_PORT } from "../protocol/messages.ts";
6
6
  import { ProcessSupervisor } from "../native/processes.ts";
7
7
  import { startTrace, trace } from "../trace.ts";
8
8
  import { ListenError, parsePort, startHostServer, stopSignal } from "./server.ts";
9
+ import { viceFlaws } from "../native/discover.ts";
9
10
  import { createToolDispatcher } from "./tools/index.ts";
10
11
  import { checkViceStarts, findVice } from "./vice/process.ts";
11
12
  import { viceSessionFactory } from "./vice/session.ts";
13
+ import { PACKAGE } from "../package.ts";
12
14
 
13
15
  const HELP = `c64-re-tools-host
14
16
 
15
17
  Usage:
16
- npx -y --package=@henols/c64-re-tools@latest c64-re-tools-host [--port <port>]
18
+ npx -y --package=${PACKAGE.name}@${PACKAGE.tag} c64-re-tools-host [--port <port>]
17
19
  c64-re-tools-host --help
18
20
 
19
21
  Runs the c64-re-tools Host Runtime in the foreground. It first starts VICE
@@ -68,14 +70,17 @@ async function main(): Promise<number> {
68
70
  supervisor.startWatchdog("host");
69
71
 
70
72
  // Finding x64sc is not enough: VICE without its ROM files exits at once.
73
+ let flaws: string[];
71
74
  try {
72
- await checkViceStarts({ supervisor });
75
+ flaws = viceFlaws(await checkViceStarts({ supervisor }));
73
76
  } catch (error) {
74
77
  process.stderr.write(`${(error as Error).message}\n`);
75
78
  await supervisor.stopAll();
76
79
  return 1;
77
80
  }
78
81
  log("VICE starts");
82
+ // A known VICE flaw is named, not worked around (D56).
83
+ for (const flaw of flaws) log(`Known VICE flaw: ${flaw}`);
79
84
 
80
85
  let server;
81
86
  try {
@@ -83,7 +88,7 @@ async function main(): Promise<number> {
83
88
  host: "0.0.0.0",
84
89
  port,
85
90
  log,
86
- createViceSession: viceSessionFactory({ supervisor, log }),
91
+ createViceSession: viceSessionFactory({ supervisor, log, flaws }),
87
92
  tools: createToolDispatcher({ supervisor, log }),
88
93
  });
89
94
  } catch (error) {
@@ -36,9 +36,13 @@ export async function decode(
36
36
  if ("reason" in tokenized) return { result: { decoded: false, reason: tokenized.reason } };
37
37
  const executable = findTool(PETCAT, context.env);
38
38
  await requireMinimumVersion(PETCAT, executable, context);
39
+ // petcat gets the BASIC text only, closed with a real end marker: after a
40
+ // link that only ends BASIC (machine code right after the last line), petcat
41
+ // prints a comment line where a listing line belongs.
42
+ const text = Buffer.concat([program.subarray(0, tokenized.textBytes), Buffer.of(0, 0)]);
39
43
  const workspace = Workspace.create(context.supervisor);
40
44
  try {
41
- workspace.materialize("input", [{ path: "program.prg", size: program.length }], [program]);
45
+ workspace.materialize("input", [{ path: "program.prg", size: text.length }], [text]);
42
46
  const listingPath = join(workspace.directory("out"), LISTING);
43
47
  const run = await runToolOrFail(
44
48
  "petcat",
@@ -3,7 +3,7 @@
3
3
  // VICE older than MIN_VICE is reported as not working, as its operations refuse it.
4
4
 
5
5
  import type { ToolStatus } from "../../protocol/tools.ts";
6
- import { C1541, findTool, isOlderThanMinimum, MIN_VICE, PETCAT, VICE, viceVersionOf, type ToolSpec } from "../../native/discover.ts";
6
+ import { C1541, findTool, isOlderThanMinimum, MIN_VICE, PETCAT, VICE, viceFlaws, viceVersionOf, type ToolSpec } from "../../native/discover.ts";
7
7
  import type { ToolContext } from "../../native/run.ts";
8
8
  import { probeTools } from "../../native/status.ts";
9
9
  import { findVice } from "../vice/process.ts";
@@ -26,5 +26,11 @@ export async function hostStatus(context: ToolContext): Promise<{ result: { tool
26
26
  ],
27
27
  context,
28
28
  );
29
- return { result: { tools: tools.map((status, index) => requireMinimum(status, specs[index]!)) } };
29
+ const checked = tools.map((status, index) => requireMinimum(status, specs[index]!));
30
+ // VICE runs with its known flaws; status names them (D56).
31
+ const vice = checked[0]!;
32
+ const version = vice.runs ? viceVersionOf(vice.version ?? "") : undefined;
33
+ const flaws = version === undefined ? [] : viceFlaws(version);
34
+ if (flaws.length > 0) checked[0] = { ...vice, flaws };
35
+ return { result: { tools: checked } };
30
36
  }
@@ -9,7 +9,8 @@
9
9
  // operations only it offers; both act on the same machine;
10
10
  // - autostart of a PRG needs `-autostartprgmode 1` (inject into RAM);
11
11
  // - the joyport command reaches the C64 only through the "Joyport I/O
12
- // simulation" device (37), whose lines start all pressed until set;
12
+ // simulation" device (37). In VICE 3.10 and older it holds bits 5-7 of
13
+ // both CIA 1 ports low (VICE bug #2202, a known flaw named by D56);
13
14
  // - with the default JAM action a jammed CPU hangs silently; -jamaction 2
14
15
  // enters the monitor instead;
15
16
  // - the monitor's profiler prints numbers in the host locale, so VICE runs
@@ -29,7 +30,7 @@ import { setTimeout as sleep } from "node:timers/promises";
29
30
 
30
31
  import { WireFailure, type VideoStandard } from "../../protocol/messages.ts";
31
32
  import { elapsedMs, trace } from "../../trace.ts";
32
- import { findTool, isOlderThanMinimum, MIN_VICE, VICE } from "../../native/discover.ts";
33
+ import { findTool, isOlderThanMinimum, MIN_VICE, VICE, type ViceVersion } from "../../native/discover.ts";
33
34
  import type { ExitStatus, ProcessSupervisor, SupervisedProcess } from "../../native/processes.ts";
34
35
  import { BinaryMonitor, Command, decodeMemory, memoryGetBody } from "./binary-monitor.ts";
35
36
  import { TextMonitor } from "./text-monitor.ts";
@@ -200,9 +201,10 @@ export function startErrorLines(output: string[]): string[] {
200
201
  /**
201
202
  * Starts VICE once, as a session does, and stops it again. Finding
202
203
  * x64sc is not enough: without its ROM files VICE exits at once. Throws
203
- * WireFailure installation-incomplete with VICE's own error lines.
204
+ * WireFailure installation-incomplete with VICE's own error lines. Returns
205
+ * the VICE version.
204
206
  */
205
- export async function checkViceStarts(options: Omit<LaunchOptions, "videoStandard" | "log">): Promise<void> {
207
+ export async function checkViceStarts(options: Omit<LaunchOptions, "videoStandard" | "log">): Promise<ViceVersion> {
206
208
  const binary = findVice(options.env ?? process.env);
207
209
  const output: string[] = [];
208
210
  let vice: ViceProcess;
@@ -231,13 +233,16 @@ export async function checkViceStarts(options: Omit<LaunchOptions, "videoStandar
231
233
  } finally {
232
234
  await vice.stop();
233
235
  }
234
- const version = { major: info[1] ?? 0, minor: info[2] ?? 0 };
236
+ // The SVN revision is 0 in a release build.
237
+ const revision = info.length >= 10 ? info.readUInt32LE(6) : 0;
238
+ const version = { major: info[1] ?? 0, minor: info[2] ?? 0, ...(revision === 0 ? {} : { revision }) };
235
239
  if (isOlderThanMinimum(version)) {
236
240
  throw new WireFailure(
237
241
  "installation-incomplete",
238
242
  `VICE (${binary}) is version ${version.major}.${version.minor}, and c64-re-tools needs VICE ${MIN_VICE.major}.${MIN_VICE.minor} or later. ${VICE.remedy}`,
239
243
  );
240
244
  }
245
+ return version;
241
246
  }
242
247
 
243
248
  async function launchOnce(options: LaunchOptions): Promise<ViceProcess> {
@@ -74,6 +74,8 @@ const WINDOW_CLOSED =
74
74
  "Breakpoints and watchpoints are kept. Check the machine state before you continue.";
75
75
 
76
76
  /** Emulator state that a move to another VICE cannot carry. */
77
+ /** The first bytes of every VICE snapshot file. */
78
+ const VSF_MAGIC = Buffer.from("VICE Snapshot File\x1a", "latin1");
77
79
  const NOT_CARRIED = ["cpu history", "memory map", "profile", "keyboard input not yet typed"];
78
80
 
79
81
  /** Starts another VICE for this session; `log` also gets that launch's lines. */
@@ -119,6 +121,8 @@ export interface SessionOptions {
119
121
  launch?: ViceLauncher;
120
122
  /** Makes the host remove a path if it dies; returns the release. */
121
123
  ownPath?: (path: string) => () => void;
124
+ /** The known flaws of this VICE version (D56); keyboard and joystick results name them. */
125
+ flaws?: readonly string[];
122
126
  }
123
127
 
124
128
  export class ViceSession implements ViceSessionHandle {
@@ -128,6 +132,8 @@ export class ViceSession implements ViceSessionHandle {
128
132
  #mode: ViceMode = "headless";
129
133
  readonly #videoStandard: VideoStandard;
130
134
  readonly #log: (line: string) => void;
135
+ /** The known VICE flaws that keyboard and joystick input meet, as one text, or undefined. */
136
+ readonly #inputFlaw: string | undefined;
131
137
  readonly #untilReturnLimitMs: number;
132
138
  readonly #stepLimitMs: number;
133
139
  readonly #launch: ViceLauncher | undefined;
@@ -196,6 +202,7 @@ export class ViceSession implements ViceSessionHandle {
196
202
  this.#untilReturnLimitMs = options.untilReturnLimitMs ?? UNTIL_RETURN_LIMIT_MS;
197
203
  this.#stepLimitMs = options.stepLimitMs ?? RUNNING_COMMAND_TIMEOUT_MS;
198
204
  this.#launch = options.launch;
205
+ this.#inputFlaw = options.flaws === undefined || options.flaws.length === 0 ? undefined : options.flaws.join(" ");
199
206
  this.#workDir = mkdtempSync(join(tmpdir(), "c64-re-tools-session-"));
200
207
  this.#releaseWorkDir = options.ownPath?.(this.#workDir) ?? (() => {});
201
208
  this.#watch(vice);
@@ -268,7 +275,7 @@ export class ViceSession implements ViceSessionHandle {
268
275
  session.#timingStart = await session.#adapter.clock();
269
276
  // c64_profile reports what ran since the session started.
270
277
  await session.#adapter.startProfiler();
271
- // The control-port lines start all pressed; release them before the C64 reads them.
278
+ // In VICE 3.10 and older the control-port lines start all pressed; release them before the C64 reads them.
272
279
  for (const joystick of session.#joysticks.values()) await session.#adapter.setJoystick(joystick);
273
280
  }),
274
281
  );
@@ -368,7 +375,7 @@ export class ViceSession implements ViceSessionHandle {
368
375
  case "keyboard": {
369
376
  const bytes = Buffer.from((params as ViceOperations["keyboard"]["params"]).data, "hex");
370
377
  await this.#observe(() => this.#machine.feedKeyboard(bytes));
371
- return { queuedBytes: bytes.length };
378
+ return { queuedBytes: bytes.length, ...(this.#inputFlaw === undefined ? {} : { emulatorFlaw: this.#inputFlaw }) };
372
379
  }
373
380
  case "breakpoint":
374
381
  return this.#breakpoint(params as BreakpointParams);
@@ -450,7 +457,7 @@ export class ViceSession implements ViceSessionHandle {
450
457
  const joystick = params as JoystickState;
451
458
  await this.#observe(() => this.#machine.setJoystick(joystick));
452
459
  this.#joysticks.set(joystick.port, joystick);
453
- return joystick;
460
+ return { ...joystick, ...(this.#inputFlaw === undefined ? {} : { emulatorFlaw: this.#inputFlaw }) };
454
461
  }
455
462
  case "warp": {
456
463
  const { enabled } = params as ViceOperations["warp"]["params"];
@@ -553,7 +560,7 @@ export class ViceSession implements ViceSessionHandle {
553
560
  * checkpoint, a user breakpoint or watchpoint, or a JAM ends the run early.
554
561
  * The machine is stopped afterwards.
555
562
  */
556
- async #runFrames(limit: number, target?: { checkpoint: number; deferred: boolean }): Promise<{ end: RunEnd; frames: number; pc: number }> {
563
+ async #runFrames(limit: number, target?: { checkpoint: number; deferred: boolean }): Promise<{ end: RunEnd; frames: number; pc: number; space: "c64" | "drive8" }> {
557
564
  await this.#stop();
558
565
  const lines = RASTER[this.#videoStandard].lines;
559
566
  const start = await this.#machine.rasterPosition();
@@ -582,18 +589,19 @@ export class ViceSession implements ViceSessionHandle {
582
589
  }
583
590
  } else {
584
591
  frames++;
585
- if (frames >= limit) return { end: "frames", frames, pc: stop.pc };
592
+ if (frames >= limit) return { end: "frames", frames, pc: stop.pc, space: "c64" };
586
593
  await this.#machine.setCondition(clock, arming);
587
594
  }
588
595
  armingPhase = !armingPhase;
589
596
  continue;
590
597
  }
591
598
  if (end === "unknown") {
592
- if (await this.#jammedAt(stop.pc)) return { end: "jam", frames, pc: stop.pc };
599
+ if (await this.#jammedAt(stop.pc)) return { end: "jam", frames, pc: stop.pc, space: "c64" };
593
600
  throw new WireFailure("operation-failed", "The emulator stopped the machine for a reason outside this run.");
594
601
  }
595
- // A drive stop is completed before the result names the computer's pc.
596
- return { end, frames, pc: (await this.#completeDriveStop()) ?? stop.pc };
602
+ // A drive stop is completed before the result names the computer's pc; `space` says which CPU stopped.
603
+ const driveStop = this.#driveStop;
604
+ return { end, frames, pc: (await this.#completeDriveStop()) ?? stop.pc, space: driveStop ? "drive8" : "c64" };
597
605
  }
598
606
  } finally {
599
607
  await this.#machine.deleteCheckpoint(clock);
@@ -685,9 +693,9 @@ export class ViceSession implements ViceSessionHandle {
685
693
  if (expression !== undefined) await this.#machine.setCondition(checkpoint, expression);
686
694
  const outcome = await this.#runFrames(params.timeoutFrames, { checkpoint, deferred });
687
695
  const stopReason = outcome.end === "frames" ? "timeout" : outcome.end;
688
- // A stop event carries the computer's PC; a drive target reports the drive's.
689
- const pc = target.kind !== "raster" && target.space === "drive8" ? (await this.#machine.readRegisters("drive8")).pc : outcome.pc;
690
- return { reached: stopReason === "target", stopReason, state: "stopped", pc };
696
+ // A stop event carries the computer's PC; a stop in the drive reports the drive's (F-31).
697
+ const pc = outcome.space === "drive8" ? (await this.#machine.readRegisters("drive8")).pc : outcome.pc;
698
+ return { reached: stopReason === "target", stopReason, state: "stopped", pc, space: outcome.space };
691
699
  } finally {
692
700
  await this.#machine.deleteCheckpoint(checkpoint);
693
701
  }
@@ -952,7 +960,10 @@ export class ViceSession implements ViceSessionHandle {
952
960
  reply.push(bytes);
953
961
  return { exported: true, bytes: bytes.length };
954
962
  }
955
- const file = this.#stage(onlyAttachment(attachments), "vsf");
963
+ const bytes = onlyAttachment(attachments);
964
+ // Checked before the machine stops: VICE gives no reason for a file that is no snapshot (F-07).
965
+ if (!bytes.subarray(0, VSF_MAGIC.length).equals(VSF_MAGIC)) throw new WireFailure("invalid-input", "The file is not a VICE snapshot file (.vsf). Give a file that c64_snapshot save wrote.");
966
+ const file = this.#stage(bytes, "vsf");
956
967
  try {
957
968
  return await this.#restoreFrom(file);
958
969
  } catch (error) {
@@ -1340,6 +1351,8 @@ export function viceSessionFactory(options: {
1340
1351
  supervisor: ProcessSupervisor;
1341
1352
  env?: NodeJS.ProcessEnv;
1342
1353
  log?: (line: string) => void;
1354
+ /** The known flaws of the VICE version that the host checked at start. */
1355
+ flaws?: readonly string[];
1343
1356
  }): ViceSessionFactory {
1344
1357
  const log = options.log ?? (() => {});
1345
1358
  return async ({ videoStandard }) => {
@@ -1355,6 +1368,6 @@ export function viceSessionFactory(options: {
1355
1368
  ...(options.env === undefined ? {} : { env: options.env }),
1356
1369
  });
1357
1370
  const vice = await launch("headless", () => {});
1358
- return ViceSession.start(vice, videoStandard, log, { launch, ownPath: (path) => options.supervisor.ownPath(path) });
1371
+ return ViceSession.start(vice, videoStandard, log, { launch, ownPath: (path) => options.supervisor.ownPath(path), ...(options.flaws === undefined ? {} : { flaws: options.flaws }) });
1359
1372
  };
1360
1373
  }
@@ -5,6 +5,7 @@ import { connect as connectTcp, type Socket } from "node:net";
5
5
  import { encodeFrame, MessageReader } from "../protocol/framing.ts";
6
6
  import { elapsedMs, trace } from "../trace.ts";
7
7
  import { DEFAULT_HOST_PORT, HEARTBEAT_INTERVAL_MS, HEARTBEAT_TIMEOUT_MS, HOST_PROTOCOL_ID, HOST_PROTOCOL_VERSION, parseHostMessage, ProtocolError, WireFailure, withStopAndAsk, type ErrorCode, type Hello, type HostMessage, type Ping, type Request, type Role, type VideoStandard } from "../protocol/messages.ts";
8
+ import { PACKAGE } from "../package.ts";
8
9
 
9
10
  export interface Endpoint {
10
11
  host: string;
@@ -41,7 +42,7 @@ function unreachable(role: Role): WireFailure {
41
42
  return new WireFailure(
42
43
  code,
43
44
  withStopAndAsk(
44
- "The c64-re-tools host runtime is not running or cannot be reached. Start it on the host (npx -y --package=@henols/c64-re-tools@latest c64-re-tools-host) and try again.",
45
+ `The c64-re-tools host runtime is not running or cannot be reached. Start it on the host (npx -y --package=${PACKAGE.name}@${PACKAGE.tag} c64-re-tools-host) and try again.`,
45
46
  ),
46
47
  );
47
48
  }
@@ -52,8 +52,3 @@ export function writtenBlocks(map: Uint8Array, minimum: number): CodeRange[] {
52
52
  return blocks;
53
53
  }
54
54
 
55
- /** True when the CPU executed an instruction in RAM somewhere in the range. */
56
- export function executedIn(map: Uint8Array, range: CodeRange): boolean {
57
- for (let address = range.start; address <= range.end; address++) if ((map[address]! & COVERAGE.ramExecute) !== 0) return true;
58
- return false;
59
- }
@@ -7,9 +7,20 @@ import { HostConnection } from "./connect.ts";
7
7
  import { readProjectFile } from "./transfer.ts";
8
8
 
9
9
  const HOST_LOST =
10
- "The connection to the c64-re-tools host runtime was lost, and the emulator and its machine state went with it. " +
11
- "Check that the Host Runtime (c64-re-tools-host) is running, then restart the c64-re-tools MCP server.";
10
+ "The connection to the c64-re-tools host runtime was lost. The emulator and its machine state are gone: memory, registers, breakpoints, snapshots and the loaded program. " +
11
+ "The next call starts a new machine when the Host Runtime (c64-re-tools-host) runs. Load the program again and set the breakpoints again.";
12
12
  const SESSION_CLOSED = "The C64 session is closed because the c64-re-tools MCP server stops.";
13
+ const LOST_BEFORE_ANSWER = "The connection to the c64-re-tools host runtime was lost before the emulator answered. No machine state was lost. Do the call again.";
14
+
15
+ /**
16
+ * The loss of a session that never answered (D57): it had no machine state,
17
+ * so the caller can do the call again on a new session.
18
+ */
19
+ export class LostBeforeAnswer extends WireFailure {
20
+ constructor() {
21
+ super("machine-unavailable", LOST_BEFORE_ANSWER);
22
+ }
23
+ }
13
24
 
14
25
  interface Pending {
15
26
  op: ViceOperation;
@@ -24,6 +35,8 @@ export class ViceSessionClient {
24
35
  readonly #pending = new Map<number, Pending>();
25
36
  #nextId = 1;
26
37
  #lost: WireFailure | undefined;
38
+ #answered = false;
39
+ #lossReported = false;
27
40
 
28
41
  private constructor(connection: HostConnection) {
29
42
  this.#connection = connection;
@@ -33,6 +46,7 @@ export class ViceSessionClient {
33
46
  const pending = this.#pending.get(message.id);
34
47
  if (pending === undefined) return; // not ours; ignore
35
48
  this.#pending.delete(message.id);
49
+ this.#answered = true;
36
50
  if ("error" in message) {
37
51
  pending.reject(new WireFailure(message.error.code, message.error.message));
38
52
  return;
@@ -50,7 +64,8 @@ export class ViceSessionClient {
50
64
  }
51
65
  });
52
66
  this.closed = connection.closed.then(() => {
53
- this.#lost ??= new WireFailure("machine-state-lost", HOST_LOST);
67
+ this.#lost ??= this.#answered ? new WireFailure("machine-state-lost", HOST_LOST) : new LostBeforeAnswer();
68
+ this.#lossReported = this.#pending.size > 0;
54
69
  for (const pending of this.#pending.values()) pending.reject(this.#lost);
55
70
  this.#pending.clear();
56
71
  });
@@ -246,6 +261,21 @@ export class ViceSessionClient {
246
261
  return this.#request("window", { action });
247
262
  }
248
263
 
264
+ /** True when the connection to the host is gone, whatever the reason. */
265
+ get lost(): boolean {
266
+ return this.#lost !== undefined;
267
+ }
268
+
269
+ /** True when the emulator answered a request: then the session had machine state to lose (D57). */
270
+ get answered(): boolean {
271
+ return this.#answered;
272
+ }
273
+
274
+ /** The loss of a lost session, when no request got it; undefined while the session lives (D57). */
275
+ get unreportedLoss(): WireFailure | undefined {
276
+ return this.#lost?.code === "machine-state-lost" && !this.#lossReported ? this.#lost : undefined;
277
+ }
278
+
249
279
  /** Ends the session; the host stops its emulator. Open and later requests fail as closed. */
250
280
  async close(): Promise<void> {
251
281
  this.#lost ??= new WireFailure("machine-unavailable", SESSION_CLOSED);
@@ -267,7 +297,10 @@ export class ViceSessionClient {
267
297
  params: ViceOperations[O]["params"],
268
298
  attachments: readonly Uint8Array[] = [],
269
299
  ): Promise<{ result: ViceOperations[O]["result"]; attachments: Buffer[] }> {
270
- if (this.#lost !== undefined) return Promise.reject(this.#lost);
300
+ if (this.#lost !== undefined) {
301
+ this.#lossReported = true;
302
+ return Promise.reject(this.#lost);
303
+ }
271
304
  const id = this.#nextId;
272
305
  this.#nextId = this.#nextId >= 0xffff_ffff ? 1 : this.#nextId + 1;
273
306
  return new Promise((resolve, reject) => {
@@ -103,6 +103,14 @@ export function historyNamed(db: DatabaseSync | undefined, name: string, space:
103
103
  .sort(bySequence);
104
104
  }
105
105
 
106
+ /** Why a revision was written and what it rests on: its description and its evidence, when it has them. */
107
+ export function revisionNote(db: DatabaseSync | undefined, id: number): { reason?: string; evidence?: Evidence[] } {
108
+ const raw = db?.prepare("SELECT * FROM revisions WHERE id = ?").get(id) as Raw | undefined;
109
+ if (db === undefined || raw === undefined) return {};
110
+ const { description, evidence } = withEvidence(db, toRevision(raw));
111
+ return { ...(description === undefined ? {} : { reason: description }), ...(evidence === undefined ? {} : { evidence }) };
112
+ }
113
+
106
114
  /** The revision with its evidence, when it has any. */
107
115
  function withEvidence(db: DatabaseSync, revision: Revision): Revision {
108
116
  const rows = db.prepare("SELECT kind, ref, sha256 FROM evidence WHERE revision = ? ORDER BY id").all(revision.id) as Raw[];