@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.
Files changed (108) hide show
  1. package/README.md +4 -3
  2. package/THIRD-PARTY-NOTICES.md +423 -25
  3. package/{r2000-acme-ident.ts → anno-acme-ident.ts} +13 -13
  4. package/anno-bank.ts +171 -0
  5. package/anno-cli.ts +3040 -0
  6. package/{r2000-confidence.ts → anno-confidence.ts} +22 -22
  7. package/anno-coverage.ts +2465 -0
  8. package/anno-derive.ts +590 -0
  9. package/anno-details.ts +169 -0
  10. package/anno-enum-gen.ts +919 -0
  11. package/anno-export-asm.ts +2396 -0
  12. package/anno-graphics.ts +338 -0
  13. package/anno-hazard-report.ts +1367 -0
  14. package/anno-import.ts +495 -0
  15. package/anno-index.ts +150 -0
  16. package/anno-join.ts +480 -0
  17. package/{r2000-memmap-render.ts → anno-memmap-render.ts} +236 -95
  18. package/anno-provenance-ledger.ts +472 -0
  19. package/{r2000-regbits-gen.ts → anno-regbits-gen.ts} +20 -15
  20. package/{r2000-regbits.json → anno-regbits.json} +2 -2
  21. package/anno-register.ts +399 -0
  22. package/anno-store-export.ts +661 -0
  23. package/anno-store.ts +4002 -0
  24. package/anno-symbols.ts +266 -0
  25. package/anno-tools.ts +3264 -0
  26. package/anno-types.ts +1909 -0
  27. package/backend-detect.mts +124 -312
  28. package/block-class.ts +201 -0
  29. package/build.ts +4 -2
  30. package/capture-predicate.ts +597 -0
  31. package/channel-lock.ts +349 -0
  32. package/disasm-decoder.ts +14 -14
  33. package/disasm-opcodes.ts +4 -4
  34. package/disasm-renderer.ts +2 -2
  35. package/evid-ingest.ts +217 -0
  36. package/evid-reconcile.ts +316 -0
  37. package/host-tool-client.ts +430 -0
  38. package/hostpath.ts +1 -1
  39. package/incident-record.ts +23 -12
  40. package/install-resources.ts +30 -14
  41. package/memmap-lookup.ts +285 -0
  42. package/package.json +48 -23
  43. package/prg-image.ts +118 -0
  44. package/repo-root.ts +107 -8
  45. package/resources/backend-detect.mjs +98 -236
  46. package/resources/broker-control.mjs +189 -16
  47. package/resources/broker-epoch.mjs +1 -1
  48. package/resources/broker-kill.mjs +8 -2
  49. package/resources/broker-launch.mjs +373 -214
  50. package/resources/broker-state.mjs +64 -18
  51. package/resources/container-guard.mjs +1 -1
  52. package/resources/ghidra-project.mjs +790 -0
  53. package/resources/host-tool.mjs +2561 -0
  54. package/resources/vice-broker.mjs +330 -184
  55. package/resources/vice-launcher.sh +130 -12
  56. package/stock-address.ts +6 -6
  57. package/stock-cia.ts +2 -2
  58. package/stock-condition.ts +8 -8
  59. package/stock-connect.ts +10 -6
  60. package/stock-derived.ts +29 -37
  61. package/stock-diagnose.ts +200 -36
  62. package/stock-dispatch.ts +200 -68
  63. package/stock-execution.ts +5 -3
  64. package/stock-handler.ts +1 -1
  65. package/stock-input.ts +9 -9
  66. package/stock-machine.ts +17 -6
  67. package/stock-paths.ts +18 -14
  68. package/stock-petscii.ts +1 -1
  69. package/stock-protocol.ts +17 -12
  70. package/stock-recycle.ts +83 -2
  71. package/stock-registers.ts +54 -29
  72. package/stock-reproducible-run.ts +811 -0
  73. package/stock-run-until.ts +100 -1
  74. package/stock-sprites.ts +3 -3
  75. package/stock-symbols.ts +13 -13
  76. package/stock-timing.ts +2 -2
  77. package/stock-vicii.ts +1 -1
  78. package/stop-oracle.ts +167 -0
  79. package/text-capability-probe.ts +660 -0
  80. package/text-connect.ts +157 -0
  81. package/text-protocol.ts +810 -0
  82. package/text-tools.ts +778 -0
  83. package/textmon-backtrace.ts +385 -0
  84. package/textmon-cpuhistory.ts +335 -0
  85. package/textmon-memmap.ts +494 -0
  86. package/textmon-profile.ts +458 -0
  87. package/textmon-registers.ts +748 -0
  88. package/tools-manifest.stock.json +864 -3
  89. package/version.ts +1 -1
  90. package/vice-broker-client.ts +189 -42
  91. package/vice-errors.ts +268 -0
  92. package/vice-proxy.ts +392 -2175
  93. package/vsf-slice.ts +640 -0
  94. package/capability-registry.ts +0 -388
  95. package/r2000-cli.ts +0 -1103
  96. package/r2000-d64.ts +0 -310
  97. package/r2000-enum-gen.ts +0 -574
  98. package/r2000-launch.ts +0 -357
  99. package/r2000-mcp-client.ts +0 -596
  100. package/r2000-project.ts +0 -190
  101. package/r2000-symbols.ts +0 -388
  102. package/r2000-tools.ts +0 -914
  103. package/r2000-verify.ts +0 -184
  104. package/refresh-manifest.ts +0 -124
  105. package/tools-manifest.json +0 -1223
  106. package/vice-probe.ts +0 -278
  107. package/vice-sync.ts +0 -336
  108. package/vice.ts +0 -772
package/stock-diagnose.ts CHANGED
@@ -48,8 +48,9 @@ import { ensureStockSession, type StockDispatchDeps, type EnsureStockSessionOutc
48
48
  import type { DerivedPureHandler } from "./stock-derived.ts";
49
49
  import type { StockConnectSession } from "./stock-connect.ts";
50
50
  import { runStateFor, jamObservedFor } from "./stock-runstate.ts";
51
- import { MachineRestartedError, readEpoch, type EpochResult } from "./vice.ts";
51
+ import { MachineRestartedError, readEpoch, type EpochResult } from "./vice-errors.ts";
52
52
  import { MonitorOwnershipError } from "./vice-broker-client.ts";
53
+ import { tryAcquireChannelLock, currentChannelLockHolder, type ChannelLockHandle, type ChannelLockHolder, type MonitorChannel } from "./channel-lock.ts";
53
54
 
54
55
  /** True iff `value` is a well-formed, generic JSON object -- not null, not
55
56
  * an array. Matches this module tree's own isPlainObject() convention,
@@ -649,6 +650,29 @@ function renderStockWedgedReport(bracket1: StockLivenessBracketResult, bracket2:
649
650
  );
650
651
  }
651
652
 
653
+ /** D-10/D-11: rendered when step 4's guard finds a FOREIGN hold already live
654
+ * -- `tryAcquireChannelLock()` returned `null`, so no bracket was ever run.
655
+ * `holder` is a snapshot taken at the moment the guard observed it; the
656
+ * authoritative `evidence.channelContention` is derived independently and
657
+ * slightly later, inside diagnoseVerdictResult() -- a few milliseconds'
658
+ * difference in `heldMs` between the two is expected and honest, not a bug.
659
+ * Deliberately self-contained (repeats the channel/operation/duration the
660
+ * universal channelContentionNote() will also append), so this report reads
661
+ * completely even if the note above it is somehow stripped. Must never
662
+ * contain wedge, wedged, hang, hung, frozen, stuck or unresponsive. */
663
+ function renderStockContendedReport(holder: ChannelLockHolder): string {
664
+ const heldMs = Math.max(0, Date.now() - holder.heldSince);
665
+ const grantId = holder.grantId ?? "unknown";
666
+ return (
667
+ "vice_diagnose verdict: live\n\n" +
668
+ `The ${holder.channel} channel currently holds this instance's halt authority (operation "${holder.operation}", ` +
669
+ `grant ${grantId}, held for ${heldMs}ms). No liveness bracket was run: this call was never granted halt ` +
670
+ "authority, so the machine was never resumed and liveness was not measured here -- the contention evidence is " +
671
+ "what explains the zero bracket count, not a bracket that read zero. The instance is healthy and answering: " +
672
+ "this is never a reason to recycle. Wait for the other channel's operation to finish, or find its holder."
673
+ );
674
+ }
675
+
652
676
  /** The `detail` half of the `liveness_unmeasurable` refusal (07-REVIEW.md
653
677
  * WR-02). Deliberately carries NO "vice_diagnose:" prefix of its own --
654
678
  * diagnoseUnavailableResult() owns the documented, machine-parseable prefix,
@@ -751,24 +775,118 @@ const JAM_OBSERVED_NOTE =
751
775
  "a wedge. Recover with vice_machine_reset -- do NOT vice_recycle, which destroys an instance a reset " +
752
776
  "would have fixed.";
753
777
 
778
+ // ---------------------------------------------------------------------------
779
+ // D-09/D-10/D-11 (plan 41-04, CHAN-05): channelContention -- always-present
780
+ // cross-cutting evidence on the jamObserved model, plus the guard (in
781
+ // handleDiagnoseStock()'s step 4, below) that makes `wedged` structurally
782
+ // unreachable while contended.
783
+ //
784
+ // DELIBERATE ASYMMETRY WITH jamObserved, RECORDED HERE SO A LATER READER DOES
785
+ // NOT "FIX" IT: jamObserved guards only in vice-wedge-triage/SKILL.md's
786
+ // prose and still lets `wedged` return -- the caller has to read the
787
+ // evidence and choose not to recycle. channelContention guards in CODE, in
788
+ // the handler itself, and makes `wedged` structurally unreachable while a
789
+ // foreign hold is live. The reason is the cost of being wrong: a contended
790
+ // instance is not merely misdiagnosed, it would be DESTROYED by the
791
+ // documented `wedged` remedy (T-41-15). Do not collapse this asymmetry by
792
+ // demoting the code guard to a prose-only warning to match jamObserved's
793
+ // shape -- they are deliberately different because what they are protecting
794
+ // against is not the same severity.
795
+ // ---------------------------------------------------------------------------
796
+
797
+ /** Always-present evidence on every `vice_diagnose` verdict (D-09), naming
798
+ * whether the OTHER monitor channel currently holds this instance's halt
799
+ * authority. `channel`/`operation`/`grantId`/`heldMs` are `null` exactly when
800
+ * `held` is `false` -- never omitted, so "not held" and "held for under a
801
+ * millisecond" stay distinguishable (`heldMs: null` vs `heldMs: 0`). */
802
+ export interface StockChannelContention {
803
+ held: boolean;
804
+ channel: MonitorChannel | null;
805
+ operation: string | null;
806
+ grantId: string | null;
807
+ heldMs: number | null;
808
+ }
809
+
810
+ /**
811
+ * Derives `StockChannelContention` from channel-lock.ts's holder record.
812
+ *
813
+ * `self` is the handle THIS diagnose call itself currently holds, if any --
814
+ * passed only from inside handleDiagnoseStock()'s own step-4 guard, while it
815
+ * holds the lock across its own liveness bracket(s). When `self` is
816
+ * non-null, this call IS the current holder by construction (only one
817
+ * holder can exist at a time), so reporting that as "contention" would be
818
+ * dishonest -- a diagnose call's own halt authority over its own bracket is
819
+ * not a FOREIGN hold. Every other caller (the four verdicts reached before
820
+ * the guard ever runs) passes no `self`, and a genuine foreign hold observed
821
+ * at that moment is reported exactly as measured.
822
+ *
823
+ * `grantId` reads the literal `unknown` when the holder recorded none --
824
+ * never fabricated, matching claimMonitor()'s own posture. `heldMs` is
825
+ * `Math.max(0, Math.trunc(nowMs - holder.heldSince))`, a non-negative whole
826
+ * millisecond integer.
827
+ */
828
+ export function channelContentionFor(nowMs: number, self: ChannelLockHandle | null): StockChannelContention {
829
+ if (self !== null) {
830
+ return { held: false, channel: null, operation: null, grantId: null, heldMs: null };
831
+ }
832
+ const holder = currentChannelLockHolder();
833
+ if (holder === null) {
834
+ return { held: false, channel: null, operation: null, grantId: null, heldMs: null };
835
+ }
836
+ return {
837
+ held: true,
838
+ channel: holder.channel,
839
+ operation: holder.operation,
840
+ grantId: holder.grantId ?? "unknown",
841
+ heldMs: Math.max(0, Math.trunc(nowMs - holder.heldSince)),
842
+ };
843
+ }
844
+
845
+ /** Appended to a verdict's report in the same register as JAM_OBSERVED_NOTE
846
+ * above, whenever `channelContention.held` is true -- deliberately UNIVERSAL:
847
+ * contention is evidence cutting across all five verdicts (D-09), not
848
+ * exclusive to the live-with-bracketsRun:0 path the guard itself reaches. A
849
+ * concurrent foreign hold can just as easily be observed while THIS call is
850
+ * answering restarted/checkpoint_trap/monitor_held_elsewhere. Must never
851
+ * contain wedge, wedged, hang, hung, frozen, stuck or unresponsive -- the
852
+ * same register channelLockRefusalMessage() (channel-lock.ts) already
853
+ * establishes, and the instance is healthy and answering regardless of which
854
+ * verdict carries this note. */
855
+ function channelContentionNote(contention: StockChannelContention): string {
856
+ return (
857
+ "\n\nCHANNEL CONTENTION: the " +
858
+ `${contention.channel} channel currently holds this instance's halt authority (operation ` +
859
+ `"${contention.operation}", grant ${contention.grantId}, held for ${contention.heldMs}ms). The instance is ` +
860
+ "healthy and answering -- this is never a reason to recycle."
861
+ );
862
+ }
863
+
754
864
  /** WR-04: `jamObserved` is derived HERE, from the same seam as
755
865
  * `machinePaused`, and stamped into EVERY verdict's evidence -- not added
756
866
  * per-call-site, for the same reason WR-03 removed the hand-passed
757
867
  * `machinePaused`. It is evidence on the existing five verdicts, never a
758
868
  * sixth verdict (D-03). Always present (never omitted when false), so an
759
- * absent field can never be read as "no jam". */
869
+ * absent field can never be read as "no jam". D-09: `channelContention` joins
870
+ * it here, on the identical always-present, derived-once, spread-unconditionally
871
+ * shape -- see `self`'s doc comment on channelContentionFor() above for why
872
+ * this function must be told which handle (if any) THIS call itself holds. */
760
873
  function diagnoseVerdictResult(
761
874
  session: StockConnectSession | null,
762
875
  verdict: StockDiagnoseVerdict,
763
876
  evidence: Record<string, unknown>,
764
877
  report: string,
878
+ self: ChannelLockHandle | null = null,
765
879
  ): StockToolResult {
766
880
  const { machinePaused, machinePausedSource } = deriveMachinePaused(session);
767
881
  const jamObserved = session === null ? false : jamObservedFor(session.client);
882
+ const channelContention = channelContentionFor(Date.now(), self);
883
+ let finalReport = report;
884
+ if (jamObserved) finalReport += JAM_OBSERVED_NOTE;
885
+ if (channelContention.held) finalReport += channelContentionNote(channelContention);
768
886
  const payload: Record<string, unknown> = {
769
887
  verdict,
770
- evidence: { ...evidence, jamObserved },
771
- report: jamObserved ? report + JAM_OBSERVED_NOTE : report,
888
+ evidence: { ...evidence, jamObserved, channelContention },
889
+ report: finalReport,
772
890
  machinePaused,
773
891
  machinePausedSource,
774
892
  };
@@ -936,47 +1054,93 @@ export async function handleDiagnoseStock(_args: Record<string, unknown>, deps:
936
1054
  }
937
1055
 
938
1056
  // Step 4: the liveness bracket -- the only step that resumes.
939
- let bracket1: StockLivenessBracketResult;
940
- try {
941
- bracket1 = await runStockLivenessBracket(session);
942
- } catch (err) {
943
- return diagnoseUnavailableResult("evidence_gathering_failed", `the liveness bracket failed (${describeStockError(err)}).`);
1057
+ //
1058
+ // D-10 guard, placed immediately BEFORE the first bracket can run --
1059
+ // that is, before the only step that resumes the machine, and therefore
1060
+ // before any path that can reach the `wedged` return below.
1061
+ // tryAcquireChannelLock() is the SYNCHRONOUS, non-blocking entry point,
1062
+ // chosen deliberately over acquireChannelLock(): a diagnostic that
1063
+ // queued behind a foreign hold would block behind the exact holder it
1064
+ // exists to report, and one that merely peeked at the holder record
1065
+ // without taking the lock could still have a foreign hold begin during
1066
+ // its own bracket. On `null` a foreign hold is live RIGHT NOW --
1067
+ // short-circuit to `live` with contention evidence and `bracketsRun: 0`
1068
+ // (the honest count: the machine was never resumed, so liveness was not
1069
+ // measured), with NO bracket run at all -- this is D-11's verdict choice
1070
+ // (the instance is healthy and responsive, not wedged). On a handle,
1071
+ // hold it across BOTH brackets and release in a `finally` covering every
1072
+ // return from here on, so a thrown bracket still releases. This is what
1073
+ // makes `wedged` structurally unreachable while contended: the only
1074
+ // route to it is through a bracket, and a bracket only runs when this
1075
+ // lock was actually taken by THIS call.
1076
+ const channelHandle = tryAcquireChannelLock({ channel: "binary", operation: "vice_diagnose" });
1077
+ if (channelHandle === null) {
1078
+ const holder = currentChannelLockHolder();
1079
+ // Invariant: tryAcquireChannelLock() returning null means SOME holder
1080
+ // exists (that is precisely why it refused) -- currentChannelLockHolder()
1081
+ // reading null here would be a lock-module invariant violation, not a
1082
+ // reachable case this handler needs to degrade gracefully for.
1083
+ if (holder === null) {
1084
+ return diagnoseUnavailableResult(
1085
+ "unknown",
1086
+ "channel-lock: tryAcquireChannelLock() refused but currentChannelLockHolder() reported no holder -- internal invariant violation.",
1087
+ );
1088
+ }
1089
+ return diagnoseVerdictResult(session, "live", { bracketsRun: 0 }, renderStockContendedReport(holder));
944
1090
  }
1091
+ try {
1092
+ let bracket1: StockLivenessBracketResult;
1093
+ try {
1094
+ bracket1 = await runStockLivenessBracket(session);
1095
+ } catch (err) {
1096
+ return diagnoseUnavailableResult("evidence_gathering_failed", `the liveness bracket failed (${describeStockError(err)}).`);
1097
+ }
945
1098
 
946
- if (bracket1.advanced === null) {
947
- return diagnoseUnavailableResult("liveness_unmeasurable", inconclusiveBracketText(bracket1));
948
- }
949
- if (bracket1.advanced) {
950
- return diagnoseVerdictResult(session, "live", { bracketsRun: 1, bracket: serializeBracket(bracket1) }, renderStockLiveReport(bracket1));
951
- }
1099
+ if (bracket1.advanced === null) {
1100
+ return diagnoseUnavailableResult("liveness_unmeasurable", inconclusiveBracketText(bracket1));
1101
+ }
1102
+ if (bracket1.advanced) {
1103
+ return diagnoseVerdictResult(
1104
+ session,
1105
+ "live",
1106
+ { bracketsRun: 1, bracket: serializeBracket(bracket1) },
1107
+ renderStockLiveReport(bracket1),
1108
+ channelHandle,
1109
+ );
1110
+ }
952
1111
 
953
- // Run a second bracket only when the first shows no advance -- mirroring
954
- // the fork's own short-circuit.
955
- let bracket2: StockLivenessBracketResult;
956
- try {
957
- bracket2 = await runStockLivenessBracket(session);
958
- } catch (err) {
959
- return diagnoseUnavailableResult("evidence_gathering_failed", `the second liveness bracket failed (${describeStockError(err)}).`);
960
- }
1112
+ // Run a second bracket only when the first shows no advance -- mirroring
1113
+ // the fork's own short-circuit.
1114
+ let bracket2: StockLivenessBracketResult;
1115
+ try {
1116
+ bracket2 = await runStockLivenessBracket(session);
1117
+ } catch (err) {
1118
+ return diagnoseUnavailableResult("evidence_gathering_failed", `the second liveness bracket failed (${describeStockError(err)}).`);
1119
+ }
1120
+
1121
+ if (bracket2.advanced === null) {
1122
+ return diagnoseUnavailableResult("liveness_unmeasurable", inconclusiveBracketText(bracket2));
1123
+ }
1124
+ if (bracket2.advanced) {
1125
+ return diagnoseVerdictResult(
1126
+ session,
1127
+ "live",
1128
+ { bracketsRun: 2, bracket1: serializeBracket(bracket1), bracket2: serializeBracket(bracket2) },
1129
+ renderStockLiveReport(bracket2),
1130
+ channelHandle,
1131
+ );
1132
+ }
961
1133
 
962
- if (bracket2.advanced === null) {
963
- return diagnoseUnavailableResult("liveness_unmeasurable", inconclusiveBracketText(bracket2));
964
- }
965
- if (bracket2.advanced) {
966
1134
  return diagnoseVerdictResult(
967
1135
  session,
968
- "live",
1136
+ "wedged",
969
1137
  { bracketsRun: 2, bracket1: serializeBracket(bracket1), bracket2: serializeBracket(bracket2) },
970
- renderStockLiveReport(bracket2),
1138
+ renderStockWedgedReport(bracket1, bracket2),
1139
+ channelHandle,
971
1140
  );
1141
+ } finally {
1142
+ channelHandle.release();
972
1143
  }
973
-
974
- return diagnoseVerdictResult(
975
- session,
976
- "wedged",
977
- { bracketsRun: 2, bracket1: serializeBracket(bracket1), bracket2: serializeBracket(bracket2) },
978
- renderStockWedgedReport(bracket1, bracket2),
979
- );
980
1144
  } catch (err) {
981
1145
  // WR-02: the outer catch-all goes through the classifier too, so there is
982
1146
  // no isError answer this handler can produce that lacks the documented