@remnic/core 9.63.12 → 9.64.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 (403) hide show
  1. package/dist/access-admin-ops-surface.d.ts +11 -11
  2. package/dist/access-admin-ops-surface.js +15 -15
  3. package/dist/access-authorization-probe.d.ts +11 -11
  4. package/dist/access-authorization-probe.js +16 -16
  5. package/dist/access-boundary.d.ts +11 -11
  6. package/dist/access-boundary.js +15 -15
  7. package/dist/access-cli.js +49 -49
  8. package/dist/access-coding-context-resolution.d.ts +1 -1
  9. package/dist/access-extraction-force-flush.d.ts +11 -11
  10. package/dist/access-extraction-force-flush.js +15 -15
  11. package/dist/access-health-types.d.ts +1 -1
  12. package/dist/access-http-lcm-compaction.d.ts +11 -11
  13. package/dist/access-http-lifecycle-flush.d.ts +11 -11
  14. package/dist/access-http-offline-stream.d.ts +11 -11
  15. package/dist/access-http-query.d.ts +2 -2
  16. package/dist/access-http-query.js +17 -17
  17. package/dist/access-http.d.ts +12 -12
  18. package/dist/access-http.js +26 -26
  19. package/dist/access-identity-continuity-surface.d.ts +9 -9
  20. package/dist/access-identity-continuity-surface.js +15 -15
  21. package/dist/access-lcm-surface.d.ts +11 -11
  22. package/dist/access-lcm-surface.js +15 -15
  23. package/dist/access-mcp.d.ts +11 -11
  24. package/dist/access-mcp.js +20 -20
  25. package/dist/access-memory-search-fanout.d.ts +2 -2
  26. package/dist/{access-namespace-preflight-CrF0FKaz.d.ts → access-namespace-preflight-Dj9uR4Cq.d.ts} +1 -1
  27. package/dist/access-namespace-preflight.d.ts +3 -3
  28. package/dist/access-namespace-preflight.js +15 -15
  29. package/dist/access-observe-write-surface.d.ts +11 -11
  30. package/dist/access-observe-write-surface.js +15 -15
  31. package/dist/access-offline-manifest.d.ts +9 -9
  32. package/dist/access-operations-batch.js +16 -16
  33. package/dist/access-operations.d.ts +11 -11
  34. package/dist/access-operations.js +19 -19
  35. package/dist/access-recall-concurrency.d.ts +11 -11
  36. package/dist/access-recall-concurrency.js +15 -15
  37. package/dist/access-recall-response.d.ts +11 -11
  38. package/dist/access-recall-response.js +15 -15
  39. package/dist/access-recall-surface.d.ts +11 -11
  40. package/dist/access-recall-surface.js +15 -15
  41. package/dist/{access-service-U6jiERic.d.ts → access-service-Bd0lzaHp.d.ts} +7 -7
  42. package/dist/access-service-helpers.d.ts +11 -11
  43. package/dist/access-service.d.ts +11 -11
  44. package/dist/access-service.js +15 -15
  45. package/dist/access-surface-catalog.d.ts +11 -11
  46. package/dist/access-surface-catalog.js +17 -17
  47. package/dist/access-wearables-meetings-surface.d.ts +3 -3
  48. package/dist/action-confidence.d.ts +1 -1
  49. package/dist/active-memory-bridge.d.ts +1 -1
  50. package/dist/active-recall.d.ts +1 -1
  51. package/dist/active-recall.js +1 -1
  52. package/dist/ambient-provenance.d.ts +1 -1
  53. package/dist/artifact-search.d.ts +1 -1
  54. package/dist/behavior-learner.d.ts +1 -1
  55. package/dist/behavior-signals.d.ts +1 -1
  56. package/dist/bootstrap.d.ts +9 -9
  57. package/dist/briefing-window.d.ts +2 -2
  58. package/dist/briefing.d.ts +2 -2
  59. package/dist/briefing.js +3 -3
  60. package/dist/buffer-surprise-report.d.ts +1 -1
  61. package/dist/buffer-turn-helpers.d.ts +1 -1
  62. package/dist/buffer.d.ts +2 -2
  63. package/dist/buffer.js +2 -2
  64. package/dist/bulk-import/index.d.ts +3 -3
  65. package/dist/calibration.d.ts +1 -1
  66. package/dist/capabilities.d.ts +1 -1
  67. package/dist/{catalog-C0KIwZqR.d.ts → catalog-rKYbN7zF.d.ts} +1 -1
  68. package/dist/causal-behavior.d.ts +1 -1
  69. package/dist/causal-consolidation.d.ts +1 -1
  70. package/dist/causal-consolidation.js +4 -4
  71. package/dist/causal-trajectory-graph.d.ts +1 -1
  72. package/dist/{chunk-5SZ6CLGJ.js → chunk-2RZCOG63.js} +3 -2
  73. package/dist/{chunk-5SZ6CLGJ.js.map → chunk-2RZCOG63.js.map} +1 -1
  74. package/dist/{chunk-7DRSWPUV.js → chunk-3CYVTO6A.js} +3 -3
  75. package/dist/{chunk-LJQGQUNS.js → chunk-4FSCGYTQ.js} +2 -2
  76. package/dist/{chunk-HP6B732E.js → chunk-4KKVDOCD.js} +24 -24
  77. package/dist/{chunk-36XS2ASP.js → chunk-5Q5BEWJ3.js} +2 -2
  78. package/dist/{chunk-4XN35YQD.js → chunk-5TBO7CDR.js} +29 -1
  79. package/dist/chunk-5TBO7CDR.js.map +1 -0
  80. package/dist/{chunk-BYFKJXIR.js → chunk-6I6IOMKS.js} +2 -2
  81. package/dist/{chunk-YI25J2JD.js → chunk-7D3TTGHY.js} +22 -13
  82. package/dist/chunk-7D3TTGHY.js.map +1 -0
  83. package/dist/{chunk-4IIDTPSK.js → chunk-7X35RPN3.js} +2 -2
  84. package/dist/{chunk-FEP45MYY.js → chunk-AGC3Q53R.js} +2 -2
  85. package/dist/{chunk-ZWYXSF2T.js → chunk-AL43367I.js} +96 -4
  86. package/dist/chunk-AL43367I.js.map +1 -0
  87. package/dist/{chunk-WCWIV2JH.js → chunk-B2GICNOJ.js} +2 -2
  88. package/dist/{chunk-BTBAHTQ5.js → chunk-CI6YIT5S.js} +2 -2
  89. package/dist/{chunk-CUDSA46O.js → chunk-CUIAHZPZ.js} +2 -2
  90. package/dist/{chunk-BTUIHRRR.js → chunk-D267K4V5.js} +2 -2
  91. package/dist/{chunk-QICOU4RY.js → chunk-DLZBOEJY.js} +2 -2
  92. package/dist/{chunk-AR253JBM.js → chunk-DPPTG6VJ.js} +1 -1
  93. package/dist/chunk-DPPTG6VJ.js.map +1 -0
  94. package/dist/{chunk-BPLPGOUJ.js → chunk-DRS3OX77.js} +2 -2
  95. package/dist/{chunk-3F3CVVDS.js → chunk-DTQ3OB7O.js} +2 -2
  96. package/dist/{chunk-Z4UPRZR6.js → chunk-DZ2AVS2H.js} +2 -2
  97. package/dist/{chunk-YINSSARY.js → chunk-EAWJSY2Z.js} +1 -1
  98. package/dist/{chunk-NWHKHBL4.js → chunk-FMGTFMT3.js} +9 -9
  99. package/dist/{chunk-57WMVU4D.js → chunk-GSQDPL45.js} +3 -3
  100. package/dist/{chunk-UUYQWKHO.js → chunk-IHPYTG6F.js} +2 -2
  101. package/dist/{chunk-XREXYQ5U.js → chunk-IPUUMLSM.js} +21 -21
  102. package/dist/{chunk-SQ3EGGII.js → chunk-J3JEW5CT.js} +2 -2
  103. package/dist/{chunk-VCEE4YNA.js → chunk-JE3AKPOO.js} +2 -2
  104. package/dist/{chunk-IUSV4XJC.js → chunk-KOE3RLNN.js} +2 -2
  105. package/dist/{chunk-WQ6TRWAE.js → chunk-NMQXUGTU.js} +3 -3
  106. package/dist/{chunk-V2YH7T6T.js → chunk-OTVHRZZD.js} +2 -2
  107. package/dist/{chunk-77E7LLMV.js → chunk-OWAYQK5V.js} +8 -8
  108. package/dist/{chunk-YGSDWO3U.js → chunk-P4SD7BSA.js} +2 -2
  109. package/dist/{chunk-G5F4MXXC.js → chunk-Q3IRE6E4.js} +2 -2
  110. package/dist/{chunk-HZXO2CJ6.js → chunk-QHNO6AND.js} +2 -2
  111. package/dist/{chunk-5OOTHWGJ.js → chunk-RMB22Q3R.js} +2 -2
  112. package/dist/{chunk-BB2LH5JM.js → chunk-RNORDDD6.js} +11 -1
  113. package/dist/chunk-RNORDDD6.js.map +1 -0
  114. package/dist/{chunk-BS75WJBH.js → chunk-UIYYS2YI.js} +4 -4
  115. package/dist/{chunk-SYU5JOBQ.js → chunk-UWSETW6O.js} +2 -2
  116. package/dist/{chunk-CK3DZ6OE.js → chunk-VCNGVU62.js} +2 -2
  117. package/dist/{chunk-DIJCMHUE.js → chunk-VSKA2ATE.js} +2 -2
  118. package/dist/{chunk-PQODT53H.js → chunk-WDFXRHOZ.js} +3 -3
  119. package/dist/{chunk-6NMZXEJ7.js → chunk-WNO5W2TP.js} +2 -2
  120. package/dist/{chunk-RFFCBYBD.js → chunk-XBRGJJK7.js} +2 -2
  121. package/dist/{chunk-R5AWIILH.js → chunk-XFAWGUNX.js} +2 -2
  122. package/dist/{chunk-JASFZQ76.js → chunk-XSPRZZEX.js} +3 -3
  123. package/dist/{chunk-EHC3FIJ4.js → chunk-Y5ZIQFWH.js} +2 -2
  124. package/dist/{chunk-6IKXAAJI.js → chunk-YY63AMGP.js} +2 -2
  125. package/dist/{chunk-7ML7STOD.js → chunk-ZGGTIBWN.js} +2 -2
  126. package/dist/{chunk-X6HNXVXS.js → chunk-ZUIVJ3BB.js} +4 -4
  127. package/dist/{cli-BsaYHAH9.d.ts → cli-0dDmOsJ8.d.ts} +5 -5
  128. package/dist/cli.d.ts +13 -13
  129. package/dist/cli.js +39 -39
  130. package/dist/coding/pre-action-gate.d.ts +1 -1
  131. package/dist/compounding/engine.d.ts +2 -2
  132. package/dist/compounding/engine.js +3 -3
  133. package/dist/compounding/preference-consolidator.d.ts +1 -1
  134. package/dist/compression-optimizer.d.ts +1 -1
  135. package/dist/config.d.ts +1 -1
  136. package/dist/config.js +1 -1
  137. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  138. package/dist/connectors/codex-materialize-runner.js +3 -3
  139. package/dist/connectors/codex-materialize.d.ts +1 -1
  140. package/dist/connectors/index.d.ts +1 -1
  141. package/dist/connectors/index.js +4 -4
  142. package/dist/consolidation-provenance-check.d.ts +2 -2
  143. package/dist/consolidation-undo.d.ts +2 -2
  144. package/dist/contradiction/index.d.ts +2 -2
  145. package/dist/converge-config.d.ts +1 -1
  146. package/dist/convergence-refresh.d.ts +2 -2
  147. package/dist/convergence-refresh.js +5 -5
  148. package/dist/conversation-index/backend.d.ts +1 -1
  149. package/dist/conversation-index/chunker.d.ts +1 -1
  150. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  151. package/dist/conversation-index/indexer.d.ts +1 -1
  152. package/dist/conversation-index/search.d.ts +1 -1
  153. package/dist/corpus-watermark.d.ts +1 -1
  154. package/dist/corpus-watermark.js +5 -5
  155. package/dist/day-summary.d.ts +1 -1
  156. package/dist/delinearize.d.ts +1 -1
  157. package/dist/dependency-propagation-config.d.ts +1 -1
  158. package/dist/{dependency-propagation-delivery-Cph4PnBe.d.ts → dependency-propagation-delivery-Cr8u-XwC.d.ts} +1 -1
  159. package/dist/direct-answer-wiring.d.ts +1 -1
  160. package/dist/direct-answer.d.ts +1 -1
  161. package/dist/embedding-fallback.d.ts +1 -1
  162. package/dist/enrichment/index.d.ts +1 -1
  163. package/dist/entity-origin-fields.d.ts +1 -1
  164. package/dist/entity-retrieval.d.ts +2 -2
  165. package/dist/entity-retrieval.js +3 -3
  166. package/dist/entity-schema.d.ts +1 -1
  167. package/dist/episodic-context.d.ts +1 -1
  168. package/dist/explicit-capture.d.ts +9 -9
  169. package/dist/external-wiki-access.d.ts +11 -11
  170. package/dist/external-wiki-access.js +16 -16
  171. package/dist/external-wiki-collection-registration.d.ts +1 -1
  172. package/dist/external-wiki-collection.d.ts +1 -1
  173. package/dist/external-wiki-mcp-tools.d.ts +11 -11
  174. package/dist/extraction-error-classification.d.ts +1 -1
  175. package/dist/extraction-faithfulness.d.ts +1 -1
  176. package/dist/extraction-judge-telemetry.d.ts +1 -1
  177. package/dist/extraction-judge-training.d.ts +1 -1
  178. package/dist/extraction-judge.d.ts +1 -1
  179. package/dist/extraction-liveness.d.ts +1 -1
  180. package/dist/extraction-normalization.d.ts +1 -1
  181. package/dist/extraction-prompt.d.ts +1 -1
  182. package/dist/extraction-source-grounding-rules.d.ts +1 -1
  183. package/dist/extraction-source-grounding.d.ts +1 -1
  184. package/dist/extraction.d.ts +1 -1
  185. package/dist/extraction.js +8 -8
  186. package/dist/fallback-llm.d.ts +1 -1
  187. package/dist/graph-dashboard-diff.d.ts +1 -1
  188. package/dist/graph-dashboard-key.d.ts +1 -1
  189. package/dist/graph-dashboard-parser.d.ts +1 -1
  190. package/dist/graph-edge-reinforcement.d.ts +1 -1
  191. package/dist/graph-path-reconstruction.d.ts +1 -1
  192. package/dist/graph-path-scoring.d.ts +1 -1
  193. package/dist/graph-snapshot.d.ts +1 -1
  194. package/dist/graph.d.ts +1 -1
  195. package/dist/harmonic-retrieval.js +1 -1
  196. package/dist/importance.d.ts +1 -1
  197. package/dist/importers/index.d.ts +1 -1
  198. package/dist/in-flight-reads.d.ts +1 -1
  199. package/dist/index.d.ts +21 -21
  200. package/dist/index.js +49 -49
  201. package/dist/intent.d.ts +1 -1
  202. package/dist/lcm/engine.d.ts +1 -1
  203. package/dist/lcm/index.d.ts +1 -1
  204. package/dist/lcm/tools.d.ts +1 -1
  205. package/dist/lifecycle.d.ts +1 -1
  206. package/dist/live-connectors-runner.d.ts +1 -1
  207. package/dist/local-llm.d.ts +1 -1
  208. package/dist/local-model-endpoint.d.ts +1 -1
  209. package/dist/maintenance/memory-governance.d.ts +1 -1
  210. package/dist/maintenance/memory-governance.js +3 -3
  211. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +2 -2
  212. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  213. package/dist/maintenance/rebuild-memory-projection.d.ts +2 -2
  214. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  215. package/dist/{maintenance-BNq31xpo.d.ts → maintenance-BpNjuZk4.d.ts} +3 -3
  216. package/dist/mcp-memory-inspector-app.d.ts +11 -11
  217. package/dist/memory-action-policy.d.ts +1 -1
  218. package/dist/memory-cache.d.ts +1 -1
  219. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  220. package/dist/memory-projection-store.d.ts +1 -1
  221. package/dist/memory-provenance.d.ts +1 -1
  222. package/dist/memory-snapshot.d.ts +1 -1
  223. package/dist/memory-worth-outcomes.d.ts +2 -2
  224. package/dist/models-json.d.ts +1 -1
  225. package/dist/namespaces/migrate.d.ts +3 -3
  226. package/dist/namespaces/migrate.js +4 -4
  227. package/dist/namespaces/principal.d.ts +1 -1
  228. package/dist/namespaces/search.d.ts +1 -1
  229. package/dist/namespaces/storage.d.ts +3 -3
  230. package/dist/namespaces/storage.js +3 -3
  231. package/dist/native-knowledge.d.ts +1 -1
  232. package/dist/offline-sync-impression-drain.d.ts +1 -1
  233. package/dist/operator-doctor-corpus.d.ts +1 -1
  234. package/dist/operator-doctor-corpus.js +6 -6
  235. package/dist/operator-doctor-replica.d.ts +1 -1
  236. package/dist/operator-toolkit.d.ts +3 -3
  237. package/dist/operator-toolkit.js +12 -12
  238. package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
  239. package/dist/orchestration/maintenance.d.ts +4 -4
  240. package/dist/orchestration/maintenance.js +9 -9
  241. package/dist/{orchestrator-CBuUOP2J.d.ts → orchestrator-B3ZQ8CTJ.d.ts} +8 -8
  242. package/dist/orchestrator.d.ts +9 -9
  243. package/dist/orchestrator.js +49 -49
  244. package/dist/patterns-cli.d.ts +1 -1
  245. package/dist/{pipeline-CW_Q4WvX.d.ts → pipeline-q5emoXD6.d.ts} +1 -1
  246. package/dist/policy-runtime.d.ts +1 -1
  247. package/dist/proactive-contention.d.ts +1 -1
  248. package/dist/provenance.d.ts +1 -1
  249. package/dist/{public-http-jMlmLny4.d.ts → public-http-CQN-i7Ai.d.ts} +1 -1
  250. package/dist/{qmd-q-cAzYuC.d.ts → qmd-YC6YGphs.d.ts} +1 -1
  251. package/dist/qmd-preflight.d.ts +2 -2
  252. package/dist/qmd-recall-cache.d.ts +1 -1
  253. package/dist/qmd.d.ts +2 -2
  254. package/dist/recall-concurrency-config.d.ts +1 -1
  255. package/dist/recall-disclosure-escalation.d.ts +1 -1
  256. package/dist/recall-explain-renderer.d.ts +1 -1
  257. package/dist/recall-explain-renderer.js +3 -3
  258. package/dist/recall-memory-map.d.ts +1 -1
  259. package/dist/recall-planner-llm.d.ts +1 -1
  260. package/dist/recall-state.d.ts +1 -1
  261. package/dist/recall-tag-filter.d.ts +1 -1
  262. package/dist/recall-timings.d.ts +1 -1
  263. package/dist/recall-xray-cli.d.ts +1 -1
  264. package/dist/recall-xray-cli.js +4 -4
  265. package/dist/recall-xray-renderer.d.ts +1 -1
  266. package/dist/recall-xray-renderer.js +3 -3
  267. package/dist/recall-xray.d.ts +1 -1
  268. package/dist/recall-xray.js +2 -2
  269. package/dist/reconcile/cursor.d.ts +1 -1
  270. package/dist/reconcile/manifest.d.ts +1 -1
  271. package/dist/reconcile/plan.d.ts +1 -1
  272. package/dist/replay/normalizers/chatgpt.d.ts +1 -1
  273. package/dist/replay/normalizers/claude.d.ts +1 -1
  274. package/dist/replay/normalizers/openclaw.d.ts +1 -1
  275. package/dist/replay/normalizers/shared.d.ts +1 -1
  276. package/dist/replay/runner.d.ts +1 -1
  277. package/dist/replay/types.d.ts +1 -1
  278. package/dist/replica-divergence.d.ts +1 -1
  279. package/dist/replica-peers-config.d.ts +1 -1
  280. package/dist/resolve-auth-token.d.ts +1 -1
  281. package/dist/resume-bundles.js +2 -2
  282. package/dist/retrieval-agents.d.ts +2 -2
  283. package/dist/retrieval-tiers.d.ts +1 -1
  284. package/dist/routing/engine.d.ts +1 -1
  285. package/dist/routing/store.d.ts +1 -1
  286. package/dist/salvage-envelope.d.ts +1 -1
  287. package/dist/{scope-profiles-CIr4mXgp.d.ts → scope-profiles-De8F7YQ6.d.ts} +1 -1
  288. package/dist/search/embed-helper.d.ts +1 -1
  289. package/dist/search/factory.d.ts +1 -1
  290. package/dist/search/index.d.ts +1 -1
  291. package/dist/search/lancedb-backend.d.ts +1 -1
  292. package/dist/search/meilisearch-backend.d.ts +1 -1
  293. package/dist/search/noop-backend.d.ts +1 -1
  294. package/dist/search/orama-backend.d.ts +1 -1
  295. package/dist/search/port.d.ts +1 -1
  296. package/dist/search/remote-backend.d.ts +1 -1
  297. package/dist/{semantic-consolidation-BZ6J46j-.d.ts → semantic-consolidation-Bf-hm2LQ.d.ts} +1 -1
  298. package/dist/semantic-consolidation.d.ts +2 -2
  299. package/dist/semantic-consolidation.js +4 -4
  300. package/dist/semantic-rule-promotion.js +3 -3
  301. package/dist/semantic-rule-verifier.d.ts +1 -1
  302. package/dist/semantic-rule-verifier.js +3 -3
  303. package/dist/{service-CCl4iyyY.d.ts → service-D83osSRn.d.ts} +2 -2
  304. package/dist/session-observer-bands.d.ts +1 -1
  305. package/dist/session-observer-state.d.ts +1 -1
  306. package/dist/shared-context/manager.d.ts +1 -1
  307. package/dist/signal.d.ts +1 -1
  308. package/dist/source-agent-qualifier.d.ts +1 -1
  309. package/dist/source-agent-qualifier.js +7 -7
  310. package/dist/{storage-BPXmJKIt.d.ts → storage-BHWbpbhj.d.ts} +1 -1
  311. package/dist/storage.d.ts +2 -2
  312. package/dist/storage.js +2 -2
  313. package/dist/summarizer.d.ts +1 -1
  314. package/dist/summary-snapshot.d.ts +1 -1
  315. package/dist/support-passport/index.d.ts +14 -14
  316. package/dist/support-passport/index.js +18 -18
  317. package/dist/temporal-supersession.d.ts +2 -2
  318. package/dist/temporal-timeline-recall.d.ts +1 -1
  319. package/dist/temporal-validity.d.ts +1 -1
  320. package/dist/threading.d.ts +1 -1
  321. package/dist/tier-migration.d.ts +2 -2
  322. package/dist/tier-routing.d.ts +1 -1
  323. package/dist/topics.d.ts +1 -1
  324. package/dist/transcript.d.ts +1 -1
  325. package/dist/trust-score-stage.d.ts +1 -1
  326. package/dist/trust-score.d.ts +1 -1
  327. package/dist/{types-CltSWcND.d.ts → types-CpgsN-Gb.d.ts} +24 -3
  328. package/dist/types.d.ts +1 -1
  329. package/dist/types.js +1 -1
  330. package/dist/utility-runtime.d.ts +1 -1
  331. package/dist/verified-recall.js +3 -3
  332. package/dist/write-envelope.d.ts +1 -1
  333. package/package.json +2 -2
  334. package/src/config.ts +5 -6
  335. package/src/connectors/index.test.ts +24 -0
  336. package/src/connectors/index.ts +2 -0
  337. package/src/connectors/prime-agent.ts +38 -0
  338. package/src/location/cli.ts +163 -0
  339. package/src/location/config.test.ts +75 -0
  340. package/src/location/config.ts +105 -0
  341. package/src/location/intervals.test.ts +202 -0
  342. package/src/location/intervals.ts +221 -0
  343. package/src/location/parse-companions.ts +12 -0
  344. package/src/location/pipeline.test.ts +324 -0
  345. package/src/location/pipeline.ts +245 -0
  346. package/src/location/recall-exclusion.test.ts +36 -0
  347. package/src/location/registry.test.ts +74 -0
  348. package/src/location/registry.ts +54 -0
  349. package/src/location/store.test.ts +170 -0
  350. package/src/location/store.ts +478 -0
  351. package/src/location/types.ts +95 -0
  352. package/src/orchestration/generic-recall-paths.ts +17 -0
  353. package/src/orchestration/orchestrator-helpers.ts +22 -0
  354. package/src/orchestration/qmd-result-resolver.ts +1 -0
  355. package/src/types.ts +2 -3
  356. package/dist/chunk-4XN35YQD.js.map +0 -1
  357. package/dist/chunk-AR253JBM.js.map +0 -1
  358. package/dist/chunk-BB2LH5JM.js.map +0 -1
  359. package/dist/chunk-YI25J2JD.js.map +0 -1
  360. package/dist/chunk-ZWYXSF2T.js.map +0 -1
  361. /package/dist/{chunk-7DRSWPUV.js.map → chunk-3CYVTO6A.js.map} +0 -0
  362. /package/dist/{chunk-LJQGQUNS.js.map → chunk-4FSCGYTQ.js.map} +0 -0
  363. /package/dist/{chunk-HP6B732E.js.map → chunk-4KKVDOCD.js.map} +0 -0
  364. /package/dist/{chunk-36XS2ASP.js.map → chunk-5Q5BEWJ3.js.map} +0 -0
  365. /package/dist/{chunk-BYFKJXIR.js.map → chunk-6I6IOMKS.js.map} +0 -0
  366. /package/dist/{chunk-4IIDTPSK.js.map → chunk-7X35RPN3.js.map} +0 -0
  367. /package/dist/{chunk-FEP45MYY.js.map → chunk-AGC3Q53R.js.map} +0 -0
  368. /package/dist/{chunk-WCWIV2JH.js.map → chunk-B2GICNOJ.js.map} +0 -0
  369. /package/dist/{chunk-BTBAHTQ5.js.map → chunk-CI6YIT5S.js.map} +0 -0
  370. /package/dist/{chunk-CUDSA46O.js.map → chunk-CUIAHZPZ.js.map} +0 -0
  371. /package/dist/{chunk-BTUIHRRR.js.map → chunk-D267K4V5.js.map} +0 -0
  372. /package/dist/{chunk-QICOU4RY.js.map → chunk-DLZBOEJY.js.map} +0 -0
  373. /package/dist/{chunk-BPLPGOUJ.js.map → chunk-DRS3OX77.js.map} +0 -0
  374. /package/dist/{chunk-3F3CVVDS.js.map → chunk-DTQ3OB7O.js.map} +0 -0
  375. /package/dist/{chunk-Z4UPRZR6.js.map → chunk-DZ2AVS2H.js.map} +0 -0
  376. /package/dist/{chunk-YINSSARY.js.map → chunk-EAWJSY2Z.js.map} +0 -0
  377. /package/dist/{chunk-NWHKHBL4.js.map → chunk-FMGTFMT3.js.map} +0 -0
  378. /package/dist/{chunk-57WMVU4D.js.map → chunk-GSQDPL45.js.map} +0 -0
  379. /package/dist/{chunk-UUYQWKHO.js.map → chunk-IHPYTG6F.js.map} +0 -0
  380. /package/dist/{chunk-XREXYQ5U.js.map → chunk-IPUUMLSM.js.map} +0 -0
  381. /package/dist/{chunk-SQ3EGGII.js.map → chunk-J3JEW5CT.js.map} +0 -0
  382. /package/dist/{chunk-VCEE4YNA.js.map → chunk-JE3AKPOO.js.map} +0 -0
  383. /package/dist/{chunk-IUSV4XJC.js.map → chunk-KOE3RLNN.js.map} +0 -0
  384. /package/dist/{chunk-WQ6TRWAE.js.map → chunk-NMQXUGTU.js.map} +0 -0
  385. /package/dist/{chunk-V2YH7T6T.js.map → chunk-OTVHRZZD.js.map} +0 -0
  386. /package/dist/{chunk-77E7LLMV.js.map → chunk-OWAYQK5V.js.map} +0 -0
  387. /package/dist/{chunk-YGSDWO3U.js.map → chunk-P4SD7BSA.js.map} +0 -0
  388. /package/dist/{chunk-G5F4MXXC.js.map → chunk-Q3IRE6E4.js.map} +0 -0
  389. /package/dist/{chunk-HZXO2CJ6.js.map → chunk-QHNO6AND.js.map} +0 -0
  390. /package/dist/{chunk-5OOTHWGJ.js.map → chunk-RMB22Q3R.js.map} +0 -0
  391. /package/dist/{chunk-BS75WJBH.js.map → chunk-UIYYS2YI.js.map} +0 -0
  392. /package/dist/{chunk-SYU5JOBQ.js.map → chunk-UWSETW6O.js.map} +0 -0
  393. /package/dist/{chunk-CK3DZ6OE.js.map → chunk-VCNGVU62.js.map} +0 -0
  394. /package/dist/{chunk-DIJCMHUE.js.map → chunk-VSKA2ATE.js.map} +0 -0
  395. /package/dist/{chunk-PQODT53H.js.map → chunk-WDFXRHOZ.js.map} +0 -0
  396. /package/dist/{chunk-6NMZXEJ7.js.map → chunk-WNO5W2TP.js.map} +0 -0
  397. /package/dist/{chunk-RFFCBYBD.js.map → chunk-XBRGJJK7.js.map} +0 -0
  398. /package/dist/{chunk-R5AWIILH.js.map → chunk-XFAWGUNX.js.map} +0 -0
  399. /package/dist/{chunk-JASFZQ76.js.map → chunk-XSPRZZEX.js.map} +0 -0
  400. /package/dist/{chunk-EHC3FIJ4.js.map → chunk-Y5ZIQFWH.js.map} +0 -0
  401. /package/dist/{chunk-6IKXAAJI.js.map → chunk-YY63AMGP.js.map} +0 -0
  402. /package/dist/{chunk-7ML7STOD.js.map → chunk-ZGGTIBWN.js.map} +0 -0
  403. /package/dist/{chunk-X6HNXVXS.js.map → chunk-ZUIVJ3BB.js.map} +0 -0
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Location provider registry (issue #2044).
3
+ *
4
+ * In-memory only: host adapters register provider implementations at startup.
5
+ * Core never statically imports an optional provider package — the registry
6
+ * holds whatever the host wired in, and a configured source whose provider is
7
+ * not registered is skipped (not an error), so partial installs keep working.
8
+ */
9
+
10
+ import type { LocationProvider } from "./types.js";
11
+
12
+ const PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
13
+
14
+ const providers = new Map<string, LocationProvider>();
15
+
16
+ function assertValidProvider(provider: LocationProvider): void {
17
+ if (typeof provider !== "object" || provider === null) {
18
+ throw new TypeError("location provider must be an object");
19
+ }
20
+ if (typeof provider.id !== "string" || !PROVIDER_ID_PATTERN.test(provider.id)) {
21
+ throw new RangeError(
22
+ "location provider id must be a non-empty lowercase kebab string (a-z, 0-9, hyphens)",
23
+ );
24
+ }
25
+ if (typeof provider.displayName !== "string" || provider.displayName.trim().length === 0) {
26
+ throw new TypeError(`location provider '${provider.id}' must declare a non-empty displayName`);
27
+ }
28
+ if (typeof provider.verify !== "function" || typeof provider.fetchObservations !== "function") {
29
+ throw new TypeError(
30
+ `location provider '${provider.id}' must implement verify() and fetchObservations()`,
31
+ );
32
+ }
33
+ }
34
+
35
+ export function registerLocationProvider(provider: LocationProvider): void {
36
+ assertValidProvider(provider);
37
+ if (providers.has(provider.id)) {
38
+ throw new Error(`location provider '${provider.id}' is already registered`);
39
+ }
40
+ providers.set(provider.id, provider);
41
+ }
42
+
43
+ export function getLocationProvider(id: string): LocationProvider | undefined {
44
+ return typeof id === "string" ? providers.get(id) : undefined;
45
+ }
46
+
47
+ export function listLocationProviders(): string[] {
48
+ return [...providers.keys()].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
49
+ }
50
+
51
+ /** Test-only: reset the registry between cases. */
52
+ export function clearLocationProviders(): void {
53
+ providers.clear();
54
+ }
@@ -0,0 +1,170 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, mkdir, rm, symlink, writeFile, readFile } 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 {
8
+ composeLocationDayBody,
9
+ composeLocationDayMeta,
10
+ emptyLocationSyncState,
11
+ listLocationDayDates,
12
+ loadLocationSyncState,
13
+ locationDayFilePath,
14
+ locationSyncStateFilePath,
15
+ parseLocationDaySummary,
16
+ serializeLocationDay,
17
+ updateLocationSourceDay,
18
+ writeLocationDay,
19
+ } from "./store.js";
20
+ import type { LocationDaySourceEntry } from "./store.js";
21
+
22
+ function entry(overrides: Partial<LocationDaySourceEntry> = {}): LocationDaySourceEntry {
23
+ return {
24
+ observationCount: 1,
25
+ segments: [
26
+ {
27
+ startUtc: "2026-08-17T09:00:00.000Z",
28
+ endUtc: "2026-08-17T09:30:00.000Z",
29
+ place: { id: "home", label: "Home" },
30
+ },
31
+ ],
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ test("day rendering is deterministic across source-map insertion order", () => {
37
+ const a: Record<string, LocationDaySourceEntry> = {
38
+ reitti: entry({ providerDisplayName: "Reitti" }),
39
+ other: entry({
40
+ providerDisplayName: "Other",
41
+ observationCount: 0,
42
+ segments: [],
43
+ }),
44
+ };
45
+ const b: Record<string, LocationDaySourceEntry> = {
46
+ other: entry({ providerDisplayName: "Other", observationCount: 0, segments: [] }),
47
+ reitti: entry({ providerDisplayName: "Reitti" }),
48
+ };
49
+ const render = (sources: Record<string, LocationDaySourceEntry>) => {
50
+ const body = composeLocationDayBody("2026-08-17", "UTC", sources);
51
+ return serializeLocationDay(composeLocationDayMeta("2026-08-17", "UTC", sources, body), body);
52
+ };
53
+ const first = render(a);
54
+ const second = render(b);
55
+ assert.equal(first, second, "identical content renders byte-identical regardless of key order");
56
+ const reparsed = parseLocationDaySummary(first);
57
+ assert.deepEqual(reparsed, {
58
+ date: "2026-08-17",
59
+ timezone: "UTC",
60
+ sources: ["other", "reitti"],
61
+ observationCount: 1,
62
+ contentHash: parseLocationDaySummary(second)!.contentHash,
63
+ });
64
+ assert.equal(parseLocationDaySummary("# not a location day\n"), null);
65
+ });
66
+
67
+ test("writeLocationDay is idempotent and read-back verifies content", async () => {
68
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-location-store-"));
69
+ try {
70
+ const body = composeLocationDayBody("2026-08-17", "UTC", { reitti: entry() });
71
+ const serialized = serializeLocationDay(
72
+ composeLocationDayMeta("2026-08-17", "UTC", { reitti: entry() }, body),
73
+ body,
74
+ );
75
+ assert.equal(await writeLocationDay(memoryDir, "2026-08-17", serialized), true);
76
+ assert.equal(await writeLocationDay(memoryDir, "2026-08-17", serialized), false, "unchanged content rewrites nothing");
77
+ assert.equal(await readFile(locationDayFilePath(memoryDir, "2026-08-17"), "utf-8"), serialized);
78
+ assert.deepEqual(await listLocationDayDates(memoryDir), ["2026-08-17"]);
79
+ } finally {
80
+ await rm(memoryDir, { recursive: true, force: true });
81
+ }
82
+ });
83
+
84
+ test("locationDayFilePath rejects traversal-shaped dates", () => {
85
+ assert.throws(() => locationDayFilePath("/tmp/x", "../../etc/passwd"), RangeError);
86
+ assert.throws(() => locationDayFilePath("/tmp/x", "2026-13-01"), RangeError);
87
+ });
88
+
89
+ test("store rejects a symlinked locations directory even when it resolves inside memoryDir", async () => {
90
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-location-symlink-"));
91
+ try {
92
+ const memoryDir = path.join(root, "memory");
93
+ const target = path.join(root, "elsewhere");
94
+ await mkdir(memoryDir, { recursive: true });
95
+ await mkdir(target, { recursive: true });
96
+ await symlink(target, path.join(memoryDir, "locations"));
97
+ await assert.rejects(
98
+ () => writeLocationDay(memoryDir, "2026-08-17", "---\nkind: location-day\n"),
99
+ /symlink/,
100
+ );
101
+ } finally {
102
+ await rm(root, { recursive: true, force: true });
103
+ }
104
+ });
105
+
106
+ test("store rejects a symlinked day file that escapes the memory dir", async () => {
107
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-location-symlinkfile-"));
108
+ try {
109
+ const memoryDir = path.join(root, "memory");
110
+ await mkdir(path.join(memoryDir, "locations"), { recursive: true });
111
+ await writeFile(path.join(root, "secret.md"), "outside", "utf-8");
112
+ await symlink(path.join(root, "secret.md"), path.join(memoryDir, "locations", "2026-08-17.md"));
113
+ await assert.rejects(
114
+ () => writeLocationDay(memoryDir, "2026-08-17", "---\nkind: location-day\n"),
115
+ /outside the memory dir|symbolic link/,
116
+ );
117
+ } finally {
118
+ await rm(root, { recursive: true, force: true });
119
+ }
120
+ });
121
+
122
+ test("sync state cold-starts on absence and corrupt JSON, then persists atomically", async () => {
123
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-location-state-"));
124
+ try {
125
+ assert.deepEqual(await loadLocationSyncState(memoryDir), emptyLocationSyncState());
126
+ await mkdir(path.dirname(locationSyncStateFilePath(memoryDir)), { recursive: true });
127
+ await writeFile(locationSyncStateFilePath(memoryDir), "{not json", "utf-8");
128
+ assert.deepEqual(await loadLocationSyncState(memoryDir), emptyLocationSyncState());
129
+ await updateLocationSourceDay(memoryDir, "reitti", "2026-08-17", entry(), "2026-08-18T00:00:05.000Z");
130
+ const state = await loadLocationSyncState(memoryDir);
131
+ assert.equal(state.sources.reitti?.lastSyncedAtUtc, "2026-08-18T00:00:05.000Z");
132
+ assert.equal(state.sources.reitti?.days?.["2026-08-17"]?.observationCount, 1);
133
+ } finally {
134
+ await rm(memoryDir, { recursive: true, force: true });
135
+ }
136
+ });
137
+
138
+ test("concurrent source state updates do not lose each other", async () => {
139
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-location-concurrent-"));
140
+ try {
141
+ await Promise.all([
142
+ updateLocationSourceDay(memoryDir, "reitti", "2026-08-17", entry(), "2026-08-18T00:00:01.000Z"),
143
+ updateLocationSourceDay(memoryDir, "other", "2026-08-17", entry({ observationCount: 2 }), "2026-08-18T00:00:02.000Z"),
144
+ updateLocationSourceDay(memoryDir, "third", "2026-08-16", entry(), "2026-08-18T00:00:03.000Z"),
145
+ ]);
146
+ const state = await loadLocationSyncState(memoryDir);
147
+ assert.deepEqual(Object.keys(state.sources).sort(), ["other", "reitti", "third"]);
148
+ assert.equal(state.sources.reitti?.days?.["2026-08-17"]?.observationCount, 1);
149
+ assert.equal(state.sources.other?.days?.["2026-08-17"]?.observationCount, 2);
150
+ } finally {
151
+ await rm(memoryDir, { recursive: true, force: true });
152
+ }
153
+ });
154
+
155
+ test("day payloads prune beyond the retention cap, newest kept", async () => {
156
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-location-prune-"));
157
+ try {
158
+ for (let day = 0; day < 95; day += 1) {
159
+ const date = new Date(Date.UTC(2026, 5, 1) + day * 86_400_000).toISOString().slice(0, 10);
160
+ await updateLocationSourceDay(memoryDir, "reitti", date, entry(), "2026-09-01T00:00:00.000Z");
161
+ }
162
+ const state = await loadLocationSyncState(memoryDir);
163
+ const days = Object.keys(state.sources.reitti?.days ?? {});
164
+ assert.equal(days.length, 90);
165
+ assert.equal(days.includes("2026-06-01"), false, "oldest days pruned");
166
+ assert.equal(days.includes("2026-06-06"), true, "newest 90 days kept");
167
+ } finally {
168
+ await rm(memoryDir, { recursive: true, force: true });
169
+ }
170
+ });
@@ -0,0 +1,478 @@
1
+ /**
2
+ * Location day-file + sync-state store (issue #2044).
3
+ *
4
+ * Day documents live at `<memoryDir>/locations/<YYYY-MM-DD>.md` — OUTSIDE the
5
+ * memory scan roots (facts/, entities/, …) so raw location history never
6
+ * surfaces as a memory in recall or governance passes, and INSIDE the QMD
7
+ * collection root so the markdown stays full-text searchable once an index
8
+ * update runs. Per-source sync state lives at
9
+ * `<memoryDir>/state/locations/sync.json` and is written only after the day
10
+ * document is durable (see pipeline.ts).
11
+ *
12
+ * Both writes are atomic (temp file + rename) and containment-checked: a
13
+ * symlinked `locations/` root or day file is rejected even when it resolves
14
+ * inside the memory dir (AGENTS.md pattern #3), and a path that escapes the
15
+ * memory dir is refused outright.
16
+ */
17
+
18
+ import { createHash } from "node:crypto";
19
+ import { lstat, mkdir, readFile, readdir, realpath } from "node:fs/promises";
20
+ import path from "node:path";
21
+
22
+ import { writeFileAtomically } from "../maintenance/atomic-file.js";
23
+ import { pathIsInside } from "../utils/path-containment.js";
24
+ import { isValidLocationDate, placeDurations } from "./intervals.js";
25
+ import type { LocationSegment } from "./types.js";
26
+
27
+ export const LOCATIONS_DIR_NAME = "locations";
28
+ export const LOCATION_DAY_KIND = "location-day";
29
+ export const LOCATION_DAY_FORMAT_VERSION = 1;
30
+
31
+ /** Cap on remembered day payloads per source (~3 months of days). */
32
+ const MAX_TRACKED_LOCATION_DAYS = 90;
33
+
34
+ const DAY_FILE_NAME_PATTERN = /^\d{4}-\d{2}-\d{2}\.md$/;
35
+
36
+ // Serializes state load→mutate→save sections per state file so two sources
37
+ // syncing concurrently cannot lose-update each other's entries.
38
+ const stateLocks = new Map<string, Promise<unknown>>();
39
+
40
+ function withStateLock<T>(key: string, fn: () => Promise<T>): Promise<T> {
41
+ const prior = stateLocks.get(key) ?? Promise.resolve();
42
+ const next = prior.then(fn, fn);
43
+ const tail = next.then(
44
+ () => undefined,
45
+ () => undefined,
46
+ );
47
+ stateLocks.set(key, tail);
48
+ void tail.then(() => {
49
+ if (stateLocks.get(key) === tail) stateLocks.delete(key);
50
+ });
51
+ return next;
52
+ }
53
+
54
+ export function locationDayFilePath(memoryDir: string, date: string): string {
55
+ if (!isValidLocationDate(date)) {
56
+ // Never interpolate an unvalidated date into a filesystem path (a `../`
57
+ // would escape <memoryDir>/locations/). Reject loudly.
58
+ throw new RangeError(`Invalid location date "${date}"; expected a real YYYY-MM-DD day.`);
59
+ }
60
+ return path.join(memoryDir, LOCATIONS_DIR_NAME, `${date}.md`);
61
+ }
62
+
63
+ export function locationSyncStateFilePath(memoryDir: string): string {
64
+ return path.join(memoryDir, "state", "locations", "sync.json");
65
+ }
66
+
67
+ /** A source's derived day payload, kept in sync state for multi-source merges. */
68
+ export interface LocationSourceDayPayload {
69
+ observationCount: number;
70
+ segments: LocationSegment[];
71
+ providerDisplayName?: string;
72
+ }
73
+
74
+ export interface LocationSourceSyncState {
75
+ lastSyncedAtUtc?: string;
76
+ days?: Record<string, LocationSourceDayPayload>;
77
+ }
78
+
79
+ export interface LocationSyncStateFile {
80
+ version: 1;
81
+ sources: Record<string, LocationSourceSyncState>;
82
+ }
83
+
84
+ export function emptyLocationSyncState(): LocationSyncStateFile {
85
+ return { version: 1, sources: {} };
86
+ }
87
+
88
+ export async function loadLocationSyncState(memoryDir: string): Promise<LocationSyncStateFile> {
89
+ let raw: string;
90
+ try {
91
+ raw = await readFile(locationSyncStateFilePath(memoryDir), "utf-8");
92
+ } catch (err) {
93
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return emptyLocationSyncState();
94
+ throw err;
95
+ }
96
+ let parsed: unknown;
97
+ try {
98
+ parsed = JSON.parse(raw);
99
+ } catch {
100
+ // A corrupt state file must not brick syncing forever; treat it as a cold
101
+ // start. The worst case is re-syncing days already on disk, and the day
102
+ // file's contentHash makes that rewrite idempotent.
103
+ return emptyLocationSyncState();
104
+ }
105
+ if (
106
+ typeof parsed !== "object" ||
107
+ parsed === null ||
108
+ Array.isArray(parsed) ||
109
+ typeof (parsed as LocationSyncStateFile).sources !== "object" ||
110
+ (parsed as LocationSyncStateFile).sources === null
111
+ ) {
112
+ return emptyLocationSyncState();
113
+ }
114
+ return { version: 1, sources: (parsed as LocationSyncStateFile).sources };
115
+ }
116
+
117
+ export async function saveLocationSyncState(
118
+ memoryDir: string,
119
+ state: LocationSyncStateFile,
120
+ ): Promise<void> {
121
+ await mkdir(path.dirname(locationSyncStateFilePath(memoryDir)), { recursive: true, mode: 0o700 });
122
+ await writeFileAtomically(
123
+ locationSyncStateFilePath(memoryDir),
124
+ `${JSON.stringify(state, null, 2)}\n`,
125
+ );
126
+ }
127
+
128
+ function pruneSourceDays(source: LocationSourceSyncState): void {
129
+ const days = source.days;
130
+ if (days === undefined) return;
131
+ const keys = Object.keys(days).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
132
+ for (const key of keys.slice(0, Math.max(0, keys.length - MAX_TRACKED_LOCATION_DAYS))) {
133
+ delete days[key];
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Record one source's completed day sync. Runs the whole read-modify-write
139
+ * under the per-directory state lock so concurrent sources cannot clobber
140
+ * each other, and prunes day payloads beyond the retention cap.
141
+ */
142
+ export async function updateLocationSourceDay(
143
+ memoryDir: string,
144
+ sourceId: string,
145
+ date: string,
146
+ payload: LocationSourceDayPayload,
147
+ completedAtUtc: string,
148
+ ): Promise<LocationSyncStateFile> {
149
+ if (!isValidLocationDate(date)) {
150
+ throw new RangeError(`Invalid location date "${date}"; expected a real YYYY-MM-DD day.`);
151
+ }
152
+ return withStateLock(locationSyncStateFilePath(memoryDir), async () => {
153
+ const state = await loadLocationSyncState(memoryDir);
154
+ const source = state.sources[sourceId] ?? {};
155
+ const days = source.days ?? {};
156
+ days[date] = payload;
157
+ source.days = days;
158
+ source.lastSyncedAtUtc = completedAtUtc;
159
+ state.sources[sourceId] = source;
160
+ for (const candidate of Object.values(state.sources)) pruneSourceDays(candidate);
161
+ await saveLocationSyncState(memoryDir, state);
162
+ return state;
163
+ });
164
+ }
165
+ /** Frontmatter persisted on a rendered day document. */
166
+ export interface LocationDayMeta {
167
+ kind: typeof LOCATION_DAY_KIND;
168
+ date: string;
169
+ timezone: string;
170
+ formatVersion: number;
171
+ sources: string[];
172
+ observationCount: number;
173
+ /** SHA-256 of the rendered body (rebuild idempotency). */
174
+ contentHash: string;
175
+ }
176
+
177
+ /** A parsed day document (frontmatter + rendered body). */
178
+ export interface LocationDayDocument {
179
+ meta: LocationDayMeta;
180
+ body: string;
181
+ }
182
+
183
+ /** The per-source view a day document is composed from. */
184
+ export interface LocationDaySourceEntry extends LocationSourceDayPayload {
185
+ /** Provider display name for the timeline heading; falls back to the id. */
186
+ providerDisplayName?: string;
187
+ }
188
+
189
+ export function hashLocationBody(body: string): string {
190
+ return createHash("sha256").update(body, "utf8").digest("hex");
191
+ }
192
+
193
+ function clockHhMm(iso: string, timezone: string): string {
194
+ return new Intl.DateTimeFormat("en-GB", {
195
+ timeZone: timezone,
196
+ hour: "2-digit",
197
+ minute: "2-digit",
198
+ hour12: false,
199
+ }).format(new Date(iso));
200
+ }
201
+
202
+ function formatDuration(ms: number): string {
203
+ const totalMinutes = Math.round(ms / 60_000);
204
+ const hours = Math.floor(totalMinutes / 60);
205
+ const minutes = totalMinutes % 60;
206
+ if (hours <= 0) return `${minutes}m`;
207
+ return `${hours}h ${String(minutes).padStart(2, "0")}m`;
208
+ }
209
+
210
+ /** Collapse whitespace and strip markdown emphasis/backticks from a label. */
211
+ function safeLabel(label: string): string {
212
+ return label.replace(/\s+/g, " ").replace(/[`*_]/g, "").trim();
213
+ }
214
+
215
+ function formatPlaceLabel(segment: LocationSegment): string {
216
+ const label = safeLabel(segment.place.label);
217
+ return segment.place.kind === undefined ? label : `${label} (${segment.place.kind})`;
218
+ }
219
+
220
+ function formatCoordinates(segment: LocationSegment): string {
221
+ if (segment.place.latitude === undefined || segment.place.longitude === undefined) return "";
222
+ return ` @ ${segment.place.latitude.toFixed(4)},${segment.place.longitude.toFixed(4)}`;
223
+ }
224
+
225
+ export function composeLocationDayBody(
226
+ date: string,
227
+ timezone: string,
228
+ sources: Record<string, LocationDaySourceEntry>,
229
+ ): string {
230
+ const lines: string[] = [`# Location day — ${date} (${timezone})`, ""];
231
+ const allSegments: LocationSegment[] = [];
232
+ let observationCount = 0;
233
+ for (const entry of Object.values(sources)) {
234
+ allSegments.push(...entry.segments);
235
+ observationCount += entry.observationCount;
236
+ }
237
+
238
+ lines.push("## Summary", "");
239
+ if (observationCount === 0) {
240
+ lines.push("No location observations recorded.", "");
241
+ } else {
242
+ const durations = placeDurations(allSegments).sort((a, b) => b.totalMs - a.totalMs);
243
+ lines.push(
244
+ `- Places visited: ${durations.length}`,
245
+ ...durations.map(
246
+ (duration) => `- ${safeLabel(duration.place.label)}: ${formatDuration(duration.totalMs)}`,
247
+ ),
248
+ "",
249
+ );
250
+ }
251
+
252
+ const sourceIds = Object.keys(sources).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
253
+ for (const sourceId of sourceIds) {
254
+ const entry = sources[sourceId]!;
255
+ lines.push(`## Timeline — ${safeLabel(entry.providerDisplayName ?? sourceId)}`, "");
256
+ if (entry.segments.length === 0) {
257
+ lines.push("No observations.", "");
258
+ continue;
259
+ }
260
+ for (const segment of entry.segments) {
261
+ lines.push(
262
+ `- ${clockHhMm(segment.startUtc, timezone)}–${clockHhMm(segment.endUtc, timezone)} — ${formatPlaceLabel(segment)}${formatCoordinates(segment)}`,
263
+ );
264
+ }
265
+ lines.push("");
266
+ }
267
+ return `${lines.join("\n").trimEnd()}\n`;
268
+ }
269
+
270
+ export function composeLocationDayMeta(
271
+ date: string,
272
+ timezone: string,
273
+ sources: Record<string, LocationDaySourceEntry>,
274
+ body: string,
275
+ ): LocationDayMeta {
276
+ let observationCount = 0;
277
+ for (const entry of Object.values(sources)) observationCount += entry.observationCount;
278
+ return {
279
+ kind: LOCATION_DAY_KIND,
280
+ date,
281
+ timezone,
282
+ formatVersion: LOCATION_DAY_FORMAT_VERSION,
283
+ sources: Object.keys(sources).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)),
284
+ observationCount,
285
+ contentHash: hashLocationBody(body),
286
+ };
287
+ }
288
+
289
+ function frontmatterScalar(key: string, value: string | number): string {
290
+ return `${key}: ${typeof value === "number" ? value : JSON.stringify(value)}`;
291
+ }
292
+
293
+ function frontmatterList(key: string, values: readonly string[]): string[] {
294
+ if (values.length === 0) return [`${key}: []`];
295
+ const lines = [`${key}:`];
296
+ for (const value of values) lines.push(` - ${JSON.stringify(value)}`);
297
+ return lines;
298
+ }
299
+
300
+ export function serializeLocationDay(meta: LocationDayMeta, body: string): string {
301
+ const lines: string[] = ["---"];
302
+ lines.push(frontmatterScalar("kind", meta.kind));
303
+ lines.push(frontmatterScalar("date", meta.date));
304
+ lines.push(frontmatterScalar("timezone", meta.timezone));
305
+ lines.push(frontmatterScalar("formatVersion", meta.formatVersion));
306
+ lines.push(...frontmatterList("sources", meta.sources));
307
+ lines.push(frontmatterScalar("observationCount", meta.observationCount));
308
+ lines.push(frontmatterScalar("contentHash", meta.contentHash));
309
+ lines.push("---", "", body);
310
+ return lines.join("\n");
311
+ }
312
+
313
+ function parseScalar(raw: string): string {
314
+ try {
315
+ const parsed = JSON.parse(raw);
316
+ return typeof parsed === "string" ? parsed : raw;
317
+ } catch {
318
+ return raw;
319
+ }
320
+ }
321
+
322
+ /** A day document's frontmatter, parsed for idempotency checks and status. */
323
+ export interface LocationDaySummary {
324
+ date: string;
325
+ timezone: string;
326
+ sources: string[];
327
+ observationCount: number;
328
+ contentHash: string;
329
+ }
330
+
331
+ /**
332
+ * Parse a day document's frontmatter. Returns null when the content is not a
333
+ * location day document (wrong kind, missing frontmatter) so callers can
334
+ * distinguish a foreign file from a corrupt one.
335
+ */
336
+ export function parseLocationDaySummary(raw: string): LocationDaySummary | null {
337
+ if (!raw.startsWith("---\n")) return null;
338
+ const closeIndex = raw.indexOf("\n---\n", 4);
339
+ if (closeIndex === -1) return null;
340
+ const scalars: Record<string, string> = {};
341
+ const lists: Record<string, string[]> = {};
342
+ let currentList: string | null = null;
343
+ for (const line of raw.slice(4, closeIndex).split("\n")) {
344
+ const listItem = /^ {2}- (.*)$/.exec(line);
345
+ if (listItem !== null && currentList !== null) {
346
+ lists[currentList]!.push(parseScalar(listItem[1]!));
347
+ continue;
348
+ }
349
+ currentList = null;
350
+ const separator = line.indexOf(":");
351
+ if (separator === -1) continue;
352
+ const key = line.slice(0, separator);
353
+ const value = line.slice(separator + 1).trim();
354
+ if (value === "" || value === "[]") {
355
+ lists[key] = [];
356
+ currentList = key;
357
+ continue;
358
+ }
359
+ if (value.startsWith("[")) {
360
+ lists[key] = [parseScalar(value)];
361
+ currentList = key;
362
+ continue;
363
+ }
364
+ scalars[key] = value;
365
+ }
366
+ if (scalars.kind === undefined || parseScalar(scalars.kind) !== LOCATION_DAY_KIND) return null;
367
+ if (scalars.date === undefined || scalars.contentHash === undefined) return null;
368
+ return {
369
+ date: parseScalar(scalars.date),
370
+ timezone: scalars.timezone === undefined ? "UTC" : parseScalar(scalars.timezone),
371
+ sources: lists.sources ?? [],
372
+ observationCount: scalars.observationCount === undefined ? 0 : Number(scalars.observationCount) || 0,
373
+ contentHash: parseScalar(scalars.contentHash),
374
+ };
375
+ }
376
+
377
+ // ── Containment-checked day-file IO ─────────────────────────────────────────
378
+
379
+ function isErrnoCode(err: unknown, code: string): boolean {
380
+ return err instanceof Error && (err as NodeJS.ErrnoException).code === code;
381
+ }
382
+
383
+ async function assertNotSymlink(target: string, kind: "directory" | "day file"): Promise<void> {
384
+ let stat: { isSymbolicLink(): boolean };
385
+ try {
386
+ stat = await lstat(target);
387
+ } catch (err) {
388
+ if (isErrnoCode(err, "ENOENT")) return;
389
+ throw err;
390
+ }
391
+ if (stat.isSymbolicLink()) {
392
+ throw new Error(
393
+ `locations ${kind} '${path.basename(target)}' is a symbolic link — refusing to follow it even when it resolves inside the memory dir (AGENTS.md pattern #3)`,
394
+ );
395
+ }
396
+ }
397
+
398
+ async function assertNoEscape(rootReal: string, candidate: string): Promise<void> {
399
+ let candidateReal: string;
400
+ try {
401
+ candidateReal = await realpath(candidate);
402
+ } catch {
403
+ return;
404
+ }
405
+ if (!pathIsInside(rootReal, candidateReal)) {
406
+ throw new Error(
407
+ "location day path resolves outside the memory dir — refusing to follow a symlink/traversal (AGENTS.md pattern #3)",
408
+ );
409
+ }
410
+ }
411
+
412
+ async function assertDayPathContained(memoryDir: string, targetPath: string): Promise<void> {
413
+ let rootReal: string;
414
+ try {
415
+ rootReal = await realpath(memoryDir);
416
+ } catch {
417
+ rootReal = path.resolve(memoryDir);
418
+ }
419
+ const locationsDir = path.join(memoryDir, LOCATIONS_DIR_NAME);
420
+ await assertNoEscape(rootReal, locationsDir);
421
+ await assertNoEscape(rootReal, targetPath);
422
+ // Reject a symlinked locations root or day file outright — even when the
423
+ // link resolves inside the memory dir (aliasing / tampering) — before IO.
424
+ await assertNotSymlink(locationsDir, "directory");
425
+ await assertNotSymlink(targetPath, "day file");
426
+ }
427
+
428
+ /**
429
+ * Idempotently persist a day document: skip the write when the on-disk
430
+ * contentHash already matches. The write is atomic and containment-checked;
431
+ * the caller (pipeline) advances sync state only after this resolves.
432
+ */
433
+ export async function writeLocationDay(
434
+ memoryDir: string,
435
+ date: string,
436
+ serialized: string,
437
+ ): Promise<boolean> {
438
+ const target = locationDayFilePath(memoryDir, date);
439
+ await assertDayPathContained(memoryDir, target);
440
+ try {
441
+ if ((await readFile(target, "utf-8")) === serialized) return false;
442
+ } catch (err) {
443
+ if (!isErrnoCode(err, "ENOENT")) throw err;
444
+ }
445
+ await mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
446
+ await writeFileAtomically(target, serialized);
447
+ return true;
448
+ }
449
+
450
+ /** Read a stored day document's raw markdown; null when absent. */
451
+ export async function readLocationDay(memoryDir: string, date: string): Promise<string | null> {
452
+ const target = locationDayFilePath(memoryDir, date);
453
+ await assertDayPathContained(memoryDir, target);
454
+ try {
455
+ return await readFile(target, "utf-8");
456
+ } catch (err) {
457
+ if (isErrnoCode(err, "ENOENT")) return null;
458
+ throw err;
459
+ }
460
+ }
461
+
462
+ /** List dates that have a stored day document, newest first. */
463
+ export async function listLocationDayDates(memoryDir: string): Promise<string[]> {
464
+ const locationsDir = path.join(memoryDir, LOCATIONS_DIR_NAME);
465
+ await assertDayPathContained(memoryDir, locationsDir);
466
+ let entries: string[];
467
+ try {
468
+ entries = await readdir(locationsDir);
469
+ } catch (err) {
470
+ if (isErrnoCode(err, "ENOENT")) return [];
471
+ throw err;
472
+ }
473
+ const dates = entries
474
+ .filter((entry) => DAY_FILE_NAME_PATTERN.test(entry))
475
+ .map((entry) => entry.slice(0, -3));
476
+ dates.sort((a, b) => (a > b ? -1 : a < b ? 1 : 0));
477
+ return dates;
478
+ }