@remnic/core 9.7.11 → 9.7.13

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 (286) hide show
  1. package/dist/access-admin-ops-surface.d.ts +6 -6
  2. package/dist/access-admin-ops-surface.js +7 -7
  3. package/dist/access-boundary.d.ts +6 -6
  4. package/dist/access-boundary.js +8 -8
  5. package/dist/access-cli.js +31 -31
  6. package/dist/access-http.d.ts +6 -6
  7. package/dist/access-http.js +12 -12
  8. package/dist/access-identity-continuity-surface.d.ts +5 -5
  9. package/dist/access-identity-continuity-surface.js +7 -7
  10. package/dist/access-lcm-surface.d.ts +6 -6
  11. package/dist/access-lcm-surface.js +7 -7
  12. package/dist/access-mcp.d.ts +6 -6
  13. package/dist/access-mcp.js +11 -11
  14. package/dist/access-observe-write-surface.d.ts +6 -6
  15. package/dist/access-observe-write-surface.js +7 -7
  16. package/dist/access-operations-batch.js +9 -9
  17. package/dist/access-operations.d.ts +9 -9
  18. package/dist/access-operations.js +10 -10
  19. package/dist/access-recall-surface.d.ts +6 -6
  20. package/dist/access-recall-surface.js +7 -7
  21. package/dist/access-schema.d.ts +64 -64
  22. package/dist/{access-service-DSuzikHQ.d.ts → access-service-tHd3eQpd.d.ts} +4 -4
  23. package/dist/access-service.d.ts +6 -6
  24. package/dist/access-service.js +7 -7
  25. package/dist/access-surface-catalog.d.ts +6 -6
  26. package/dist/action-confidence.d.ts +1 -1
  27. package/dist/active-memory-bridge.d.ts +1 -1
  28. package/dist/active-recall.d.ts +1 -1
  29. package/dist/active-recall.js +1 -1
  30. package/dist/behavior-learner.d.ts +1 -1
  31. package/dist/behavior-signals.d.ts +1 -1
  32. package/dist/bootstrap.d.ts +5 -5
  33. package/dist/briefing.d.ts +2 -2
  34. package/dist/briefing.js +3 -3
  35. package/dist/buffer-surprise-report.d.ts +1 -1
  36. package/dist/buffer.d.ts +2 -2
  37. package/dist/bulk-import/index.d.ts +3 -3
  38. package/dist/calibration.d.ts +1 -1
  39. package/dist/capabilities.d.ts +1 -1
  40. package/dist/{catalog-BNwjACa5.d.ts → catalog-DEMiw6HZ.d.ts} +48 -1
  41. package/dist/causal-behavior.d.ts +1 -1
  42. package/dist/causal-consolidation.d.ts +1 -1
  43. package/dist/causal-consolidation.js +4 -4
  44. package/dist/causal-trajectory-graph.d.ts +1 -1
  45. package/dist/{chunk-U4KHQTJ6.js → chunk-2AL7N3BH.js} +4 -2
  46. package/dist/{chunk-U4KHQTJ6.js.map → chunk-2AL7N3BH.js.map} +1 -1
  47. package/dist/{chunk-L4H6MQ7I.js → chunk-2RSL6YRA.js} +2 -2
  48. package/dist/{chunk-QHT4W3RU.js → chunk-2V5QI6NQ.js} +41 -9
  49. package/dist/chunk-2V5QI6NQ.js.map +1 -0
  50. package/dist/{chunk-JYRFO4EH.js → chunk-2ZPQUTMW.js} +4 -4
  51. package/dist/{chunk-673NPXE3.js → chunk-3SIC3E65.js} +2 -2
  52. package/dist/{chunk-OVMS3DCB.js → chunk-675YU5IK.js} +2 -2
  53. package/dist/{chunk-7LMEY5KZ.js → chunk-6N7R7TR2.js} +34 -1
  54. package/dist/chunk-6N7R7TR2.js.map +1 -0
  55. package/dist/{chunk-TGBJ2OMK.js → chunk-BQTSGK5Y.js} +2 -2
  56. package/dist/{chunk-6LM6PPDX.js → chunk-CE3GMHT4.js} +2 -2
  57. package/dist/{chunk-YXOAQAH2.js → chunk-CLFY35ED.js} +3 -3
  58. package/dist/{chunk-HQ7QZJM5.js → chunk-D4TLWDYR.js} +2 -2
  59. package/dist/{chunk-LQKITFGB.js → chunk-FUUF7FMX.js} +2 -2
  60. package/dist/{chunk-3ZVFLMFQ.js → chunk-G5PVJJRA.js} +1 -1
  61. package/dist/chunk-G5PVJJRA.js.map +1 -0
  62. package/dist/{chunk-MXRMNKSY.js → chunk-I3GKTDM2.js} +3 -3
  63. package/dist/{chunk-EQF5763Z.js → chunk-IQULRIZM.js} +2 -2
  64. package/dist/{chunk-W6R43NLA.js → chunk-KQMKF4G2.js} +17 -17
  65. package/dist/{chunk-DJCKXI4M.js → chunk-NVXQTRBL.js} +2 -2
  66. package/dist/{chunk-GHVJASQ4.js → chunk-OCY3CRPG.js} +3 -3
  67. package/dist/{chunk-YLTG6BO4.js → chunk-OIFZII6J.js} +2 -2
  68. package/dist/{chunk-XF4KOVDK.js → chunk-PA6DMYEK.js} +6 -6
  69. package/dist/{chunk-3OQ4IZJN.js → chunk-QPZCAVDC.js} +7 -7
  70. package/dist/{chunk-DLWYAN6A.js → chunk-T6U7HDIY.js} +2 -2
  71. package/dist/{chunk-JZM5QUTE.js → chunk-TMGK6YE7.js} +171 -9
  72. package/dist/chunk-TMGK6YE7.js.map +1 -0
  73. package/dist/{chunk-KXLRMM35.js → chunk-TRH533CA.js} +30 -7
  74. package/dist/chunk-TRH533CA.js.map +1 -0
  75. package/dist/{chunk-UTDA43TB.js → chunk-TWGGNUWJ.js} +2 -2
  76. package/dist/{chunk-SBFDL2EQ.js → chunk-UTZW3X2R.js} +2 -2
  77. package/dist/{chunk-SECMZACR.js → chunk-XUQUBZ7O.js} +2 -2
  78. package/dist/{chunk-5U65RIVI.js → chunk-YI7XUXLX.js} +16 -15
  79. package/dist/chunk-YI7XUXLX.js.map +1 -0
  80. package/dist/{chunk-7XRUYC6G.js → chunk-Z6WN2KSH.js} +19 -5
  81. package/dist/chunk-Z6WN2KSH.js.map +1 -0
  82. package/dist/{chunk-WUDBMLC6.js → chunk-ZC3HG22I.js} +2 -2
  83. package/dist/{chunk-VCEH5UYS.js → chunk-ZRXSHGXT.js} +2 -2
  84. package/dist/{cli-DACyuK8-.d.ts → cli-D8Tsa4Sh.d.ts} +5 -5
  85. package/dist/cli.d.ts +8 -8
  86. package/dist/cli.js +25 -25
  87. package/dist/compounding/engine.d.ts +2 -2
  88. package/dist/compounding/engine.js +3 -3
  89. package/dist/compounding/preference-consolidator.d.ts +1 -1
  90. package/dist/compression-optimizer.d.ts +1 -1
  91. package/dist/config.d.ts +1 -1
  92. package/dist/config.js +1 -1
  93. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  94. package/dist/connectors/codex-materialize-runner.js +3 -3
  95. package/dist/connectors/codex-materialize.d.ts +1 -1
  96. package/dist/connectors/index.d.ts +1 -1
  97. package/dist/connectors/index.js +3 -3
  98. package/dist/consolidation-provenance-check.d.ts +2 -2
  99. package/dist/consolidation-undo.d.ts +2 -2
  100. package/dist/contradiction/index.d.ts +2 -2
  101. package/dist/conversation-index/backend.d.ts +1 -1
  102. package/dist/conversation-index/chunker.d.ts +1 -1
  103. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  104. package/dist/conversation-index/indexer.d.ts +1 -1
  105. package/dist/conversation-index/search.d.ts +1 -1
  106. package/dist/day-summary.d.ts +1 -1
  107. package/dist/delinearize.d.ts +1 -1
  108. package/dist/direct-answer-wiring.d.ts +1 -1
  109. package/dist/direct-answer.d.ts +1 -1
  110. package/dist/embedding-fallback.d.ts +1 -1
  111. package/dist/enrichment/index.d.ts +1 -1
  112. package/dist/entity-retrieval.d.ts +2 -2
  113. package/dist/entity-retrieval.js +3 -3
  114. package/dist/entity-schema.d.ts +1 -1
  115. package/dist/explicit-capture.d.ts +5 -5
  116. package/dist/extraction-faithfulness.d.ts +1 -1
  117. package/dist/extraction-judge-telemetry.d.ts +1 -1
  118. package/dist/extraction-judge-training.d.ts +1 -1
  119. package/dist/extraction-judge.d.ts +1 -1
  120. package/dist/extraction.d.ts +1 -1
  121. package/dist/fallback-llm.d.ts +1 -1
  122. package/dist/graph-dashboard-diff.d.ts +1 -1
  123. package/dist/graph-dashboard-key.d.ts +1 -1
  124. package/dist/graph-dashboard-parser.d.ts +1 -1
  125. package/dist/graph-edge-reinforcement.d.ts +1 -1
  126. package/dist/graph-snapshot.d.ts +1 -1
  127. package/dist/graph.d.ts +1 -1
  128. package/dist/identity-continuity.d.ts +1 -1
  129. package/dist/importance.d.ts +1 -1
  130. package/dist/importers/index.d.ts +1 -1
  131. package/dist/in-flight-reads.d.ts +1 -1
  132. package/dist/index.d.ts +386 -386
  133. package/dist/index.js +31 -31
  134. package/dist/intent.d.ts +1 -1
  135. package/dist/lcm/engine.d.ts +1 -1
  136. package/dist/lcm/index.d.ts +1 -1
  137. package/dist/lcm/tools.d.ts +1 -1
  138. package/dist/lifecycle.d.ts +1 -1
  139. package/dist/live-connectors-runner.d.ts +1 -1
  140. package/dist/local-llm.d.ts +1 -1
  141. package/dist/local-model-endpoint.d.ts +1 -1
  142. package/dist/maintenance/memory-governance.d.ts +1 -1
  143. package/dist/maintenance/memory-governance.js +3 -3
  144. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  145. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  146. package/dist/mcp-memory-inspector-app.d.ts +6 -6
  147. package/dist/memory-action-policy.d.ts +1 -1
  148. package/dist/memory-cache.d.ts +1 -1
  149. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  150. package/dist/memory-projection-store.d.ts +1 -1
  151. package/dist/memory-provenance.d.ts +1 -1
  152. package/dist/memory-worth-outcomes.d.ts +2 -2
  153. package/dist/models-json.d.ts +1 -1
  154. package/dist/namespaces/migrate.d.ts +3 -3
  155. package/dist/namespaces/migrate.js +4 -4
  156. package/dist/namespaces/principal.d.ts +1 -1
  157. package/dist/namespaces/search.d.ts +1 -1
  158. package/dist/namespaces/storage.d.ts +3 -3
  159. package/dist/namespaces/storage.js +3 -3
  160. package/dist/native-knowledge.d.ts +1 -1
  161. package/dist/operator-toolkit.d.ts +2 -2
  162. package/dist/operator-toolkit.js +8 -8
  163. package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
  164. package/dist/orchestration/maintenance.d.ts +2 -2
  165. package/dist/orchestration/maintenance.js +5 -5
  166. package/dist/{orchestrator-D83AvrPf.d.ts → orchestrator-B_WXBmuE.d.ts} +4 -4
  167. package/dist/orchestrator.d.ts +5 -5
  168. package/dist/orchestrator.js +31 -31
  169. package/dist/patterns-cli.d.ts +1 -1
  170. package/dist/{pipeline-BSqGpYl1.d.ts → pipeline-Dvtvt141.d.ts} +1 -1
  171. package/dist/policy-runtime.d.ts +1 -1
  172. package/dist/provenance.d.ts +1 -1
  173. package/dist/{qmd-CutnpRVe.d.ts → qmd-DwBLh_hl.d.ts} +1 -1
  174. package/dist/qmd-preflight.d.ts +2 -2
  175. package/dist/qmd-recall-cache.d.ts +1 -1
  176. package/dist/qmd.d.ts +2 -2
  177. package/dist/recall-disclosure-escalation.d.ts +1 -1
  178. package/dist/recall-explain-renderer.d.ts +1 -1
  179. package/dist/recall-explain-renderer.js +3 -3
  180. package/dist/recall-planner-llm.d.ts +1 -1
  181. package/dist/recall-state.d.ts +1 -1
  182. package/dist/recall-tag-filter.d.ts +1 -1
  183. package/dist/recall-timings.d.ts +1 -1
  184. package/dist/recall-xray-cli.d.ts +1 -1
  185. package/dist/recall-xray-cli.js +4 -4
  186. package/dist/recall-xray-renderer.d.ts +1 -1
  187. package/dist/recall-xray-renderer.js +3 -3
  188. package/dist/recall-xray.d.ts +1 -1
  189. package/dist/recall-xray.js +2 -2
  190. package/dist/replay/normalizers/chatgpt.d.ts +1 -1
  191. package/dist/replay/normalizers/claude.d.ts +1 -1
  192. package/dist/replay/normalizers/openclaw.d.ts +1 -1
  193. package/dist/replay/normalizers/shared.d.ts +1 -1
  194. package/dist/replay/runner.d.ts +1 -1
  195. package/dist/replay/types.d.ts +1 -1
  196. package/dist/resolve-auth-token.d.ts +1 -1
  197. package/dist/resume-bundles.js +2 -2
  198. package/dist/retrieval-agents.d.ts +2 -2
  199. package/dist/retrieval-tiers.d.ts +1 -1
  200. package/dist/routing/engine.d.ts +1 -1
  201. package/dist/routing/store.d.ts +1 -1
  202. package/dist/schemas.d.ts +74 -74
  203. package/dist/search/embed-helper.d.ts +1 -1
  204. package/dist/search/factory.d.ts +1 -1
  205. package/dist/search/index.d.ts +1 -1
  206. package/dist/search/lancedb-backend.d.ts +1 -1
  207. package/dist/search/meilisearch-backend.d.ts +1 -1
  208. package/dist/search/noop-backend.d.ts +1 -1
  209. package/dist/search/orama-backend.d.ts +1 -1
  210. package/dist/search/port.d.ts +1 -1
  211. package/dist/search/remote-backend.d.ts +1 -1
  212. package/dist/{semantic-consolidation-btIVRC4l.d.ts → semantic-consolidation-Dia_6FOn.d.ts} +1 -1
  213. package/dist/semantic-consolidation.d.ts +2 -2
  214. package/dist/semantic-consolidation.js +4 -4
  215. package/dist/semantic-rule-promotion.js +3 -3
  216. package/dist/semantic-rule-verifier.d.ts +1 -1
  217. package/dist/semantic-rule-verifier.js +3 -3
  218. package/dist/session-observer-bands.d.ts +1 -1
  219. package/dist/session-observer-state.d.ts +1 -1
  220. package/dist/shared-context/manager.d.ts +9 -9
  221. package/dist/signal.d.ts +1 -1
  222. package/dist/{storage-BVv_Mqa0.d.ts → storage-RshM7i8-.d.ts} +18 -1
  223. package/dist/storage.d.ts +2 -2
  224. package/dist/storage.js +2 -2
  225. package/dist/summarizer.d.ts +1 -1
  226. package/dist/summary-snapshot.d.ts +1 -1
  227. package/dist/temporal-supersession.d.ts +2 -2
  228. package/dist/temporal-timeline-recall.d.ts +1 -1
  229. package/dist/temporal-validity.d.ts +1 -1
  230. package/dist/threading.d.ts +1 -1
  231. package/dist/tier-migration.d.ts +2 -2
  232. package/dist/tier-routing.d.ts +1 -1
  233. package/dist/topics.d.ts +1 -1
  234. package/dist/transcript.d.ts +1 -1
  235. package/dist/transfer/types.d.ts +66 -66
  236. package/dist/trust-score-stage.d.ts +1 -1
  237. package/dist/trust-score.d.ts +1 -1
  238. package/dist/{types-Cmkz7csa.d.ts → types-CHSXOq7s.d.ts} +34 -0
  239. package/dist/types.d.ts +1 -1
  240. package/dist/types.js +1 -1
  241. package/dist/utility-runtime.d.ts +1 -1
  242. package/dist/verified-recall.js +3 -3
  243. package/package.json +2 -2
  244. package/src/access-http.ts +40 -2
  245. package/src/config.test.ts +44 -0
  246. package/src/config.ts +43 -0
  247. package/src/namespaces/catalog.test.ts +258 -0
  248. package/src/namespaces/catalog.ts +235 -10
  249. package/src/namespaces/storage.ts +9 -0
  250. package/src/orchestrator.ts +3 -0
  251. package/src/relay/mission-access.test.ts +58 -3
  252. package/src/relay/mission-access.ts +17 -3
  253. package/src/relay/mission-ui.test.ts +90 -0
  254. package/src/storage-catalog-touch-gating.test.ts +70 -0
  255. package/src/storage.ts +31 -5
  256. package/src/types.ts +34 -0
  257. package/dist/chunk-3ZVFLMFQ.js.map +0 -1
  258. package/dist/chunk-5U65RIVI.js.map +0 -1
  259. package/dist/chunk-7LMEY5KZ.js.map +0 -1
  260. package/dist/chunk-7XRUYC6G.js.map +0 -1
  261. package/dist/chunk-JZM5QUTE.js.map +0 -1
  262. package/dist/chunk-KXLRMM35.js.map +0 -1
  263. package/dist/chunk-QHT4W3RU.js.map +0 -1
  264. /package/dist/{chunk-L4H6MQ7I.js.map → chunk-2RSL6YRA.js.map} +0 -0
  265. /package/dist/{chunk-JYRFO4EH.js.map → chunk-2ZPQUTMW.js.map} +0 -0
  266. /package/dist/{chunk-673NPXE3.js.map → chunk-3SIC3E65.js.map} +0 -0
  267. /package/dist/{chunk-OVMS3DCB.js.map → chunk-675YU5IK.js.map} +0 -0
  268. /package/dist/{chunk-TGBJ2OMK.js.map → chunk-BQTSGK5Y.js.map} +0 -0
  269. /package/dist/{chunk-6LM6PPDX.js.map → chunk-CE3GMHT4.js.map} +0 -0
  270. /package/dist/{chunk-YXOAQAH2.js.map → chunk-CLFY35ED.js.map} +0 -0
  271. /package/dist/{chunk-HQ7QZJM5.js.map → chunk-D4TLWDYR.js.map} +0 -0
  272. /package/dist/{chunk-LQKITFGB.js.map → chunk-FUUF7FMX.js.map} +0 -0
  273. /package/dist/{chunk-MXRMNKSY.js.map → chunk-I3GKTDM2.js.map} +0 -0
  274. /package/dist/{chunk-EQF5763Z.js.map → chunk-IQULRIZM.js.map} +0 -0
  275. /package/dist/{chunk-W6R43NLA.js.map → chunk-KQMKF4G2.js.map} +0 -0
  276. /package/dist/{chunk-DJCKXI4M.js.map → chunk-NVXQTRBL.js.map} +0 -0
  277. /package/dist/{chunk-GHVJASQ4.js.map → chunk-OCY3CRPG.js.map} +0 -0
  278. /package/dist/{chunk-YLTG6BO4.js.map → chunk-OIFZII6J.js.map} +0 -0
  279. /package/dist/{chunk-XF4KOVDK.js.map → chunk-PA6DMYEK.js.map} +0 -0
  280. /package/dist/{chunk-3OQ4IZJN.js.map → chunk-QPZCAVDC.js.map} +0 -0
  281. /package/dist/{chunk-DLWYAN6A.js.map → chunk-T6U7HDIY.js.map} +0 -0
  282. /package/dist/{chunk-UTDA43TB.js.map → chunk-TWGGNUWJ.js.map} +0 -0
  283. /package/dist/{chunk-SBFDL2EQ.js.map → chunk-UTZW3X2R.js.map} +0 -0
  284. /package/dist/{chunk-SECMZACR.js.map → chunk-XUQUBZ7O.js.map} +0 -0
  285. /package/dist/{chunk-WUDBMLC6.js.map → chunk-ZC3HG22I.js.map} +0 -0
  286. /package/dist/{chunk-VCEH5UYS.js.map → chunk-ZRXSHGXT.js.map} +0 -0
@@ -414,6 +414,10 @@ export class NamespaceStorageRouter {
414
414
  * default-namespace storage (`this.storage`) that bypasses the router.
415
415
  */
416
416
  bindCatalogWriteHook(sm: StorageManager, namespace: string): void {
417
+ // Issue #1903: pure state-file writes only touch the catalog when explicitly
418
+ // opted in. Router-created storages (storageFor) and the legacy default
419
+ // storage both flow through here, so this is the single wiring point.
420
+ sm.touchStateWrites = this.config.namespacesCatalogTouchStateWrites === true;
417
421
  sm.onCatalogWrite = () => this.touchCatalogWrite(namespace, sm.dir);
418
422
  }
419
423
 
@@ -505,6 +509,11 @@ export class NamespaceStorageRouter {
505
509
  * moved should await this instead of racing a timer.
506
510
  */
507
511
  async whenWriteTouchesSettled(): Promise<void> {
512
+ // Flush any coalesced (buffered) touches first so their appends join the
513
+ // pending set below — otherwise this could resolve while a buffered touch's
514
+ // lastWriteAt is still unwritten on disk (#1903, Cursor). No-op when
515
+ // coalescing is disabled (window 0) or the catalog is inert.
516
+ await this.catalog?.flushPendingTouches().catch(() => undefined);
508
517
  while (this.pendingWriteTouches.size > 0) {
509
518
  await Promise.allSettled([...this.pendingWriteTouches]);
510
519
  }
@@ -948,6 +948,9 @@ export class Orchestrator {
948
948
  }
949
949
  this.maintenanceScheduler.dispose();
950
950
  await drainRecallWrites(this);
951
+ // Issue #1903: flush any coalesced namespace-catalog touches before teardown
952
+ // so a long-lived host does not drop buffered read/write timestamps.
953
+ await this.namespaceCatalog.flushPendingTouches().catch(() => undefined);
951
954
  await this.namespaceSearchRouter.dispose();
952
955
  await (this.qmd as { dispose?: () => void | Promise<void> }).dispose?.();
953
956
  if (this.conversationQmd && this.conversationQmd !== this.qmd) {
@@ -60,13 +60,33 @@ test("HTTP fixture endpoint returns one complete, namespace-authorized Relay rec
60
60
  service: fakeService(root, calls),
61
61
  port: 0,
62
62
  authToken: "relay-token",
63
- principal: "relay-operator",
63
+ principal: "operator-build-week",
64
64
  adminConsoleEnabled: false,
65
65
  });
66
66
  const status = await server.start();
67
67
  const base = `http://127.0.0.1:${status.port}/engram/v1/relay/missions/${RELAY_DEMO_MISSION_ID}`;
68
68
  try {
69
69
  const fixture = createRelayMissionFixture();
70
+ const approval = fixture.find((event) => event.payload.kind === "correction_approved");
71
+ assert.ok(approval && approval.payload.kind === "correction_approved");
72
+ const mismatchedApproval = await fetch(`${base}/events`, {
73
+ method: "POST",
74
+ headers: authHeaders(),
75
+ body: JSON.stringify({
76
+ namespace: RELAY_DEMO_NAMESPACE,
77
+ event: {
78
+ ...approval,
79
+ idempotencyKey: "mismatched-human-approval",
80
+ payload: {
81
+ ...approval.payload,
82
+ approvedBy: { ...approval.payload.approvedBy, id: "different-operator" },
83
+ },
84
+ },
85
+ }),
86
+ });
87
+ assert.equal(mismatchedApproval.status, 400);
88
+ assert.equal(((await mismatchedApproval.json()) as { code: string }).code, "input_error");
89
+
70
90
  const blankNamespace = await fetch(`${base}/events`, {
71
91
  method: "POST",
72
92
  headers: authHeaders(),
@@ -91,7 +111,7 @@ test("HTTP fixture endpoint returns one complete, namespace-authorized Relay rec
91
111
  assert.equal(response.status, 201, responseBody);
92
112
  assert.equal(
93
113
  (JSON.parse(responseBody) as { event: { authenticatedPrincipal?: string } }).event.authenticatedPrincipal,
94
- "relay-operator"
114
+ "operator-build-week"
95
115
  );
96
116
  }
97
117
 
@@ -108,6 +128,7 @@ test("HTTP fixture endpoint returns one complete, namespace-authorized Relay rec
108
128
  });
109
129
  assert.equal(response.status, 200);
110
130
  assert.equal(response.headers.get("cache-control"), "no-store");
131
+ assert.equal(response.headers.get("x-remnic-authenticated-principal"), "operator-build-week");
111
132
  const snapshot = (await response.json()) as RelayMissionSnapshot;
112
133
  assert.equal(snapshot.receipt.complete, true);
113
134
  assert.deepEqual(snapshot.receipt.activeDecisionIds, ["decision-refresh-after-expiry"]);
@@ -115,7 +136,40 @@ test("HTTP fixture endpoint returns one complete, namespace-authorized Relay rec
115
136
  assert.equal(snapshot.agents.length, 3);
116
137
  assert.equal(snapshot.tests.at(-1)?.status, "passed");
117
138
  assert.ok(calls.every((call) => call.namespace === RELAY_DEMO_NAMESPACE));
118
- assert.ok(calls.every((call) => call.principal === "relay-operator"));
139
+ assert.ok(calls.every((call) => call.principal === "operator-build-week"));
140
+ } finally {
141
+ await server.stop();
142
+ }
143
+ });
144
+ });
145
+
146
+ test("HTTP Relay rejects human approval without a server-resolved principal before persistence", async () => {
147
+ await withTempRoot(async (root) => {
148
+ const server = new EngramAccessHttpServer({
149
+ service: fakeService(root, []),
150
+ port: 0,
151
+ authToken: "relay-token",
152
+ adminConsoleEnabled: false,
153
+ });
154
+ const status = await server.start();
155
+ const base = `http://127.0.0.1:${status.port}/engram/v1/relay/missions/${RELAY_DEMO_MISSION_ID}`;
156
+ try {
157
+ const approval = createRelayMissionFixture().find((event) => event.payload.kind === "correction_approved");
158
+ assert.ok(approval);
159
+ const rejected = await fetch(`${base}/events`, {
160
+ method: "POST",
161
+ headers: authHeaders(),
162
+ body: JSON.stringify({ namespace: RELAY_DEMO_NAMESPACE, event: approval }),
163
+ });
164
+ assert.equal(rejected.status, 400);
165
+ assert.equal(((await rejected.json()) as { code: string }).code, "input_error");
166
+
167
+ const snapshotResponse = await fetch(`${base}?namespace=${RELAY_DEMO_NAMESPACE}`, {
168
+ headers: authHeaders(),
169
+ });
170
+ assert.equal(snapshotResponse.status, 200);
171
+ assert.equal(snapshotResponse.headers.get("x-remnic-authenticated-principal"), null);
172
+ assert.equal(((await snapshotResponse.json()) as RelayMissionSnapshot).found, false);
119
173
  } finally {
120
174
  await server.stop();
121
175
  }
@@ -150,6 +204,7 @@ test("HTTP Relay surface distinguishes empty, invalid, unauthorized, and backend
150
204
 
151
205
  const empty = await fetch(emptyUrl, { headers: authHeaders("read-token") });
152
206
  assert.equal(empty.status, 200);
207
+ assert.equal(empty.headers.get("x-remnic-authenticated-principal"), null);
153
208
  assert.deepEqual(await empty.json(), assertEmptyMissionShape());
154
209
 
155
210
  const badLimit = await fetch(`${emptyUrl}&limit=501`, {
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
 
3
3
  import { type OperationContext, defineOperation } from "../access-boundary.js";
4
- import type { EngramAccessService } from "../access-service.js";
4
+ import { EngramAccessInputError, type EngramAccessService } from "../access-service.js";
5
5
  import {
6
6
  RELAY_MISSION_MAX_EVENT_LIMIT,
7
7
  RelayIsoDateTimeSchema,
@@ -76,12 +76,26 @@ export async function appendRelayMissionEvent(
76
76
  authenticatedPrincipal?: string,
77
77
  beforeAppend?: () => void | Promise<void>
78
78
  ): Promise<RelayMissionAppendResult> {
79
- const resolved = await service.getWritableStorageForNamespace(input.namespace ?? undefined, authenticatedPrincipal);
79
+ const event = RelayMissionEventInputSchema.parse(input.event);
80
+ const approvalPrincipal = authenticatedPrincipal?.trim() || undefined;
81
+ if (event.payload.kind === "correction_approved" && event.payload.approvedBy.kind === "human") {
82
+ if (!approvalPrincipal) {
83
+ throw new EngramAccessInputError(
84
+ "Relay human approval requires a server-resolved authenticated principal"
85
+ );
86
+ }
87
+ if (event.payload.approvedBy.id !== approvalPrincipal) {
88
+ throw new EngramAccessInputError(
89
+ "Relay human approval identity must match the server-resolved authenticated principal"
90
+ );
91
+ }
92
+ }
93
+ const resolved = await service.getWritableStorageForNamespace(input.namespace ?? undefined, approvalPrincipal);
80
94
  const store = new RelayMissionStore({
81
95
  rootDir: resolved.storage.dir,
82
96
  namespace: resolved.namespace,
83
97
  });
84
- return store.append(input.missionId, input.event, { authenticatedPrincipal, beforeAppend });
98
+ return store.append(input.missionId, event, { authenticatedPrincipal: approvalPrincipal, beforeAppend });
85
99
  }
86
100
 
87
101
  /** Namespace-authorized, bounded Relay snapshot read shared by HTTP and UI. */
@@ -0,0 +1,90 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ import { EngramAccessHttpServer } from "../access-http.js";
9
+ import type { EngramAccessService } from "../access-service.js";
10
+ import type { StorageManager } from "../storage.js";
11
+
12
+ const adminConsolePublicDir = path.resolve(
13
+ path.dirname(fileURLToPath(import.meta.url)),
14
+ "../../../../admin-console/public"
15
+ );
16
+
17
+ async function withTempRoot(run: (root: string) => Promise<void>): Promise<void> {
18
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-relay-ui-"));
19
+ try {
20
+ await run(root);
21
+ } finally {
22
+ await rm(root, { recursive: true, force: true });
23
+ }
24
+ }
25
+
26
+ function fakeService(root: string): EngramAccessService {
27
+ const storage = { dir: root } as StorageManager;
28
+ return {
29
+ configRef: { defaultNamespace: "relay-build-week", namespacesEnabled: true },
30
+ getReadableStorageForNamespace: async (namespace?: string) => ({ namespace: namespace ?? "relay-build-week", storage }),
31
+ getWritableStorageForNamespace: async (namespace?: string) => ({ namespace: namespace ?? "relay-build-week", storage }),
32
+ } as unknown as EngramAccessService;
33
+ }
34
+
35
+ test("Relay Mission Control serves both aliases from one exact static allow-list", async () => {
36
+ await withTempRoot(async (root) => {
37
+ const server = new EngramAccessHttpServer({
38
+ service: fakeService(root),
39
+ host: "127.0.0.1",
40
+ port: 0,
41
+ authToken: "relay-token",
42
+ principal: "relay-operator",
43
+ adminConsoleEnabled: true,
44
+ adminConsolePublicDir,
45
+ adminConsolePrefillToken: true,
46
+ });
47
+ const status = await server.start();
48
+ const origin = `http://127.0.0.1:${status.port}`;
49
+ try {
50
+ for (const prefix of ["remnic", "engram"]) {
51
+ const redirect = await fetch(`${origin}/${prefix}/ui/relay`, { redirect: "manual" });
52
+ assert.equal(redirect.status, 301);
53
+ assert.equal(redirect.headers.get("location"), `/${prefix}/ui/relay/`);
54
+
55
+ const shell = await fetch(`${origin}/${prefix}/ui/relay/`);
56
+ assert.equal(shell.status, 200);
57
+ assert.match(shell.headers.get("content-type") ?? "", /^text\/html/);
58
+ assert.doesNotMatch(await shell.text(), /__REMNIC_ADMIN_CONSOLE_PREFILL_TOKEN__/);
59
+
60
+ const authorizedShell = await fetch(`${origin}/${prefix}/ui/relay/`, {
61
+ headers: { authorization: "Bearer relay-token" },
62
+ });
63
+ assert.equal(authorizedShell.status, 200);
64
+ assert.equal(authorizedShell.headers.get("cache-control"), "private, no-store");
65
+ assert.match(await authorizedShell.text(), /__REMNIC_ADMIN_CONSOLE_PREFILL_TOKEN__="relay-token"/);
66
+
67
+ const expectedAssets = new Map([
68
+ ["relay.css", /^text\/css/],
69
+ ["relay-model.js", /^application\/javascript/],
70
+ ["relay.js", /^application\/javascript/],
71
+ ["replay.json", /^application\/json/],
72
+ ]);
73
+ for (const [fileName, contentType] of expectedAssets) {
74
+ const asset = await fetch(`${origin}/${prefix}/ui/relay/${fileName}`);
75
+ assert.equal(asset.status, 200, `${prefix}/${fileName}`);
76
+ assert.match(asset.headers.get("content-type") ?? "", contentType);
77
+ assert.ok((await asset.text()).length > 100);
78
+ }
79
+
80
+ const auth = { authorization: "Bearer relay-token" };
81
+ const unknown = await fetch(`${origin}/${prefix}/ui/relay/not-allowed.js`, { headers: auth });
82
+ assert.equal(unknown.status, 404);
83
+ const nested = await fetch(`${origin}/${prefix}/ui/relay/relay.js/extra`, { headers: auth });
84
+ assert.equal(nested.status, 404);
85
+ }
86
+ } finally {
87
+ await server.stop();
88
+ }
89
+ });
90
+ });
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Issue #1903 — state-file writes must not touch the namespace catalog.
3
+ *
4
+ * The post-write catalog hook (`onCatalogWrite`) fires on every successful
5
+ * secure-file write. Pure state files (`state/buffer.json`, ledgers, indexes)
6
+ * are NOT namespace memory data, so by default their writes must be excluded
7
+ * from the catalog touch path (they were the dominant source of catalog churn
8
+ * that grew `state/namespaces.jsonl` unbounded). Namespace data writes
9
+ * (`facts/`, ...) still touch. The `touchStateWrites` opt-in restores the
10
+ * pre-#1903 behavior where every write touched.
11
+ */
12
+
13
+ import assert from "node:assert/strict";
14
+ import { mkdtemp, rm } from "node:fs/promises";
15
+ import os from "node:os";
16
+ import path from "node:path";
17
+ import test from "node:test";
18
+
19
+ import { StorageManager } from "./storage.js";
20
+ import type { BufferState } from "./types.js";
21
+
22
+ async function mkDir(): Promise<string> {
23
+ return await mkdtemp(path.join(os.tmpdir(), "remnic-touch-gate-"));
24
+ }
25
+
26
+ function emptyBuffer(): BufferState {
27
+ return { turns: [], lastExtractionAt: null, extractionCount: 0 };
28
+ }
29
+
30
+ test("#1903 state-file writes do not touch the catalog by default; data writes do", async () => {
31
+ const dir = await mkDir();
32
+ try {
33
+ const storage = new StorageManager(dir);
34
+ await storage.ensureDirectories();
35
+ let touches = 0;
36
+ storage.onCatalogWrite = () => {
37
+ touches += 1;
38
+ };
39
+
40
+ // A pure state-file write (state/buffer.json) must NOT touch the catalog.
41
+ await storage.saveBuffer(emptyBuffer());
42
+ assert.equal(touches, 0, "saving buffer.json (a state file) records no catalog touch");
43
+
44
+ // A namespace-data write (facts/) MUST touch exactly once. The lifecycle
45
+ // ledger append it also performs is a state write and stays excluded.
46
+ touches = 0;
47
+ await storage.writeMemory("fact", "The database is PostgreSQL.", { confidence: 0.9 });
48
+ assert.equal(touches, 1, "writing a fact records exactly one catalog touch");
49
+ } finally {
50
+ await rm(dir, { recursive: true, force: true });
51
+ }
52
+ });
53
+
54
+ test("#1903 touchStateWrites=true restores the state-file catalog touch", async () => {
55
+ const dir = await mkDir();
56
+ try {
57
+ const storage = new StorageManager(dir);
58
+ await storage.ensureDirectories();
59
+ storage.touchStateWrites = true;
60
+ let touches = 0;
61
+ storage.onCatalogWrite = () => {
62
+ touches += 1;
63
+ };
64
+
65
+ await storage.saveBuffer(emptyBuffer());
66
+ assert.ok(touches >= 1, "with touchStateWrites enabled, saving buffer.json touches the catalog");
67
+ } finally {
68
+ await rm(dir, { recursive: true, force: true });
69
+ }
70
+ });
package/src/storage.ts CHANGED
@@ -2447,6 +2447,32 @@ export class StorageManager {
2447
2447
  }
2448
2448
  }
2449
2449
 
2450
+ /**
2451
+ * Whether pure state-file writes touch the namespace catalog (issue #1903).
2452
+ * Set by `NamespaceStorageRouter.bindCatalogWriteHook` from
2453
+ * `namespacesCatalogTouchStateWrites` (default false). State files
2454
+ * (`state/buffer.json`, ledgers, indexes) are not namespace memory data, so by
2455
+ * default their writes do NOT record a catalog touch.
2456
+ */
2457
+ touchStateWrites = false;
2458
+
2459
+ /**
2460
+ * Post-write catalog hook, gated by file path (issue #1903). Namespace data
2461
+ * (`facts/`, `cold/`, `entities/`, `artifacts/`, `profile.md`, ...) lives
2462
+ * OUTSIDE `stateDir`, so a data write always touches; a write under `stateDir`
2463
+ * is a pure state write and is skipped unless `touchStateWrites` is enabled.
2464
+ */
2465
+ private notifyCatalogWriteForPath(filePath: string): void {
2466
+ if (!this.touchStateWrites && this.isStateFilePath(filePath)) return;
2467
+ this.notifyCatalogWrite();
2468
+ }
2469
+
2470
+ /** Whether `filePath` resolves to `stateDir` itself or a path contained in it. */
2471
+ private isStateFilePath(filePath: string): boolean {
2472
+ const rel = path.relative(this.stateDir, path.resolve(filePath));
2473
+ return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
2474
+ }
2475
+
2450
2476
  // ── Tombstone store (issue #1579) ───────────────────────────────────────
2451
2477
  // Instance-scoped (rule 11) + namespace-scoped (rule 42) tombstone index.
2452
2478
  // Lazily loaded from <stateDir>/tombstones.jsonl; invalidated together with
@@ -3083,12 +3109,12 @@ export class StorageManager {
3083
3109
  }
3084
3110
  private writeStorageSecureFile(filePath: string, content: string | Buffer): Promise<void> {
3085
3111
  return writeMaybeEncryptedFile(filePath, content, this.resolveWriteKey(), {}, this.baseDir).then(() =>
3086
- this.notifyCatalogWrite()
3112
+ this.notifyCatalogWriteForPath(filePath)
3087
3113
  );
3088
3114
  }
3089
3115
  private writeStorageSecureFileChunks(filePath: string, chunks: AsyncIterable<Buffer>): Promise<void> {
3090
3116
  return writeMaybeEncryptedFileFromChunks(filePath, chunks, this.resolveWriteKey(), {}, this.baseDir).then(() =>
3091
- this.notifyCatalogWrite()
3117
+ this.notifyCatalogWriteForPath(filePath)
3092
3118
  );
3093
3119
  }
3094
3120
 
@@ -3340,14 +3366,14 @@ export class StorageManager {
3340
3366
  if (isEncryptedFile(await readFile(filePath))) {
3341
3367
  const existing = await this.readStorageSecureFile(filePath);
3342
3368
  await writeMaybeEncryptedFile(filePath, `${existing}${content}`, null, {}, this.baseDir);
3343
- this.notifyCatalogWrite();
3369
+ this.notifyCatalogWriteForPath(filePath);
3344
3370
  return;
3345
3371
  }
3346
3372
  } catch (err) {
3347
3373
  if (!isErrnoCode(err, "ENOENT")) throw err;
3348
3374
  }
3349
3375
  await appendFile(filePath, content, "utf-8");
3350
- this.notifyCatalogWrite();
3376
+ this.notifyCatalogWriteForPath(filePath);
3351
3377
  return;
3352
3378
  }
3353
3379
 
@@ -3358,7 +3384,7 @@ export class StorageManager {
3358
3384
  if (!isErrnoCode(err, "ENOENT")) throw err;
3359
3385
  }
3360
3386
  await writeMaybeEncryptedFile(filePath, `${existing}${content}`, writeKey, {}, this.baseDir);
3361
- this.notifyCatalogWrite();
3387
+ this.notifyCatalogWriteForPath(filePath);
3362
3388
  }
3363
3389
  private get stateDir(): string {
3364
3390
  return path.join(this.baseDir, "state");
package/src/types.ts CHANGED
@@ -1690,6 +1690,40 @@ export interface PluginConfig {
1690
1690
  * existing single-namespace behavior is unchanged. Default on.
1691
1691
  */
1692
1692
  namespaceCatalogEnabled: boolean;
1693
+ /**
1694
+ * Size (bytes) at which a namespace-catalog touch triggers an in-lock
1695
+ * auto-compaction of `state/namespaces.jsonl` (issue #1903). The append-only
1696
+ * log grows one line per touch; once it exceeds this many bytes the next
1697
+ * touch folds it (last-record-wins, every namespace row preserved) and
1698
+ * atomically rewrites it. Default `16 * 1024 * 1024` (16 MB). `0` disables
1699
+ * auto-compaction (restores the pre-#1903 unbounded-append behavior).
1700
+ */
1701
+ namespacesCatalogCompactBytes: number;
1702
+ /**
1703
+ * Coalescing window (ms) for pure-timestamp READ touches on an
1704
+ * already-known namespace record (issue #1903). Repeated `markRead` touches
1705
+ * that only refresh `lastReadAt` are buffered per (namespace, kind) and
1706
+ * flushed once per window instead of appending a line each time.
1707
+ * Semantically-observable touches (first sight, provenance/field changes)
1708
+ * always flush immediately. Default `60000`. `0` disables read coalescing
1709
+ * (each touch appends immediately, the pre-#1903 behavior).
1710
+ */
1711
+ namespacesCatalogReadTouchCoalesceMs: number;
1712
+ /**
1713
+ * Coalescing window (ms) for pure-timestamp WRITE touches on an
1714
+ * already-known namespace record (issue #1903). Mirrors
1715
+ * `namespacesCatalogReadTouchCoalesceMs` for `markWrite`/`lastWriteAt`.
1716
+ * Default `1000`. `0` disables write coalescing.
1717
+ */
1718
+ namespacesCatalogWriteTouchCoalesceMs: number;
1719
+ /**
1720
+ * Whether pure state-file writes (`state/buffer.json`, ledgers, indexes)
1721
+ * touch the namespace catalog (issue #1903). State files are not namespace
1722
+ * memory data, so by default their writes do NOT record a catalog touch.
1723
+ * Set `true` to restore the pre-#1903 behavior where every secure-file
1724
+ * write touched the catalog. Default `false`.
1725
+ */
1726
+ namespacesCatalogTouchStateWrites: boolean;
1693
1727
  defaultNamespace: string;
1694
1728
  sharedNamespace: string;
1695
1729
  principalFromSessionKeyMode: PrincipalFromSessionKeyMode;