@remnic/core 9.15.3 → 9.16.1

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 (314) hide show
  1. package/dist/access-admin-ops-surface.d.ts +8 -8
  2. package/dist/access-admin-ops-surface.js +7 -7
  3. package/dist/access-boundary.d.ts +8 -8
  4. package/dist/access-boundary.js +7 -7
  5. package/dist/access-cli.js +31 -30
  6. package/dist/access-cli.js.map +1 -1
  7. package/dist/access-http.d.ts +8 -8
  8. package/dist/access-http.js +11 -11
  9. package/dist/access-identity-continuity-surface.d.ts +6 -6
  10. package/dist/access-identity-continuity-surface.js +7 -7
  11. package/dist/access-lcm-surface.d.ts +8 -8
  12. package/dist/access-lcm-surface.js +7 -7
  13. package/dist/access-mcp.d.ts +8 -8
  14. package/dist/access-mcp.js +10 -10
  15. package/dist/access-memory-search-fanout.d.ts +2 -2
  16. package/dist/{access-namespace-preflight-B9e5GnyZ.d.ts → access-namespace-preflight-DwhEFYy3.d.ts} +1 -1
  17. package/dist/access-namespace-preflight.d.ts +3 -3
  18. package/dist/access-namespace-preflight.js +7 -7
  19. package/dist/access-observe-write-surface.d.ts +8 -8
  20. package/dist/access-observe-write-surface.js +7 -7
  21. package/dist/access-operations-batch.js +8 -8
  22. package/dist/access-operations.d.ts +11 -11
  23. package/dist/access-operations.js +9 -9
  24. package/dist/access-recall-concurrency.d.ts +8 -8
  25. package/dist/access-recall-concurrency.js +7 -7
  26. package/dist/access-recall-response.d.ts +8 -8
  27. package/dist/access-recall-response.js +7 -7
  28. package/dist/access-recall-surface.d.ts +8 -8
  29. package/dist/access-recall-surface.js +7 -7
  30. package/dist/access-schema.d.ts +76 -76
  31. package/dist/{access-service-DgmJd_A-.d.ts → access-service-CSq1Wp2Z.d.ts} +6 -6
  32. package/dist/access-service.d.ts +8 -8
  33. package/dist/access-service.js +7 -7
  34. package/dist/access-surface-catalog.d.ts +8 -8
  35. package/dist/action-confidence.d.ts +1 -1
  36. package/dist/active-memory-bridge.d.ts +1 -1
  37. package/dist/active-recall.d.ts +1 -1
  38. package/dist/active-recall.js +2 -1
  39. package/dist/active-recall.js.map +1 -1
  40. package/dist/behavior-learner.d.ts +1 -1
  41. package/dist/behavior-signals.d.ts +1 -1
  42. package/dist/bootstrap.d.ts +6 -6
  43. package/dist/briefing.d.ts +2 -2
  44. package/dist/briefing.js +3 -3
  45. package/dist/buffer-surprise-report.d.ts +1 -1
  46. package/dist/buffer.d.ts +2 -2
  47. package/dist/bulk-import/index.d.ts +3 -3
  48. package/dist/calibration.d.ts +1 -1
  49. package/dist/capabilities.d.ts +1 -1
  50. package/dist/{catalog-g_dfT6oL.d.ts → catalog-B9uga4sG.d.ts} +1 -1
  51. package/dist/causal-behavior.d.ts +1 -1
  52. package/dist/causal-consolidation.d.ts +1 -1
  53. package/dist/causal-consolidation.js +4 -4
  54. package/dist/causal-trajectory-graph.d.ts +1 -1
  55. package/dist/{chunk-IRX5UCGN.js → chunk-2OH3ZRYH.js} +3 -3
  56. package/dist/{chunk-YXTSUIXC.js → chunk-5EKOBRCA.js} +6 -1
  57. package/dist/chunk-5EKOBRCA.js.map +1 -0
  58. package/dist/{chunk-42DJZZUB.js → chunk-7LXSPIVG.js} +2 -2
  59. package/dist/{chunk-47WFEKT7.js → chunk-7PK4W4RN.js} +2 -2
  60. package/dist/{chunk-O7ACJAHR.js → chunk-7YJLCNPF.js} +2 -2
  61. package/dist/{chunk-6EJL2O3G.js → chunk-AFH7PIFU.js} +2 -2
  62. package/dist/{chunk-JN4N2TMQ.js → chunk-ANGUOE2N.js} +7 -7
  63. package/dist/{chunk-GQOKO77V.js → chunk-AWT7SCWQ.js} +2 -2
  64. package/dist/{chunk-PXJEL55X.js → chunk-BIRPR5N7.js} +2 -2
  65. package/dist/{chunk-NAY25BGS.js → chunk-BTK2PIFL.js} +6 -6
  66. package/dist/{chunk-SYCVU556.js → chunk-C2RMQRPN.js} +3 -3
  67. package/dist/{chunk-T4EAQPQQ.js → chunk-G2WCVFDC.js} +2 -2
  68. package/dist/{chunk-UJIBXOJA.js → chunk-GEDBTSYU.js} +5 -5
  69. package/dist/{chunk-CXFVZ3PK.js → chunk-GRJ5UOFO.js} +84 -658
  70. package/dist/chunk-GRJ5UOFO.js.map +1 -0
  71. package/dist/chunk-GSQASASV.js +429 -0
  72. package/dist/chunk-GSQASASV.js.map +1 -0
  73. package/dist/{chunk-WIKOHCQZ.js → chunk-I6ODVWQB.js} +2 -2
  74. package/dist/{chunk-H4MDUPER.js → chunk-IZHXSLO7.js} +2 -2
  75. package/dist/{chunk-RRS5CDB6.js → chunk-JOCXI2O5.js} +2 -2
  76. package/dist/{chunk-EEUZQQRJ.js → chunk-KGKYTCDO.js} +2 -2
  77. package/dist/{chunk-JTW4TGPV.js → chunk-L3HAUO7K.js} +2 -2
  78. package/dist/{chunk-P53HGVJS.js → chunk-LMVAIZR5.js} +2 -2
  79. package/dist/{chunk-KLXG4BBO.js → chunk-M6TZQDTX.js} +2 -2
  80. package/dist/{chunk-FJ5IQ3SI.js → chunk-N7DK67GZ.js} +1 -1
  81. package/dist/chunk-N7DK67GZ.js.map +1 -0
  82. package/dist/{chunk-U6QWUFSM.js → chunk-OXT5TQDS.js} +2 -2
  83. package/dist/{chunk-T4OTDMX4.js → chunk-QDTEH6BS.js} +2 -2
  84. package/dist/{chunk-5MDE5MIC.js → chunk-SESF64QR.js} +2 -2
  85. package/dist/{chunk-4E5H4FPC.js → chunk-TUNI4GB3.js} +3 -3
  86. package/dist/{chunk-XJT6LNLY.js → chunk-V6LYAXGW.js} +834 -25
  87. package/dist/chunk-V6LYAXGW.js.map +1 -0
  88. package/dist/{chunk-5HFIHWOF.js → chunk-VLIY55Q5.js} +17 -17
  89. package/dist/{chunk-BNH2VVWP.js → chunk-WIS5JTJR.js} +2 -2
  90. package/dist/{chunk-OOGGUXAG.js → chunk-WQ2I6ODB.js} +2 -2
  91. package/dist/{cli-BXCd2cgK.d.ts → cli-DYLGXxuu.d.ts} +5 -5
  92. package/dist/cli.d.ts +10 -10
  93. package/dist/cli.js +25 -24
  94. package/dist/compounding/engine.d.ts +2 -2
  95. package/dist/compounding/engine.js +3 -3
  96. package/dist/compounding/preference-consolidator.d.ts +1 -1
  97. package/dist/compression-optimizer.d.ts +1 -1
  98. package/dist/config.d.ts +1 -1
  99. package/dist/config.js +2 -1
  100. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  101. package/dist/connectors/codex-materialize-runner.js +3 -3
  102. package/dist/connectors/codex-materialize.d.ts +1 -1
  103. package/dist/connectors/index.d.ts +1 -1
  104. package/dist/connectors/index.js +3 -3
  105. package/dist/consolidation-provenance-check.d.ts +2 -2
  106. package/dist/consolidation-undo.d.ts +2 -2
  107. package/dist/contradiction/index.d.ts +2 -2
  108. package/dist/conversation-index/backend.d.ts +1 -1
  109. package/dist/conversation-index/chunker.d.ts +1 -1
  110. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  111. package/dist/conversation-index/indexer.d.ts +1 -1
  112. package/dist/conversation-index/search.d.ts +1 -1
  113. package/dist/day-summary.d.ts +1 -1
  114. package/dist/delinearize.d.ts +1 -1
  115. package/dist/direct-answer-wiring.d.ts +1 -1
  116. package/dist/direct-answer.d.ts +1 -1
  117. package/dist/embedding-fallback.d.ts +1 -1
  118. package/dist/enrichment/index.d.ts +1 -1
  119. package/dist/entity-retrieval.d.ts +2 -2
  120. package/dist/entity-retrieval.js +3 -3
  121. package/dist/entity-schema.d.ts +1 -1
  122. package/dist/explicit-capture.d.ts +6 -6
  123. package/dist/extraction-faithfulness.d.ts +1 -1
  124. package/dist/extraction-judge-telemetry.d.ts +1 -1
  125. package/dist/extraction-judge-training.d.ts +1 -1
  126. package/dist/extraction-judge.d.ts +1 -1
  127. package/dist/extraction.d.ts +1 -1
  128. package/dist/fallback-llm.d.ts +1 -1
  129. package/dist/graph-dashboard-diff.d.ts +1 -1
  130. package/dist/graph-dashboard-key.d.ts +1 -1
  131. package/dist/graph-dashboard-parser.d.ts +1 -1
  132. package/dist/graph-edge-reinforcement.d.ts +1 -1
  133. package/dist/graph-snapshot.d.ts +1 -1
  134. package/dist/graph.d.ts +1 -1
  135. package/dist/importance.d.ts +1 -1
  136. package/dist/importers/index.d.ts +1 -1
  137. package/dist/in-flight-reads.d.ts +1 -1
  138. package/dist/index.d.ts +693 -503
  139. package/dist/index.js +70 -45
  140. package/dist/intent.d.ts +1 -1
  141. package/dist/lcm/engine.d.ts +1 -1
  142. package/dist/lcm/index.d.ts +1 -1
  143. package/dist/lcm/tools.d.ts +1 -1
  144. package/dist/lifecycle.d.ts +1 -1
  145. package/dist/live-connectors-runner.d.ts +1 -1
  146. package/dist/local-llm.d.ts +1 -1
  147. package/dist/local-model-endpoint.d.ts +1 -1
  148. package/dist/maintenance/memory-governance.d.ts +1 -1
  149. package/dist/maintenance/memory-governance.js +3 -3
  150. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +2 -2
  151. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  152. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  153. package/dist/mcp-memory-inspector-app.d.ts +8 -8
  154. package/dist/memory-action-policy.d.ts +1 -1
  155. package/dist/memory-cache.d.ts +1 -1
  156. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  157. package/dist/memory-projection-store.d.ts +1 -1
  158. package/dist/memory-provenance.d.ts +1 -1
  159. package/dist/memory-worth-outcomes.d.ts +2 -2
  160. package/dist/models-json.d.ts +1 -1
  161. package/dist/namespaces/migrate.d.ts +3 -3
  162. package/dist/namespaces/migrate.js +4 -4
  163. package/dist/namespaces/principal.d.ts +1 -1
  164. package/dist/namespaces/search.d.ts +1 -1
  165. package/dist/namespaces/storage.d.ts +3 -3
  166. package/dist/namespaces/storage.js +3 -3
  167. package/dist/native-knowledge.d.ts +1 -1
  168. package/dist/offline-sync-impression-drain.d.ts +1 -1
  169. package/dist/operator-toolkit.d.ts +2 -2
  170. package/dist/operator-toolkit.js +9 -8
  171. package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
  172. package/dist/orchestration/maintenance.d.ts +10 -7
  173. package/dist/orchestration/maintenance.js +7 -6
  174. package/dist/{orchestrator-BHfWtvEJ.d.ts → orchestrator-BHx-DWCa.d.ts} +7 -5
  175. package/dist/orchestrator.d.ts +6 -6
  176. package/dist/orchestrator.js +31 -30
  177. package/dist/patterns-cli.d.ts +1 -1
  178. package/dist/{pipeline-CBYcnmDC.d.ts → pipeline-DDHSE876.d.ts} +1 -1
  179. package/dist/policy-runtime.d.ts +1 -1
  180. package/dist/proactive-contention.d.ts +1 -1
  181. package/dist/provenance.d.ts +1 -1
  182. package/dist/{qmd-BIq-jrV4.d.ts → qmd-C4WVh6BM.d.ts} +1 -1
  183. package/dist/qmd-preflight.d.ts +2 -2
  184. package/dist/qmd-recall-cache.d.ts +1 -1
  185. package/dist/qmd.d.ts +2 -2
  186. package/dist/recall-concurrency-config.d.ts +1 -1
  187. package/dist/recall-disclosure-escalation.d.ts +1 -1
  188. package/dist/recall-explain-renderer.d.ts +1 -1
  189. package/dist/recall-explain-renderer.js +3 -3
  190. package/dist/recall-memory-map.d.ts +1 -1
  191. package/dist/recall-planner-llm.d.ts +1 -1
  192. package/dist/recall-state.d.ts +1 -1
  193. package/dist/recall-tag-filter.d.ts +1 -1
  194. package/dist/recall-timings.d.ts +1 -1
  195. package/dist/recall-xray-cli.d.ts +1 -1
  196. package/dist/recall-xray-cli.js +4 -4
  197. package/dist/recall-xray-renderer.d.ts +1 -1
  198. package/dist/recall-xray-renderer.js +3 -3
  199. package/dist/recall-xray.d.ts +1 -1
  200. package/dist/recall-xray.js +2 -2
  201. package/dist/replay/normalizers/chatgpt.d.ts +1 -1
  202. package/dist/replay/normalizers/claude.d.ts +1 -1
  203. package/dist/replay/normalizers/openclaw.d.ts +1 -1
  204. package/dist/replay/normalizers/shared.d.ts +1 -1
  205. package/dist/replay/runner.d.ts +1 -1
  206. package/dist/replay/types.d.ts +1 -1
  207. package/dist/resolve-auth-token.d.ts +1 -1
  208. package/dist/resume-bundles.js +3 -2
  209. package/dist/retrieval-agents.d.ts +2 -2
  210. package/dist/retrieval-tiers.d.ts +1 -1
  211. package/dist/routing/engine.d.ts +1 -1
  212. package/dist/routing/store.d.ts +1 -1
  213. package/dist/salvage-envelope.d.ts +1 -1
  214. package/dist/schemas.d.ts +76 -76
  215. package/dist/{scope-profiles-DDno-uvy.d.ts → scope-profiles-EqMXwzCz.d.ts} +1 -1
  216. package/dist/search/embed-helper.d.ts +1 -1
  217. package/dist/search/factory.d.ts +1 -1
  218. package/dist/search/index.d.ts +1 -1
  219. package/dist/search/lancedb-backend.d.ts +1 -1
  220. package/dist/search/meilisearch-backend.d.ts +1 -1
  221. package/dist/search/noop-backend.d.ts +1 -1
  222. package/dist/search/orama-backend.d.ts +1 -1
  223. package/dist/search/port.d.ts +1 -1
  224. package/dist/search/remote-backend.d.ts +1 -1
  225. package/dist/{semantic-consolidation-7RIJFDwY.d.ts → semantic-consolidation-CYpvwmQc.d.ts} +1 -1
  226. package/dist/semantic-consolidation.d.ts +2 -2
  227. package/dist/semantic-consolidation.js +4 -4
  228. package/dist/semantic-rule-promotion.js +3 -3
  229. package/dist/semantic-rule-verifier.d.ts +1 -1
  230. package/dist/semantic-rule-verifier.js +3 -3
  231. package/dist/session-observer-bands.d.ts +1 -1
  232. package/dist/session-observer-state.d.ts +1 -1
  233. package/dist/shared-context/manager.d.ts +9 -9
  234. package/dist/signal.d.ts +1 -1
  235. package/dist/{storage-DkphZiOJ.d.ts → storage-Dxd7zVZH.d.ts} +1 -1
  236. package/dist/storage.d.ts +2 -2
  237. package/dist/storage.js +2 -2
  238. package/dist/summarizer.d.ts +1 -1
  239. package/dist/summary-snapshot.d.ts +1 -1
  240. package/dist/temporal-supersession.d.ts +2 -2
  241. package/dist/temporal-timeline-recall.d.ts +1 -1
  242. package/dist/temporal-validity.d.ts +1 -1
  243. package/dist/threading.d.ts +1 -1
  244. package/dist/tier-migration.d.ts +2 -2
  245. package/dist/tier-routing.d.ts +1 -1
  246. package/dist/topics.d.ts +1 -1
  247. package/dist/transcript.d.ts +1 -1
  248. package/dist/transfer/types.d.ts +66 -66
  249. package/dist/trust-score-stage.d.ts +1 -1
  250. package/dist/trust-score.d.ts +1 -1
  251. package/dist/{types-2q_MRFcM.d.ts → types-OUCJALBK.d.ts} +98 -1
  252. package/dist/types.d.ts +1 -1
  253. package/dist/types.js +1 -1
  254. package/dist/utility-runtime.d.ts +1 -1
  255. package/dist/verified-recall.js +3 -3
  256. package/dist/write-envelope.d.ts +1 -1
  257. package/package.json +2 -2
  258. package/src/activity/config.test.ts +113 -0
  259. package/src/activity/config.ts +111 -0
  260. package/src/activity/digest.ts +2 -2
  261. package/src/activity/index.ts +6 -0
  262. package/src/activity/pipeline.test.ts +379 -0
  263. package/src/activity/pipeline.ts +223 -0
  264. package/src/activity/reindex.test.ts +97 -0
  265. package/src/activity/reindex.ts +40 -0
  266. package/src/activity/runner.test.ts +452 -0
  267. package/src/activity/runner.ts +237 -0
  268. package/src/activity/scheduler.test.ts +303 -0
  269. package/src/activity/scheduler.ts +147 -0
  270. package/src/activity/source-client.test.ts +208 -0
  271. package/src/activity/source-client.ts +159 -0
  272. package/src/activity/sync-registration.test.ts +134 -0
  273. package/src/activity/sync-registration.ts +139 -0
  274. package/src/activity/types.ts +17 -0
  275. package/src/config.test.ts +19 -0
  276. package/src/config.ts +3 -0
  277. package/src/orchestration/maintenance.test.ts +42 -42
  278. package/src/orchestration/maintenance.ts +37 -5
  279. package/src/orchestration/orchestrator-helpers.ts +36 -1
  280. package/src/orchestration/recall-internal.ts +6 -6
  281. package/src/orchestration/recall-search-pipeline.ts +15 -7
  282. package/src/orchestrator-background-writes.test.ts +2 -2
  283. package/src/orchestrator.ts +3 -3
  284. package/src/types.ts +4 -1
  285. package/dist/chunk-CXFVZ3PK.js.map +0 -1
  286. package/dist/chunk-FJ5IQ3SI.js.map +0 -1
  287. package/dist/chunk-XJT6LNLY.js.map +0 -1
  288. package/dist/chunk-YXTSUIXC.js.map +0 -1
  289. /package/dist/{chunk-IRX5UCGN.js.map → chunk-2OH3ZRYH.js.map} +0 -0
  290. /package/dist/{chunk-42DJZZUB.js.map → chunk-7LXSPIVG.js.map} +0 -0
  291. /package/dist/{chunk-47WFEKT7.js.map → chunk-7PK4W4RN.js.map} +0 -0
  292. /package/dist/{chunk-O7ACJAHR.js.map → chunk-7YJLCNPF.js.map} +0 -0
  293. /package/dist/{chunk-6EJL2O3G.js.map → chunk-AFH7PIFU.js.map} +0 -0
  294. /package/dist/{chunk-JN4N2TMQ.js.map → chunk-ANGUOE2N.js.map} +0 -0
  295. /package/dist/{chunk-GQOKO77V.js.map → chunk-AWT7SCWQ.js.map} +0 -0
  296. /package/dist/{chunk-PXJEL55X.js.map → chunk-BIRPR5N7.js.map} +0 -0
  297. /package/dist/{chunk-NAY25BGS.js.map → chunk-BTK2PIFL.js.map} +0 -0
  298. /package/dist/{chunk-SYCVU556.js.map → chunk-C2RMQRPN.js.map} +0 -0
  299. /package/dist/{chunk-T4EAQPQQ.js.map → chunk-G2WCVFDC.js.map} +0 -0
  300. /package/dist/{chunk-UJIBXOJA.js.map → chunk-GEDBTSYU.js.map} +0 -0
  301. /package/dist/{chunk-WIKOHCQZ.js.map → chunk-I6ODVWQB.js.map} +0 -0
  302. /package/dist/{chunk-H4MDUPER.js.map → chunk-IZHXSLO7.js.map} +0 -0
  303. /package/dist/{chunk-RRS5CDB6.js.map → chunk-JOCXI2O5.js.map} +0 -0
  304. /package/dist/{chunk-EEUZQQRJ.js.map → chunk-KGKYTCDO.js.map} +0 -0
  305. /package/dist/{chunk-JTW4TGPV.js.map → chunk-L3HAUO7K.js.map} +0 -0
  306. /package/dist/{chunk-P53HGVJS.js.map → chunk-LMVAIZR5.js.map} +0 -0
  307. /package/dist/{chunk-KLXG4BBO.js.map → chunk-M6TZQDTX.js.map} +0 -0
  308. /package/dist/{chunk-U6QWUFSM.js.map → chunk-OXT5TQDS.js.map} +0 -0
  309. /package/dist/{chunk-T4OTDMX4.js.map → chunk-QDTEH6BS.js.map} +0 -0
  310. /package/dist/{chunk-5MDE5MIC.js.map → chunk-SESF64QR.js.map} +0 -0
  311. /package/dist/{chunk-4E5H4FPC.js.map → chunk-TUNI4GB3.js.map} +0 -0
  312. /package/dist/{chunk-5HFIHWOF.js.map → chunk-VLIY55Q5.js.map} +0 -0
  313. /package/dist/{chunk-BNH2VVWP.js.map → chunk-WIS5JTJR.js.map} +0 -0
  314. /package/dist/{chunk-OOGGUXAG.js.map → chunk-WQ2I6ODB.js.map} +0 -0
@@ -1,4 +1,4 @@
1
- import { w as MemoryCategory } from './types-2q_MRFcM.js';
1
+ import { w as MemoryCategory } from './types-OUCJALBK.js';
2
2
  import './message-parts/index.js';
3
3
  import './bounded-jsonl-state.js';
4
4
  import './types-continuity.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.15.3",
3
+ "version": "9.16.1",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -3045,7 +3045,7 @@
3045
3045
  "core"
3046
3046
  ],
3047
3047
  "peerDependencies": {
3048
- "@remnic/coding-graph": "^9.15.3"
3048
+ "@remnic/coding-graph": "^9.16.1"
3049
3049
  },
3050
3050
  "peerDependenciesMeta": {
3051
3051
  "@remnic/coding-graph": {
@@ -0,0 +1,113 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { parseActivityConfig } from "./config.js";
5
+
6
+ test("parseActivityConfig defaults to an inert configuration", () => {
7
+ assert.deepEqual(parseActivityConfig(undefined), {
8
+ enabled: false,
9
+ timezone: "UTC",
10
+ syncDays: 1,
11
+ autoSyncIntervalMinutes: 15,
12
+ sources: [],
13
+ });
14
+ });
15
+
16
+ test("parseActivityConfig treats a string false gate as disabled", () => {
17
+ assert.equal(parseActivityConfig({ enabled: "false" }).enabled, false);
18
+ });
19
+
20
+ test("parseActivityConfig preserves explicit source settings", () => {
21
+ assert.deepEqual(
22
+ parseActivityConfig({
23
+ enabled: true,
24
+ timezone: "America/Chicago",
25
+ syncDays: 3,
26
+ sources: [{ machineLabel: "fixture-machine", baseUrl: "http://127.0.0.1:4319", token: "fixture-token" }],
27
+ }),
28
+ {
29
+ enabled: true,
30
+ timezone: "America/Chicago",
31
+ syncDays: 3,
32
+ autoSyncIntervalMinutes: 15,
33
+ sources: [{ machineLabel: "fixture-machine", baseUrl: "http://127.0.0.1:4319", token: "fixture-token" }],
34
+ },
35
+ );
36
+ });
37
+
38
+ test("parseActivityConfig rejects enabled configurations without valid source definitions", () => {
39
+ assert.throws(() => parseActivityConfig({ enabled: true, sources: [] }), /at least one source/);
40
+ assert.throws(
41
+ () => parseActivityConfig({ enabled: true, sources: [{ machineLabel: "fixture", baseUrl: "ftp://example.test" }] }),
42
+ /HTTP or HTTPS/,
43
+ );
44
+ assert.throws(() => parseActivityConfig({ syncDays: 0 }), /syncDays/);
45
+ });
46
+
47
+ test("parseActivityConfig enforces both ends of the syncDays range", () => {
48
+ assert.equal(parseActivityConfig({ syncDays: 90 }).syncDays, 90);
49
+ assert.equal(parseActivityConfig({ syncDays: 1 }).syncDays, 1);
50
+ assert.throws(() => parseActivityConfig({ syncDays: 91 }), /syncDays must be an integer from 1 to 90/);
51
+ assert.throws(() => parseActivityConfig({ syncDays: 0 }), /syncDays must be an integer from 1 to 90/);
52
+ });
53
+
54
+ test("parseActivityConfig rejects an invalid IANA timezone at parse time", () => {
55
+ assert.throws(() => parseActivityConfig({ timezone: "Not/AZone" }), /Invalid IANA timezone/);
56
+ });
57
+
58
+ test("parseActivityConfig rejects a whitespace-only machineLabel before any sync", () => {
59
+ assert.throws(
60
+ () => parseActivityConfig({ enabled: true, sources: [{ machineLabel: " ", baseUrl: "http://127.0.0.1:4319" }] }),
61
+ /machineLabel must not be blank/,
62
+ );
63
+ });
64
+
65
+ test("parseActivityConfig rejects duplicate machine labels that would share a cursor", () => {
66
+ assert.throws(
67
+ () =>
68
+ parseActivityConfig({
69
+ enabled: true,
70
+ sources: [
71
+ { machineLabel: "dup", baseUrl: "http://127.0.0.1:4319" },
72
+ { machineLabel: "dup", baseUrl: "http://127.0.0.1:4320" },
73
+ ],
74
+ }),
75
+ /machineLabel must be unique/,
76
+ );
77
+ });
78
+
79
+ test("parseActivityConfig reports a malformed baseUrl with a prefixed validation error", () => {
80
+ assert.throws(
81
+ () => parseActivityConfig({ enabled: true, sources: [{ machineLabel: "fixture", baseUrl: "not a url" }] }),
82
+ /activity source baseUrl must be a valid URL/,
83
+ );
84
+ });
85
+
86
+ test("parseActivityConfig rejects a non-loopback baseUrl that could exfiltrate the token", () => {
87
+ assert.throws(
88
+ () => parseActivityConfig({ enabled: true, sources: [{ machineLabel: "fixture", baseUrl: "https://example.test" }] }),
89
+ /must target a loopback host/,
90
+ );
91
+ });
92
+
93
+ test("parseActivityConfig accepts localhost and 127.x loopback hosts", () => {
94
+ assert.equal(
95
+ parseActivityConfig({ enabled: true, sources: [{ machineLabel: "a", baseUrl: "http://localhost:4319" }] }).sources
96
+ .length,
97
+ 1,
98
+ );
99
+ assert.equal(
100
+ parseActivityConfig({ enabled: true, sources: [{ machineLabel: "b", baseUrl: "http://127.0.0.5:4319" }] }).sources
101
+ .length,
102
+ 1,
103
+ );
104
+ });
105
+
106
+ test("parseActivityConfig parses and bounds autoSyncIntervalMinutes", () => {
107
+ assert.equal(parseActivityConfig(undefined).autoSyncIntervalMinutes, 15);
108
+ assert.equal(parseActivityConfig({ autoSyncIntervalMinutes: "30" }).autoSyncIntervalMinutes, 30);
109
+ assert.equal(parseActivityConfig({ autoSyncIntervalMinutes: 1 }).autoSyncIntervalMinutes, 1);
110
+ assert.equal(parseActivityConfig({ autoSyncIntervalMinutes: 1440 }).autoSyncIntervalMinutes, 1440);
111
+ assert.throws(() => parseActivityConfig({ autoSyncIntervalMinutes: 0 }), /autoSyncIntervalMinutes must be an integer from 1 to 1440/);
112
+ assert.throws(() => parseActivityConfig({ autoSyncIntervalMinutes: 1441 }), /autoSyncIntervalMinutes must be an integer from 1 to 1440/);
113
+ });
@@ -0,0 +1,111 @@
1
+ import { coerceBooleanLike, coerceNumber } from "../connectors/coerce.js";
2
+ import { assertValidTimezone } from "./digest.js";
3
+ import type { ActivityConfig, ActivitySourceConfig } from "./types.js";
4
+
5
+ function asRecord(value: unknown, name: string): Record<string, unknown> {
6
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
7
+ throw new TypeError(`${name} must be an object`);
8
+ }
9
+ return value as Record<string, unknown>;
10
+ }
11
+
12
+ function optionalString(value: unknown, name: string): string | undefined {
13
+ if (value === undefined) return undefined;
14
+ if (typeof value !== "string" || value.length === 0) throw new TypeError(`${name} must be a non-empty string`);
15
+ return value;
16
+ }
17
+
18
+ const LOOPBACK_HOSTS: Record<string, true> = { localhost: true, "127.0.0.1": true, "::1": true };
19
+
20
+ /**
21
+ * Parse, protocol-check, and confine an activity source base URL to a local
22
+ * loopback host. Shared with ActivityHttpSourceClient so config-load and client
23
+ * construction reject the exact same shapes with the same prefixed message (a
24
+ * bare `new URL()` throws an opaque TypeError on malformed input). The bearer
25
+ * token travels in an Authorization header, so a non-loopback baseUrl would
26
+ * exfiltrate it; the subsystem contract is local capture daemons only.
27
+ */
28
+ export function validateActivityBaseUrl(baseUrl: string): URL {
29
+ let parsed: URL;
30
+ try {
31
+ parsed = new URL(baseUrl);
32
+ } catch {
33
+ throw new RangeError(`activity source baseUrl must be a valid URL: ${baseUrl}`);
34
+ }
35
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
36
+ throw new RangeError("activity source baseUrl must use HTTP or HTTPS");
37
+ }
38
+ // URL keeps IPv6 hosts bracketed (e.g. "[::1]"); normalize before the lookup.
39
+ const host = parsed.hostname.replace(/^\[|\]$/g, "");
40
+ if (LOOPBACK_HOSTS[host] !== true && !/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host)) {
41
+ throw new RangeError(`activity source baseUrl must target a loopback host (got ${parsed.hostname})`);
42
+ }
43
+ return parsed;
44
+ }
45
+
46
+ function parseSource(value: unknown): ActivitySourceConfig {
47
+ const raw = asRecord(value, "activity source");
48
+ const machineLabel = optionalString(raw.machineLabel, "activity source machineLabel");
49
+ const baseUrl = optionalString(raw.baseUrl, "activity source baseUrl");
50
+ if (machineLabel === undefined || baseUrl === undefined) {
51
+ throw new TypeError("activity source requires machineLabel and baseUrl");
52
+ }
53
+ // Reject whitespace-only labels here so config-load fails loudly rather than
54
+ // parsing clean and then failing on the first sync (the client and pipeline
55
+ // both reject a blank label via trim()).
56
+ if (machineLabel.trim().length === 0) {
57
+ throw new RangeError("activity source machineLabel must not be blank");
58
+ }
59
+ validateActivityBaseUrl(baseUrl);
60
+ const token = optionalString(raw.token, "activity source token");
61
+ return { machineLabel, baseUrl, ...(token === undefined ? {} : { token }) };
62
+ }
63
+
64
+ export function parseActivityConfig(raw: unknown): ActivityConfig {
65
+ if (raw === undefined || raw === null) {
66
+ return { enabled: false, timezone: "UTC", syncDays: 1, autoSyncIntervalMinutes: 15, sources: [] };
67
+ }
68
+ const config = asRecord(raw, "activity");
69
+ const enabledValue = coerceBooleanLike(config.enabled, "activity.enabled");
70
+ if (config.enabled !== undefined && enabledValue === undefined) {
71
+ throw new TypeError("activity.enabled must be a boolean");
72
+ }
73
+ const timezone = optionalString(config.timezone, "activity.timezone") ?? "UTC";
74
+ // Validate the IANA zone at parse so a typo fails before any daemon is
75
+ // contacted; otherwise activityDayWindow() throws mid-sync after snapshot
76
+ // pages may already be persisted, and every retry replays the same window.
77
+ assertValidTimezone(timezone);
78
+ const syncDaysValue = coerceNumber(config.syncDays, "activity.syncDays");
79
+ if (config.syncDays !== undefined && syncDaysValue === undefined) {
80
+ throw new TypeError("activity.syncDays must be a finite number");
81
+ }
82
+ const syncDays = syncDaysValue ?? 1;
83
+ if (!Number.isInteger(syncDays) || syncDays < 1 || syncDays > 90) {
84
+ throw new RangeError("activity.syncDays must be an integer from 1 to 90");
85
+ }
86
+ const intervalValue = coerceNumber(config.autoSyncIntervalMinutes, "activity.autoSyncIntervalMinutes");
87
+ if (config.autoSyncIntervalMinutes !== undefined && intervalValue === undefined) {
88
+ throw new TypeError("activity.autoSyncIntervalMinutes must be a finite number");
89
+ }
90
+ const autoSyncIntervalMinutes = intervalValue ?? 15;
91
+ if (!Number.isInteger(autoSyncIntervalMinutes) || autoSyncIntervalMinutes < 1 || autoSyncIntervalMinutes > 1440) {
92
+ throw new RangeError("activity.autoSyncIntervalMinutes must be an integer from 1 to 1440");
93
+ }
94
+ if (config.sources !== undefined && !Array.isArray(config.sources)) {
95
+ throw new TypeError("activity.sources must be an array");
96
+ }
97
+ const sources = (config.sources ?? []).map(parseSource);
98
+ // Cursors and row dedup are keyed on machineLabel alone; two sources sharing
99
+ // a label would share a cursor and dedup namespace, silently skipping or
100
+ // conflating snapshots. Reject the collision at parse.
101
+ const labels = new Set<string>();
102
+ for (const source of sources) {
103
+ if (labels.has(source.machineLabel)) {
104
+ throw new RangeError(`activity source machineLabel must be unique: ${source.machineLabel}`);
105
+ }
106
+ labels.add(source.machineLabel);
107
+ }
108
+ const enabled = enabledValue ?? false;
109
+ if (enabled && sources.length === 0) throw new RangeError("activity.enabled requires at least one source");
110
+ return { enabled, timezone, syncDays, autoSyncIntervalMinutes, sources };
111
+ }
@@ -55,11 +55,11 @@ function timezoneOffsetIso(instant: Date, timezone: string): string {
55
55
  return match?.[1] ?? "+00:00";
56
56
  }
57
57
 
58
- function assertValidTimezone(timezone: string): void {
58
+ export function assertValidTimezone(timezone: string): void {
59
59
  try {
60
60
  new Intl.DateTimeFormat("en-US", { timeZone: timezone });
61
61
  } catch {
62
- throw new RangeError(`Invalid IANA timezone "${timezone}" for the activity digest.`);
62
+ throw new RangeError(`Invalid IANA timezone "${timezone}".`);
63
63
  }
64
64
  }
65
65
 
@@ -7,3 +7,9 @@
7
7
  export * from "./types.js";
8
8
  export * from "./store.js";
9
9
  export * from "./digest.js";
10
+ export * from "./pipeline.js";
11
+ export * from "./source-client.js";
12
+ export * from "./runner.js";
13
+ export * from "./scheduler.js";
14
+ export * from "./config.js";
15
+ export * from "./reindex.js";
@@ -0,0 +1,379 @@
1
+ import assert from "node:assert/strict";
2
+ import { access, mkdtemp, readFile, rm } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+
7
+ import { activityDigestPath } from "./digest.js";
8
+ import { activityCursorKey, syncActivitySource } from "./pipeline.js";
9
+ import { ActivityStore } from "./store.js";
10
+ import type { ActivitySnapshot, ActivitySourceClient } from "./types.js";
11
+
12
+ function snapshot(overrides: Partial<ActivitySnapshot> = {}): ActivitySnapshot {
13
+ return {
14
+ machine: "untrusted-source-value",
15
+ capturedAtUtc: "2026-07-22T14:00:00.000Z",
16
+ app: "Browser",
17
+ windowTitle: "Example",
18
+ text: "A synthetic activity snapshot",
19
+ textSource: "ax",
20
+ contentHash: "hash-1",
21
+ ...overrides,
22
+ };
23
+ }
24
+
25
+ function source(
26
+ machineLabel: string,
27
+ pages: Map<string | null, ActivitySnapshot[]>,
28
+ cursors: Map<string | null, string | null>,
29
+ ): ActivitySourceClient {
30
+ return {
31
+ machineLabel,
32
+ async verify() {
33
+ return { ok: true };
34
+ },
35
+ async fetchSnapshots({ cursor }) {
36
+ return { snapshots: pages.get(cursor ?? null) ?? [], nextCursor: cursors.get(cursor ?? null) ?? null };
37
+ },
38
+ };
39
+ }
40
+
41
+ test("syncActivitySource persists every page, renders the digest, then advances the cursor", async () => {
42
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-activity-pipeline-"));
43
+ const store = ActivityStore.open(memoryDir);
44
+ try {
45
+ const client = source(
46
+ "workstation-a",
47
+ new Map([
48
+ [null, [snapshot()]],
49
+ ["cursor-1", [snapshot({ capturedAtUtc: "2026-07-22T14:01:00.000Z", contentHash: "hash-2" })]],
50
+ ]),
51
+ new Map([
52
+ [null, "cursor-1"],
53
+ ["cursor-1", "cursor-2"],
54
+ ]),
55
+ );
56
+
57
+ const result = await syncActivitySource(client, { date: "2026-07-22", timezone: "UTC", memoryDir, store });
58
+
59
+ assert.deepEqual(result, {
60
+ machine: "workstation-a",
61
+ fetched: 2,
62
+ inserted: 2,
63
+ duplicates: 0,
64
+ cursor: "cursor-2",
65
+ digestWritten: true,
66
+ });
67
+ assert.equal(store.getCursor(activityCursorKey("workstation-a", "2026-07-22")), "cursor-2");
68
+ assert.deepEqual(
69
+ store.listSnapshotsForDay(null, "2026-07-22T00:00:00.000Z", "2026-07-23T00:00:00.000Z").map((item) => item.machine),
70
+ ["workstation-a", "workstation-a"],
71
+ "the source identity, not a remote payload label, owns the persisted machine field",
72
+ );
73
+ assert.match(await readFile(activityDigestPath(memoryDir, "2026-07-22"), "utf8"), /snapshotCount: 2/);
74
+ } finally {
75
+ store.close();
76
+ await rm(memoryDir, { recursive: true, force: true });
77
+ }
78
+ });
79
+
80
+ test("syncActivitySource leaves the cursor unchanged when a later page cannot persist", async () => {
81
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-activity-pipeline-"));
82
+ const store = ActivityStore.open(memoryDir);
83
+ try {
84
+ const client = source(
85
+ "workstation-a",
86
+ new Map([
87
+ [null, [snapshot()]],
88
+ ["cursor-1", [snapshot({ capturedAtUtc: "not-a-timestamp", contentHash: "bad-hash" })]],
89
+ ]),
90
+ new Map([
91
+ [null, "cursor-1"],
92
+ ["cursor-1", "cursor-2"],
93
+ ]),
94
+ );
95
+
96
+ await assert.rejects(
97
+ syncActivitySource(client, { date: "2026-07-22", timezone: "UTC", memoryDir, store }),
98
+ /capture timestamp/,
99
+ );
100
+
101
+ assert.equal(store.getCursor(activityCursorKey("workstation-a", "2026-07-22")), null);
102
+ assert.equal(store.listSnapshotsForDay(null, "2026-07-22T00:00:00.000Z", "2026-07-23T00:00:00.000Z").length, 1);
103
+ } finally {
104
+ store.close();
105
+ await rm(memoryDir, { recursive: true, force: true });
106
+ }
107
+ });
108
+
109
+ test("syncActivitySource completes normally when the final page lands on the page cap", async () => {
110
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-activity-pipeline-"));
111
+ const store = ActivityStore.open(memoryDir);
112
+ try {
113
+ const client = source(
114
+ "workstation-a",
115
+ new Map([
116
+ [null, [snapshot()]],
117
+ ["cursor-1", [snapshot({ capturedAtUtc: "2026-07-22T14:01:00.000Z", contentHash: "hash-2" })]],
118
+ ]),
119
+ new Map([
120
+ [null, "cursor-1"],
121
+ ["cursor-1", null],
122
+ ]),
123
+ );
124
+
125
+ // Two pages with a page budget of exactly two: the terminal null cursor
126
+ // arrives on the last allowed page, which must not be treated as a runaway.
127
+ const result = await syncActivitySource(client, { date: "2026-07-22", timezone: "UTC", memoryDir, store, maxPages: 2 });
128
+ assert.equal(result.inserted, 2);
129
+ assert.equal(result.cursor, "cursor-1");
130
+ assert.equal(store.getCursor(activityCursorKey("workstation-a", "2026-07-22")), "cursor-1");
131
+ } finally {
132
+ store.close();
133
+ await rm(memoryDir, { recursive: true, force: true });
134
+ }
135
+ });
136
+
137
+ test("syncActivitySource rejects runaway pagination and leaves the cursor unadvanced", async () => {
138
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-activity-pipeline-"));
139
+ const store = ActivityStore.open(memoryDir);
140
+ try {
141
+ let issued = 0;
142
+ const runaway: ActivitySourceClient = {
143
+ machineLabel: "workstation-a",
144
+ async verify() {
145
+ return { ok: true };
146
+ },
147
+ async fetchSnapshots() {
148
+ issued += 1;
149
+ return { snapshots: [], nextCursor: `cursor-${issued}` };
150
+ },
151
+ };
152
+
153
+ await assert.rejects(
154
+ syncActivitySource(runaway, { date: "2026-07-22", timezone: "UTC", memoryDir, store, maxPages: 3 }),
155
+ /exceeded 3 pages/,
156
+ );
157
+ assert.equal(store.getCursor(activityCursorKey("workstation-a", "2026-07-22")), null);
158
+ } finally {
159
+ store.close();
160
+ await rm(memoryDir, { recursive: true, force: true });
161
+ }
162
+ });
163
+
164
+ test("concurrent same-day syncs from two sources persist without corrupting the digest", async () => {
165
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-activity-pipeline-"));
166
+ const store = ActivityStore.open(memoryDir);
167
+ try {
168
+ const clientA = source("workstation-a", new Map([[null, [snapshot({ contentHash: "a-1" })]]]), new Map([[null, null]]));
169
+ const clientB = source("workstation-b", new Map([[null, [snapshot({ contentHash: "b-1" })]]]), new Map([[null, null]]));
170
+
171
+ await Promise.all([
172
+ syncActivitySource(clientA, { date: "2026-07-22", timezone: "UTC", memoryDir, store }),
173
+ syncActivitySource(clientB, { date: "2026-07-22", timezone: "UTC", memoryDir, store }),
174
+ ]);
175
+
176
+ const digest = await readFile(activityDigestPath(memoryDir, "2026-07-22"), "utf8");
177
+ assert.match(digest, /kind: activity-digest/);
178
+ assert.match(digest, /snapshotCount: 2/);
179
+ assert.equal(store.getCursor(activityCursorKey("workstation-a", "2026-07-22")), null);
180
+ assert.equal(store.getCursor(activityCursorKey("workstation-b", "2026-07-22")), null);
181
+ } finally {
182
+ store.close();
183
+ await rm(memoryDir, { recursive: true, force: true });
184
+ }
185
+ });
186
+
187
+ test("syncActivitySource reindexes after a digest write, with the digest already durable", async () => {
188
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-activity-pipeline-"));
189
+ const store = ActivityStore.open(memoryDir);
190
+ try {
191
+ const client = source("workstation-a", new Map([[null, [snapshot()]]]), new Map([[null, null]]));
192
+ let reindexed = 0;
193
+ let digestAtReindex = "";
194
+ const result = await syncActivitySource(client, {
195
+ date: "2026-07-22",
196
+ timezone: "UTC",
197
+ memoryDir,
198
+ store,
199
+ afterWrites: async () => {
200
+ reindexed += 1;
201
+ // The reindex must see the durable digest already on disk — that is
202
+ // what makes the new day discoverable (rule 31).
203
+ digestAtReindex = await readFile(activityDigestPath(memoryDir, "2026-07-22"), "utf8");
204
+ },
205
+ });
206
+
207
+ assert.equal(result.digestWritten, true);
208
+ assert.equal(reindexed, 1, "reindex fires exactly once after the digest write");
209
+ assert.match(digestAtReindex, /snapshotCount: 1/, "the written digest is visible to the reindex");
210
+ assert.equal(result.reindexError, undefined);
211
+ } finally {
212
+ store.close();
213
+ await rm(memoryDir, { recursive: true, force: true });
214
+ }
215
+ });
216
+
217
+ test("syncActivitySource skips the reindex when the digest is unchanged", async () => {
218
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-activity-pipeline-"));
219
+ const store = ActivityStore.open(memoryDir);
220
+ try {
221
+ let reindexed = 0;
222
+ const opts = {
223
+ date: "2026-07-22",
224
+ timezone: "UTC",
225
+ memoryDir,
226
+ store,
227
+ afterWrites: async () => {
228
+ reindexed += 1;
229
+ },
230
+ };
231
+ const first = source("workstation-a", new Map([[null, [snapshot()]]]), new Map([[null, null]]));
232
+ await syncActivitySource(first, opts);
233
+ const second = source("workstation-a", new Map([[null, [snapshot()]]]), new Map([[null, null]]));
234
+ const result = await syncActivitySource(second, opts);
235
+
236
+ assert.equal(result.digestWritten, false, "an unchanged day rewrites nothing");
237
+ assert.equal(reindexed, 1, "no reindex when the digest did not change");
238
+ } finally {
239
+ store.close();
240
+ await rm(memoryDir, { recursive: true, force: true });
241
+ }
242
+ });
243
+
244
+ test("syncActivitySource isolates a reindex failure: rows, digest, and cursor still commit", async () => {
245
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-activity-pipeline-"));
246
+ const store = ActivityStore.open(memoryDir);
247
+ try {
248
+ const client = source("workstation-a", new Map([[null, [snapshot()]]]), new Map([[null, null]]));
249
+ const result = await syncActivitySource(client, {
250
+ date: "2026-07-22",
251
+ timezone: "UTC",
252
+ memoryDir,
253
+ store,
254
+ afterWrites: async () => {
255
+ // A backend error whose message embeds an absolute path must be
256
+ // sanitized to a name+code in the exported reindexError, not echoed.
257
+ throw Object.assign(new Error("ENOENT: /abs/qmd/index.sqlite missing"), { code: "ENOENT" });
258
+ },
259
+ });
260
+
261
+ assert.equal(result.digestWritten, true, "the digest is durable despite the reindex failure");
262
+ assert.match(result.reindexError ?? "", /ENOENT/);
263
+ assert.ok(!result.reindexError?.includes("/abs/qmd"), "reindexError does not leak the absolute path");
264
+ assert.equal(result.inserted, 1);
265
+ await access(activityDigestPath(memoryDir, "2026-07-22"));
266
+ assert.equal(
267
+ store.getCursor(activityCursorKey("workstation-a", "2026-07-22")),
268
+ null,
269
+ "cursor still advanced; reindex is best-effort",
270
+ );
271
+ } finally {
272
+ store.close();
273
+ await rm(memoryDir, { recursive: true, force: true });
274
+ }
275
+ });
276
+
277
+ test("syncActivitySource honors an abort before committing the digest and cursor", async () => {
278
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-activity-pipeline-"));
279
+ const store = ActivityStore.open(memoryDir);
280
+ try {
281
+ const controller = new AbortController();
282
+ const client: ActivitySourceClient = {
283
+ machineLabel: "workstation-a",
284
+ async verify() {
285
+ return { ok: true };
286
+ },
287
+ async fetchSnapshots() {
288
+ // Abort fires after the last page returns but before the digest write.
289
+ controller.abort();
290
+ return { snapshots: [snapshot()], nextCursor: null };
291
+ },
292
+ };
293
+
294
+ await assert.rejects(
295
+ syncActivitySource(client, {
296
+ date: "2026-07-22",
297
+ timezone: "UTC",
298
+ memoryDir,
299
+ store,
300
+ signal: controller.signal,
301
+ }),
302
+ );
303
+
304
+ assert.equal(
305
+ store.getCursor(activityCursorKey("workstation-a", "2026-07-22")),
306
+ null,
307
+ "an aborted tick never advances the cursor",
308
+ );
309
+ await assert.rejects(
310
+ readFile(activityDigestPath(memoryDir, "2026-07-22"), "utf8"),
311
+ "an aborted tick never writes the digest",
312
+ );
313
+ } finally {
314
+ store.close();
315
+ await rm(memoryDir, { recursive: true, force: true });
316
+ }
317
+ });
318
+
319
+ test("syncActivitySource persists out-of-window snapshots under their own day, never the requested day's digest", async () => {
320
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-activity-pipeline-"));
321
+ const store = ActivityStore.open(memoryDir);
322
+ try {
323
+ const client = source(
324
+ "workstation-a",
325
+ new Map([
326
+ [
327
+ null,
328
+ [
329
+ snapshot({ capturedAtUtc: "2026-07-22T14:00:00.000Z", contentHash: "in-window" }),
330
+ // A snapshot the daemon misplaced onto a different day (replay /
331
+ // timezone bug). It must be retained under its own day, not lost.
332
+ snapshot({ capturedAtUtc: "2026-07-20T10:00:00.000Z", contentHash: "out-of-window" }),
333
+ ],
334
+ ],
335
+ ]),
336
+ new Map([[null, null]]),
337
+ );
338
+
339
+ const result = await syncActivitySource(client, { date: "2026-07-22", timezone: "UTC", memoryDir, store });
340
+
341
+ assert.equal(result.fetched, 2, "both snapshots were fetched");
342
+ assert.equal(result.inserted, 2, "both snapshots are durably persisted (no data loss)");
343
+ // The requested day's digest is a window query: only its in-window row.
344
+ const day = store.listSnapshotsForDay(null, "2026-07-22T00:00:00.000Z", "2026-07-23T00:00:00.000Z");
345
+ assert.deepEqual(day.map((s) => s.contentHash), ["in-window"], "the requested day contains only its in-window row");
346
+ assert.match(await readFile(activityDigestPath(memoryDir, "2026-07-22"), "utf8"), /snapshotCount: 1/);
347
+ // The misplaced row is durable under ITS OWN day, surfacing when that day syncs.
348
+ const ownDay = store.listSnapshotsForDay(null, "2026-07-20T00:00:00.000Z", "2026-07-21T00:00:00.000Z");
349
+ assert.deepEqual(ownDay.map((s) => s.contentHash), ["out-of-window"], "the misplaced row is retained under its own day, not dropped");
350
+ // Nothing was skipped, so the cursor advancing past the page is correct.
351
+ assert.equal(store.getCursor(activityCursorKey("workstation-a", "2026-07-22")), null, "cursor advances after a fully-persisted page");
352
+ } finally {
353
+ store.close();
354
+ await rm(memoryDir, { recursive: true, force: true });
355
+ }
356
+ });
357
+
358
+ test("syncActivitySource rejects a calendar-overflow timestamp instead of skipping it", async () => {
359
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-activity-pipeline-"));
360
+ const store = ActivityStore.open(memoryDir);
361
+ try {
362
+ const client = source(
363
+ "workstation-a",
364
+ // 2026-02-30 is impossible; Date.parse rolls it to Mar 2, which would fall
365
+ // outside a Feb 28 sync window. It must fail loudly, not be silently skipped.
366
+ new Map([[null, [snapshot({ capturedAtUtc: "2026-02-30T12:00:00.000Z", contentHash: "overflow" })]]]),
367
+ new Map([[null, null]]),
368
+ );
369
+
370
+ await assert.rejects(
371
+ syncActivitySource(client, { date: "2026-02-28", timezone: "UTC", memoryDir, store }),
372
+ /capture timestamp/,
373
+ );
374
+ assert.equal(store.getCursor(activityCursorKey("workstation-a", "2026-02-28")), null, "cursor never advances on a rejected page");
375
+ } finally {
376
+ store.close();
377
+ await rm(memoryDir, { recursive: true, force: true });
378
+ }
379
+ });