@remnic/core 9.65.4 → 9.65.6

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 (214) hide show
  1. package/dist/access-admin-ops-surface.d.ts +1 -1
  2. package/dist/access-admin-ops-surface.js +24 -24
  3. package/dist/access-audit.js +2 -2
  4. package/dist/access-authorization-probe.d.ts +1 -1
  5. package/dist/access-authorization-probe.js +25 -25
  6. package/dist/access-boundary.d.ts +1 -1
  7. package/dist/access-boundary.js +24 -24
  8. package/dist/access-cli.js +69 -68
  9. package/dist/access-cli.js.map +1 -1
  10. package/dist/access-extraction-force-flush.d.ts +1 -1
  11. package/dist/access-extraction-force-flush.js +24 -24
  12. package/dist/access-http-lcm-compaction.d.ts +1 -1
  13. package/dist/access-http-lifecycle-flush.d.ts +1 -1
  14. package/dist/access-http-offline-stream.d.ts +1 -1
  15. package/dist/access-http-query.js +26 -26
  16. package/dist/access-http.d.ts +2 -2
  17. package/dist/access-http.js +37 -37
  18. package/dist/access-identity-continuity-surface.js +24 -24
  19. package/dist/access-lcm-surface.d.ts +1 -1
  20. package/dist/access-lcm-surface.js +24 -24
  21. package/dist/access-mcp.d.ts +1 -1
  22. package/dist/access-mcp.js +30 -30
  23. package/dist/access-namespace-preflight.js +24 -24
  24. package/dist/access-observe-write-surface.d.ts +1 -1
  25. package/dist/access-observe-write-surface.js +24 -24
  26. package/dist/access-operations-batch.js +26 -26
  27. package/dist/access-operations.d.ts +6 -6
  28. package/dist/access-operations.js +29 -29
  29. package/dist/access-recall-concurrency.d.ts +1 -1
  30. package/dist/access-recall-concurrency.js +24 -24
  31. package/dist/access-recall-response.d.ts +1 -1
  32. package/dist/access-recall-response.js +24 -24
  33. package/dist/access-recall-surface.d.ts +1 -1
  34. package/dist/access-recall-surface.js +24 -24
  35. package/dist/access-schema.d.ts +83 -83
  36. package/dist/access-schema.js +3 -3
  37. package/dist/access-service-helpers.d.ts +1 -1
  38. package/dist/access-service.d.ts +1 -1
  39. package/dist/access-service.js +24 -24
  40. package/dist/access-surface-catalog.d.ts +1 -1
  41. package/dist/access-surface-catalog.js +26 -26
  42. package/dist/briefing.js +10 -10
  43. package/dist/{capsule-crypto-YO5QJ6L3.js → capsule-crypto-GWVG7LGC.js} +2 -2
  44. package/dist/causal-consolidation.js +11 -11
  45. package/dist/{chunk-ZPOHEB5Z.js → chunk-2JHRCWHT.js} +37 -37
  46. package/dist/{chunk-BK5G2MHO.js → chunk-2RXUZVPU.js} +2 -2
  47. package/dist/{chunk-JPAKBNTM.js → chunk-3I22VWB3.js} +2 -2
  48. package/dist/{chunk-ECLJ4LA2.js → chunk-53MZQEBD.js} +2 -2
  49. package/dist/{chunk-HF5BUTI6.js → chunk-5GIZM4RX.js} +2 -2
  50. package/dist/{chunk-YHGCT462.js → chunk-5TG4MU2U.js} +4 -4
  51. package/dist/{chunk-VDX2J7OX.js → chunk-5W7CTZ2E.js} +4 -4
  52. package/dist/{chunk-IWKGOCL6.js → chunk-5W7MKXMU.js} +4 -4
  53. package/dist/{chunk-YHCBJULX.js → chunk-6FUMA6AA.js} +2 -2
  54. package/dist/{chunk-THVI4Y7X.js → chunk-6LS5OX3U.js} +5 -5
  55. package/dist/{chunk-TVVEYCNW.js → chunk-7K5Q6COX.js} +4 -4
  56. package/dist/{chunk-KPB2QHJZ.js → chunk-7OZSHBWK.js} +2 -2
  57. package/dist/{chunk-CQBIN77D.js → chunk-7SSRXEZJ.js} +2 -2
  58. package/dist/{chunk-NNBZS3DY.js → chunk-AG46P24C.js} +4 -4
  59. package/dist/{chunk-QJW4MZSW.js → chunk-AYAZWXJI.js} +2 -2
  60. package/dist/{chunk-WDXD6ITO.js → chunk-BXXOPKT5.js} +2 -2
  61. package/dist/{chunk-DJZWNKFO.js → chunk-D35XLYQ7.js} +10 -10
  62. package/dist/{chunk-KOO37E4Y.js → chunk-DYO24BNB.js} +2 -2
  63. package/dist/{chunk-DORFF5N5.js → chunk-FEO2CDXR.js} +2 -2
  64. package/dist/{chunk-IURMJBGP.js → chunk-HWVNYNIV.js} +2 -2
  65. package/dist/{chunk-OZDFB22S.js → chunk-IDQ6DKXC.js} +3 -3
  66. package/dist/{chunk-J3ATBB7M.js → chunk-IF4EY26E.js} +2 -2
  67. package/dist/{chunk-W675UJCZ.js → chunk-IRXMJLTW.js} +2 -2
  68. package/dist/{chunk-DUZARALI.js → chunk-JFWZRUKP.js} +3 -3
  69. package/dist/{chunk-DUEDOYZD.js → chunk-JGG5HVI2.js} +2 -2
  70. package/dist/{chunk-3NH3VP3U.js → chunk-KBOG7FJV.js} +2 -2
  71. package/dist/{chunk-UCKG36HO.js → chunk-LSYD7GHZ.js} +2 -2
  72. package/dist/{chunk-6433HV72.js → chunk-MVIYRRVJ.js} +5 -5
  73. package/dist/{chunk-GHD2LLG4.js → chunk-NLUCDKLT.js} +2 -2
  74. package/dist/{chunk-TJFI3K2O.js → chunk-O4BAGJDN.js} +2 -2
  75. package/dist/{chunk-LYBIHM4N.js → chunk-OMZLBT4B.js} +4 -4
  76. package/dist/{chunk-BXLOS5AJ.js → chunk-OWHERGF2.js} +2 -2
  77. package/dist/{chunk-I23ZHYSL.js → chunk-PGW64IFQ.js} +2 -2
  78. package/dist/{chunk-AZLLH6YA.js → chunk-PNAGNVGK.js} +43 -48
  79. package/dist/chunk-PNAGNVGK.js.map +1 -0
  80. package/dist/{chunk-YDKF4MSJ.js → chunk-QMZY3GG6.js} +3 -3
  81. package/dist/{chunk-N37X5RU7.js → chunk-RACZB5HT.js} +4 -4
  82. package/dist/{chunk-YZJM6CLF.js → chunk-ROEAEANL.js} +7 -7
  83. package/dist/{chunk-SR5LNI6Q.js → chunk-RQ2EQRRR.js} +4 -4
  84. package/dist/chunk-SDEPLF7V.js +325 -0
  85. package/dist/chunk-SDEPLF7V.js.map +1 -0
  86. package/dist/{chunk-2IXQSGCU.js → chunk-SR7JIRAR.js} +2 -2
  87. package/dist/{chunk-OCCHUEZB.js → chunk-TWBQQ3FT.js} +2 -2
  88. package/dist/{chunk-NLEH4RTB.js → chunk-US4IRASK.js} +2 -2
  89. package/dist/{chunk-DQEMWVMT.js → chunk-UVYI6VIX.js} +1 -1
  90. package/dist/{chunk-PTZQYCJ4.js → chunk-W6J4POB6.js} +2 -2
  91. package/dist/{chunk-OU34IQXV.js → chunk-X2DWBWOX.js} +2 -2
  92. package/dist/{chunk-2MVTAIL3.js → chunk-XAWIL7TC.js} +2 -2
  93. package/dist/{chunk-UFR6HX7A.js → chunk-XUZ5CIEE.js} +1 -1
  94. package/dist/{chunk-GIC6BASP.js → chunk-XZBXINXZ.js} +4 -4
  95. package/dist/{chunk-XGM3VQWG.js → chunk-YUTTMSC7.js} +13 -13
  96. package/dist/{chunk-3JZBPTLD.js → chunk-YWBOOBDP.js} +38 -38
  97. package/dist/{chunk-KFIOPZ25.js → chunk-ZXUZ54Z5.js} +2 -2
  98. package/dist/{cli-DO3i14pM.d.ts → cli-Bfgo0n1L.d.ts} +2 -2
  99. package/dist/cli.d.ts +3 -3
  100. package/dist/cli.js +53 -53
  101. package/dist/compounding/engine.js +10 -10
  102. package/dist/connectors/codex-materialize-runner.js +10 -10
  103. package/dist/connectors/index.js +10 -10
  104. package/dist/convergence-refresh.js +12 -12
  105. package/dist/corpus-watermark.js +12 -12
  106. package/dist/entity-retrieval.js +10 -10
  107. package/dist/external-wiki-access.d.ts +1 -1
  108. package/dist/external-wiki-access.js +25 -25
  109. package/dist/external-wiki-mcp-tools.d.ts +1 -1
  110. package/dist/extraction.js +10 -10
  111. package/dist/harmonic-retrieval.js +1 -1
  112. package/dist/index.d.ts +681 -680
  113. package/dist/index.js +92 -76
  114. package/dist/lcm/index.js +3 -3
  115. package/dist/maintenance/memory-governance.js +10 -10
  116. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +10 -10
  117. package/dist/maintenance/rebuild-memory-projection.js +11 -11
  118. package/dist/mcp-memory-inspector-app.d.ts +1 -1
  119. package/dist/memory-controller.d.ts +203 -0
  120. package/dist/memory-controller.js +23 -0
  121. package/dist/memory-controller.js.map +1 -0
  122. package/dist/memory-subject-cli.d.ts +1 -1
  123. package/dist/memory-subject.d.ts +1 -1
  124. package/dist/namespaces/migrate.js +11 -11
  125. package/dist/namespaces/search.js +7 -7
  126. package/dist/namespaces/storage.js +10 -10
  127. package/dist/operator-doctor-corpus.js +13 -13
  128. package/dist/operator-toolkit.js +18 -18
  129. package/dist/orchestration/maintenance.js +16 -16
  130. package/dist/orchestrator.js +69 -68
  131. package/dist/{public-http-DhIXYBYI.d.ts → public-http-BP3VQDl5.d.ts} +1 -1
  132. package/dist/schemas.d.ts +124 -124
  133. package/dist/search/factory.js +6 -6
  134. package/dist/search/index.js +11 -11
  135. package/dist/search/lancedb-backend.js +2 -2
  136. package/dist/search/meilisearch-backend.js +2 -2
  137. package/dist/search/orama-backend.js +2 -2
  138. package/dist/semantic-consolidation.js +11 -11
  139. package/dist/semantic-rule-promotion.js +10 -10
  140. package/dist/semantic-rule-verifier.js +10 -10
  141. package/dist/shared-context/manager.d.ts +6 -6
  142. package/dist/source-agent-qualifier.js +10 -10
  143. package/dist/storage.js +9 -9
  144. package/dist/support-passport/index.d.ts +4 -4
  145. package/dist/support-passport/index.js +27 -27
  146. package/dist/transfer/backup.js +2 -2
  147. package/dist/transfer/capsule-export.js +2 -2
  148. package/dist/transfer/capsule-import.js +2 -2
  149. package/dist/transfer/types.d.ts +78 -78
  150. package/dist/verified-recall.js +10 -10
  151. package/dist/who-knows-cli.d.ts +1 -1
  152. package/dist/who-knows-cli.js +11 -11
  153. package/dist/who-knows.d.ts +1 -1
  154. package/dist/who-knows.js +10 -10
  155. package/package.json +2 -2
  156. package/src/index.ts +3 -0
  157. package/src/memory-controller.test.ts +347 -0
  158. package/src/memory-controller.ts +547 -0
  159. package/src/review/frontmatter.test.ts +15 -0
  160. package/src/review/frontmatter.ts +20 -0
  161. package/src/review/review-deck-snapshot.ts +4 -20
  162. package/src/review/review-deck.ts +1 -12
  163. package/dist/chunk-AZLLH6YA.js.map +0 -1
  164. package/dist/{access-service-DqPa3hy4.d.ts → access-service-sbiNS5UB.d.ts} +64 -64
  165. /package/dist/{capsule-crypto-YO5QJ6L3.js.map → capsule-crypto-GWVG7LGC.js.map} +0 -0
  166. /package/dist/{chunk-ZPOHEB5Z.js.map → chunk-2JHRCWHT.js.map} +0 -0
  167. /package/dist/{chunk-BK5G2MHO.js.map → chunk-2RXUZVPU.js.map} +0 -0
  168. /package/dist/{chunk-JPAKBNTM.js.map → chunk-3I22VWB3.js.map} +0 -0
  169. /package/dist/{chunk-ECLJ4LA2.js.map → chunk-53MZQEBD.js.map} +0 -0
  170. /package/dist/{chunk-HF5BUTI6.js.map → chunk-5GIZM4RX.js.map} +0 -0
  171. /package/dist/{chunk-YHGCT462.js.map → chunk-5TG4MU2U.js.map} +0 -0
  172. /package/dist/{chunk-VDX2J7OX.js.map → chunk-5W7CTZ2E.js.map} +0 -0
  173. /package/dist/{chunk-IWKGOCL6.js.map → chunk-5W7MKXMU.js.map} +0 -0
  174. /package/dist/{chunk-YHCBJULX.js.map → chunk-6FUMA6AA.js.map} +0 -0
  175. /package/dist/{chunk-THVI4Y7X.js.map → chunk-6LS5OX3U.js.map} +0 -0
  176. /package/dist/{chunk-TVVEYCNW.js.map → chunk-7K5Q6COX.js.map} +0 -0
  177. /package/dist/{chunk-KPB2QHJZ.js.map → chunk-7OZSHBWK.js.map} +0 -0
  178. /package/dist/{chunk-CQBIN77D.js.map → chunk-7SSRXEZJ.js.map} +0 -0
  179. /package/dist/{chunk-NNBZS3DY.js.map → chunk-AG46P24C.js.map} +0 -0
  180. /package/dist/{chunk-QJW4MZSW.js.map → chunk-AYAZWXJI.js.map} +0 -0
  181. /package/dist/{chunk-WDXD6ITO.js.map → chunk-BXXOPKT5.js.map} +0 -0
  182. /package/dist/{chunk-DJZWNKFO.js.map → chunk-D35XLYQ7.js.map} +0 -0
  183. /package/dist/{chunk-KOO37E4Y.js.map → chunk-DYO24BNB.js.map} +0 -0
  184. /package/dist/{chunk-DORFF5N5.js.map → chunk-FEO2CDXR.js.map} +0 -0
  185. /package/dist/{chunk-IURMJBGP.js.map → chunk-HWVNYNIV.js.map} +0 -0
  186. /package/dist/{chunk-OZDFB22S.js.map → chunk-IDQ6DKXC.js.map} +0 -0
  187. /package/dist/{chunk-J3ATBB7M.js.map → chunk-IF4EY26E.js.map} +0 -0
  188. /package/dist/{chunk-W675UJCZ.js.map → chunk-IRXMJLTW.js.map} +0 -0
  189. /package/dist/{chunk-DUZARALI.js.map → chunk-JFWZRUKP.js.map} +0 -0
  190. /package/dist/{chunk-DUEDOYZD.js.map → chunk-JGG5HVI2.js.map} +0 -0
  191. /package/dist/{chunk-3NH3VP3U.js.map → chunk-KBOG7FJV.js.map} +0 -0
  192. /package/dist/{chunk-UCKG36HO.js.map → chunk-LSYD7GHZ.js.map} +0 -0
  193. /package/dist/{chunk-6433HV72.js.map → chunk-MVIYRRVJ.js.map} +0 -0
  194. /package/dist/{chunk-GHD2LLG4.js.map → chunk-NLUCDKLT.js.map} +0 -0
  195. /package/dist/{chunk-TJFI3K2O.js.map → chunk-O4BAGJDN.js.map} +0 -0
  196. /package/dist/{chunk-LYBIHM4N.js.map → chunk-OMZLBT4B.js.map} +0 -0
  197. /package/dist/{chunk-BXLOS5AJ.js.map → chunk-OWHERGF2.js.map} +0 -0
  198. /package/dist/{chunk-I23ZHYSL.js.map → chunk-PGW64IFQ.js.map} +0 -0
  199. /package/dist/{chunk-YDKF4MSJ.js.map → chunk-QMZY3GG6.js.map} +0 -0
  200. /package/dist/{chunk-N37X5RU7.js.map → chunk-RACZB5HT.js.map} +0 -0
  201. /package/dist/{chunk-YZJM6CLF.js.map → chunk-ROEAEANL.js.map} +0 -0
  202. /package/dist/{chunk-SR5LNI6Q.js.map → chunk-RQ2EQRRR.js.map} +0 -0
  203. /package/dist/{chunk-2IXQSGCU.js.map → chunk-SR7JIRAR.js.map} +0 -0
  204. /package/dist/{chunk-OCCHUEZB.js.map → chunk-TWBQQ3FT.js.map} +0 -0
  205. /package/dist/{chunk-NLEH4RTB.js.map → chunk-US4IRASK.js.map} +0 -0
  206. /package/dist/{chunk-DQEMWVMT.js.map → chunk-UVYI6VIX.js.map} +0 -0
  207. /package/dist/{chunk-PTZQYCJ4.js.map → chunk-W6J4POB6.js.map} +0 -0
  208. /package/dist/{chunk-OU34IQXV.js.map → chunk-X2DWBWOX.js.map} +0 -0
  209. /package/dist/{chunk-2MVTAIL3.js.map → chunk-XAWIL7TC.js.map} +0 -0
  210. /package/dist/{chunk-UFR6HX7A.js.map → chunk-XUZ5CIEE.js.map} +0 -0
  211. /package/dist/{chunk-GIC6BASP.js.map → chunk-XZBXINXZ.js.map} +0 -0
  212. /package/dist/{chunk-XGM3VQWG.js.map → chunk-YUTTMSC7.js.map} +0 -0
  213. /package/dist/{chunk-3JZBPTLD.js.map → chunk-YWBOOBDP.js.map} +0 -0
  214. /package/dist/{chunk-KFIOPZ25.js.map → chunk-ZXUZ54Z5.js.map} +0 -0
@@ -0,0 +1,347 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import {
5
+ MemoryControllerCoordinator,
6
+ chooseAction,
7
+ computeEvidenceReportHash,
8
+ controllerConfigHash,
9
+ evaluateActiveGates,
10
+ hashControllerState,
11
+ type ActiveContextPlan,
12
+ type MemoryControllerConfig,
13
+ type MemoryControllerDeps,
14
+ type MemoryControllerEvent,
15
+ type MemoryControllerEvidence,
16
+ type MemoryControllerExecutors,
17
+ type MemoryControllerMode,
18
+ } from "./memory-controller.js";
19
+
20
+ const NOW = new Date("2026-08-18T12:00:00.000Z");
21
+
22
+ function config(mode: MemoryControllerMode): MemoryControllerConfig {
23
+ return {
24
+ mode,
25
+ reportVersion: "2345-v1",
26
+ evidenceMaxAgeMs: 24 * 60 * 60 * 1000,
27
+ minPairedSeeds: 2,
28
+ shadowMinRecords: 3,
29
+ };
30
+ }
31
+
32
+ function evidence(overrides?: {
33
+ report?: Partial<MemoryControllerEvidence["report"]>;
34
+ pairedSeed?: Partial<MemoryControllerEvidence["pairedSeed"]>;
35
+ }): MemoryControllerEvidence {
36
+ const report = {
37
+ status: "pass",
38
+ version: "2345-v1",
39
+ configHash: controllerConfigHash(config("active")),
40
+ generatedAt: NOW.toISOString(),
41
+ ...overrides?.report,
42
+ } as MemoryControllerEvidence["report"];
43
+ return {
44
+ report: { ...report, reportHash: computeEvidenceReportHash(report) },
45
+ pairedSeed: {
46
+ status: "pass",
47
+ seedCount: 3,
48
+ generatedAt: NOW.toISOString(),
49
+ ...overrides?.pairedSeed,
50
+ },
51
+ };
52
+ }
53
+
54
+ function echoReceipt(input: { decisionId: string }): { decisionId: string; ok: true } {
55
+ return { decisionId: input.decisionId, ok: true };
56
+ }
57
+
58
+ function harness(options?: {
59
+ mode?: MemoryControllerMode;
60
+ evidenceOverride?: MemoryControllerEvidence | null;
61
+ shadowRecords?: number;
62
+ recordThrows?: boolean;
63
+ executors?: MemoryControllerExecutors;
64
+ adapterPlan?: ActiveContextPlan | null;
65
+ }) {
66
+ const events: MemoryControllerEvent[] = [];
67
+ const calls: string[] = [];
68
+ let dispatched = false;
69
+ const markDispatch =
70
+ <T>(label: string) =>
71
+ async (input: T & { decisionId: string }) => {
72
+ calls.push(label);
73
+ dispatched = true;
74
+ return echoReceipt(input);
75
+ };
76
+ const deps: MemoryControllerDeps = {
77
+ config: config(options?.mode ?? "active"),
78
+ reportReader: {
79
+ read: async () =>
80
+ options?.evidenceOverride === undefined || options.evidenceOverride !== null
81
+ ? (options?.evidenceOverride ?? evidence())
82
+ : null,
83
+ },
84
+ recorder: {
85
+ record: async (event) => {
86
+ if (options?.recordThrows) throw new Error("recorder down");
87
+ if (event.phase === "choice") {
88
+ // Gate proof: the choice record must precede any dispatch.
89
+ assert.ok(!dispatched, "choice event recorded after dispatch");
90
+ }
91
+ events.push(event);
92
+ },
93
+ countShadowRecords: async () => options?.shadowRecords ?? 10,
94
+ },
95
+ executors:
96
+ options?.executors ??
97
+ ({
98
+ executePersistentMemory: markDispatch("persistent"),
99
+ executeRecall: markDispatch("recall"),
100
+ executeActiveContext: markDispatch("context"),
101
+ } as MemoryControllerExecutors),
102
+ activeContextAdapter:
103
+ options?.adapterPlan === null
104
+ ? undefined
105
+ : {
106
+ plan: () => options?.adapterPlan ?? { transform: "FILTER", messageIds: ["m1"], planId: "p1" },
107
+ },
108
+ telemetry: (event) => calls.push(`telemetry:${event.phase}`),
109
+ clock: () => NOW,
110
+ };
111
+ return { deps, events, calls };
112
+ }
113
+
114
+ const RECALL_PROMPT = "what did we decide last time?";
115
+
116
+ test("off mode makes no choice, no read, no write, no call", async () => {
117
+ const h = harness({ mode: "off" });
118
+ const explode = async () => {
119
+ throw new Error("touched in off mode");
120
+ };
121
+ h.deps.reportReader = { read: explode };
122
+ h.deps.recorder = { record: explode, countShadowRecords: explode };
123
+ h.deps.executors = { executeRecall: explode } as MemoryControllerExecutors;
124
+ h.deps.policyRuntime = { loadRuntimeValues: explode };
125
+ h.deps.readUtilityRuntime = explode;
126
+
127
+ const result = await new MemoryControllerCoordinator(h.deps).run({ prompt: RECALL_PROMPT });
128
+
129
+ assert.equal(result.effectiveMode, "off");
130
+ assert.equal(result.executed, false);
131
+ assert.equal(result.recorded, false);
132
+ assert.deepEqual(h.events, []);
133
+ assert.deepEqual(h.calls, []);
134
+ });
135
+
136
+ test("shadow mode records the choice but calls no executor", async () => {
137
+ const h = harness({ mode: "shadow" });
138
+ const result = await new MemoryControllerCoordinator(h.deps).run({ prompt: RECALL_PROMPT });
139
+
140
+ assert.equal(result.effectiveMode, "shadow");
141
+ assert.equal(result.recorded, true);
142
+ assert.equal(result.executed, false);
143
+ assert.deepEqual(h.calls, ["telemetry:choice"]);
144
+ assert.equal(h.events.length, 1);
145
+ assert.equal(h.events[0].phase, "choice");
146
+ assert.ok(h.events[0].stateHash.length > 0);
147
+ });
148
+
149
+ test("active mode with passing gates executes and links one id across events", async () => {
150
+ const h = harness();
151
+ const result = await new MemoryControllerCoordinator(h.deps).run({ prompt: RECALL_PROMPT });
152
+
153
+ assert.equal(result.effectiveMode, "active");
154
+ assert.deepEqual(result.demotionReasons, []);
155
+ assert.equal(result.choice.family, "recall");
156
+ assert.ok(h.calls.includes("recall"));
157
+ const ids = new Set(h.events.map((event) => event.decisionId));
158
+ ids.add(result.decisionId);
159
+ if (result.receipt) ids.add(result.receipt.decisionId);
160
+ assert.equal(ids.size, 1, "one id joins each event");
161
+ assert.equal(result.executed, true);
162
+ const outcome = h.events.find((event) => event.phase === "outcome");
163
+ assert.equal(outcome?.receipt?.decisionId, result.decisionId);
164
+ });
165
+
166
+ test("bad or old evidence forces shadow: report gates", async () => {
167
+ const cases: { name: string; options: Parameters<typeof harness>[0] }[] = [
168
+ { name: "no evidence", options: { evidenceOverride: null } },
169
+ {
170
+ name: "report not passing",
171
+ options: { evidenceOverride: evidence({ report: { status: "fail" } }) },
172
+ },
173
+ {
174
+ name: "version mismatch",
175
+ options: { evidenceOverride: evidence({ report: { version: "2345-v0" } }) },
176
+ },
177
+ {
178
+ name: "config unbound",
179
+ options: {
180
+ evidenceOverride: evidence({
181
+ report: { configHash: controllerConfigHash({ ...config("active"), shadowMinRecords: 99 }) },
182
+ }),
183
+ },
184
+ },
185
+ {
186
+ name: "report hash invalid",
187
+ options: {
188
+ evidenceOverride: {
189
+ report: { ...evidence().report, reportHash: "0".repeat(64) },
190
+ pairedSeed: evidence().pairedSeed,
191
+ },
192
+ },
193
+ },
194
+ {
195
+ name: "report stale",
196
+ options: { evidenceOverride: evidence({ report: { generatedAt: "2020-01-01T00:00:00.000Z" } }) },
197
+ },
198
+ ];
199
+ for (const testCase of cases) {
200
+ const h = harness(testCase.options);
201
+ const result = await new MemoryControllerCoordinator(h.deps).run({ prompt: RECALL_PROMPT });
202
+ assert.equal(result.effectiveMode, "shadow", testCase.name);
203
+ assert.equal(result.executed, false, testCase.name);
204
+ assert.ok(!h.calls.some((c) => c !== "telemetry:choice"), testCase.name);
205
+ }
206
+ });
207
+
208
+ test("bad or old paired-seed evidence forces shadow", async () => {
209
+ const pairedSeeds = [
210
+ { status: "fail" as const, seedCount: 3, generatedAt: NOW.toISOString() },
211
+ { status: "pass" as const, seedCount: 1, generatedAt: NOW.toISOString() },
212
+ { status: "pass" as const, seedCount: 3, generatedAt: "2020-01-01T00:00:00.000Z" },
213
+ ];
214
+ for (const pairedSeed of pairedSeeds) {
215
+ const h = harness({ evidenceOverride: evidence({ pairedSeed }) });
216
+ const result = await new MemoryControllerCoordinator(h.deps).run({ prompt: RECALL_PROMPT });
217
+ assert.equal(result.effectiveMode, "shadow", JSON.stringify(pairedSeed));
218
+ assert.ok(result.demotionReasons.some((r) => r.startsWith("paired_seed")));
219
+ assert.equal(result.executed, false);
220
+ }
221
+ });
222
+
223
+ test("active promotion requires prior shadow records (shadow before active)", async () => {
224
+ const h = harness({ shadowRecords: 1 });
225
+ const result = await new MemoryControllerCoordinator(h.deps).run({ prompt: RECALL_PROMPT });
226
+ assert.equal(result.effectiveMode, "shadow");
227
+ assert.ok(result.demotionReasons.includes("shadow_history_insufficient"));
228
+ assert.equal(result.executed, false);
229
+ });
230
+
231
+ test("failed receipt forces shadow on the next run", async () => {
232
+ const h = harness({
233
+ executors: {
234
+ executeRecall: async () => ({ decisionId: "wrong-id", ok: true }),
235
+ },
236
+ });
237
+ const coordinator = new MemoryControllerCoordinator(h.deps);
238
+ const first = await coordinator.run({ prompt: RECALL_PROMPT });
239
+ assert.equal(first.executed, false, "mismatched receipt id is a bad receipt");
240
+ assert.ok(h.events.find((event) => event.phase === "outcome")?.failureClass === "bad_receipt");
241
+
242
+ const second = await coordinator.run({ prompt: RECALL_PROMPT });
243
+ assert.equal(second.effectiveMode, "shadow");
244
+ assert.ok(second.demotionReasons.includes("recent_receipt_failures"));
245
+ assert.equal(second.executed, false);
246
+ });
247
+
248
+ test("executor error or missing executor records a failure class and demotes next run", async () => {
249
+ const h = harness({
250
+ executors: {
251
+ executeRecall: async () => Promise.reject(new Error("boom")),
252
+ },
253
+ });
254
+ const coordinator = new MemoryControllerCoordinator(h.deps);
255
+ const failed = await coordinator.run({ prompt: RECALL_PROMPT });
256
+ assert.equal(failed.executed, false);
257
+ assert.equal(h.events.find((event) => event.phase === "outcome")?.failureClass, "executor_error");
258
+ const next = await coordinator.run({ prompt: RECALL_PROMPT });
259
+ assert.equal(next.effectiveMode, "shadow");
260
+
261
+ const h2 = harness({ executors: {} });
262
+ const missing = await new MemoryControllerCoordinator(h2.deps).run({ prompt: RECALL_PROMPT });
263
+ assert.equal(missing.executed, false);
264
+ assert.equal(h2.events.find((event) => event.phase === "outcome")?.failureClass, "no_executor");
265
+ });
266
+
267
+ test("bad scores fail closed to no_op", () => {
268
+ const { choice, scores } = chooseAction({
269
+ state: {
270
+ prompt: RECALL_PROMPT,
271
+ persistentMemoryCandidate: {
272
+ action: "store_note",
273
+ eligibility: {
274
+ confidence: Number.NaN,
275
+ lifecycleState: "active",
276
+ importance: 0.5,
277
+ source: "extraction",
278
+ },
279
+ },
280
+ },
281
+ });
282
+ assert.deepEqual(choice, { family: "no_op" });
283
+ assert.ok(Number.isNaN(scores.persistent_memory));
284
+ });
285
+
286
+ test("discard stays review-only in active mode", async () => {
287
+ const h = harness();
288
+ const result = await new MemoryControllerCoordinator(h.deps).run({
289
+ prompt: "ok",
290
+ persistentMemoryCandidate: {
291
+ action: "discard",
292
+ eligibility: {
293
+ confidence: 0.9,
294
+ lifecycleState: "active",
295
+ importance: 0.1,
296
+ source: "extraction",
297
+ },
298
+ },
299
+ });
300
+ assert.equal(result.choice.family, "persistent_memory");
301
+ assert.equal(result.choice.action, "discard");
302
+ assert.equal(result.reviewOnly, true);
303
+ assert.equal(result.executed, false);
304
+ assert.deepEqual(h.calls, ["telemetry:choice", "telemetry:outcome"]);
305
+ assert.equal(h.events.find((event) => event.phase === "outcome")?.reviewOnly, true);
306
+ });
307
+
308
+ test("record failure stops work before dispatch", async () => {
309
+ const h = harness({ recordThrows: true });
310
+ const result = await new MemoryControllerCoordinator(h.deps).run({ prompt: RECALL_PROMPT });
311
+ assert.equal(result.recorded, false);
312
+ assert.equal(result.executed, false);
313
+ assert.ok(result.demotionReasons.includes("record_failed"));
314
+ assert.deepEqual(h.calls, []);
315
+ });
316
+
317
+ test("one hash names each state", () => {
318
+ assert.equal(hashControllerState({ prompt: "same" }), hashControllerState({ prompt: "same" }));
319
+ assert.notEqual(hashControllerState({ prompt: "same" }), hashControllerState({ prompt: "other" }));
320
+ });
321
+
322
+ test("no counterfactual lift is claimed in events or results", async () => {
323
+ const h = harness();
324
+ const result = await new MemoryControllerCoordinator(h.deps).run({ prompt: RECALL_PROMPT });
325
+ const banned = /lift|counterfactual/i;
326
+ for (const event of h.events) {
327
+ for (const key of Object.keys(event)) assert.ok(!banned.test(key), `event key ${key}`);
328
+ }
329
+ for (const key of Object.keys(result)) assert.ok(!banned.test(key), `result key ${key}`);
330
+ });
331
+
332
+ test("evaluateActiveGates returns empty only when all gates pass", async () => {
333
+ const passing = await evaluateActiveGates({
334
+ config: config("active"),
335
+ reportReader: { read: async () => evidence() },
336
+ recorder: { record: async () => {}, countShadowRecords: async () => 5 },
337
+ consecutiveReceiptFailures: 0,
338
+ now: NOW,
339
+ });
340
+ assert.deepEqual(passing, []);
341
+ const noReader = await evaluateActiveGates({
342
+ config: config("active"),
343
+ consecutiveReceiptFailures: 0,
344
+ now: NOW,
345
+ });
346
+ assert.deepEqual(noReader, ["no_report_reader"]);
347
+ });