@remnic/core 9.3.623 → 9.3.625

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 (262) hide show
  1. package/dist/access-cli.js +18 -16
  2. package/dist/access-cli.js.map +1 -1
  3. package/dist/access-http.d.ts +12 -5
  4. package/dist/access-http.js +10 -9
  5. package/dist/access-mcp.d.ts +5 -5
  6. package/dist/access-mcp.js +8 -8
  7. package/dist/access-schema.d.ts +5 -5
  8. package/dist/{access-service-CBNEKjzN.d.ts → access-service-C_sfOHsX.d.ts} +26 -3
  9. package/dist/access-service.d.ts +5 -5
  10. package/dist/access-service.js +7 -7
  11. package/dist/action-confidence.d.ts +1 -1
  12. package/dist/active-memory-bridge.d.ts +1 -1
  13. package/dist/active-recall.d.ts +1 -1
  14. package/dist/active-recall.js +2 -1
  15. package/dist/active-recall.js.map +1 -1
  16. package/dist/behavior-learner.d.ts +1 -1
  17. package/dist/behavior-signals.d.ts +1 -1
  18. package/dist/bootstrap.d.ts +4 -4
  19. package/dist/briefing.d.ts +1 -1
  20. package/dist/briefing.js +3 -3
  21. package/dist/buffer-surprise-report.d.ts +1 -1
  22. package/dist/buffer.d.ts +1 -1
  23. package/dist/calibration.d.ts +1 -1
  24. package/dist/causal-behavior.d.ts +1 -1
  25. package/dist/causal-consolidation.d.ts +1 -1
  26. package/dist/causal-consolidation.js +4 -4
  27. package/dist/{chunk-C4PZTWTG.js → chunk-2RHI3FGV.js} +540 -17
  28. package/dist/chunk-2RHI3FGV.js.map +1 -0
  29. package/dist/{chunk-GYTVOLNX.js → chunk-3MNBW7R7.js} +2 -2
  30. package/dist/{chunk-QFQQFX2H.js → chunk-3R2UZV3U.js} +2 -2
  31. package/dist/{chunk-O4UNM6OR.js → chunk-532VCWYW.js} +2 -2
  32. package/dist/{chunk-2UFQYU5F.js → chunk-57QXN2CS.js} +2 -2
  33. package/dist/chunk-7WV3F5DQ.js +22 -0
  34. package/dist/chunk-7WV3F5DQ.js.map +1 -0
  35. package/dist/{chunk-RKW6QR7W.js → chunk-AZ4RI3QD.js} +1461 -78
  36. package/dist/chunk-AZ4RI3QD.js.map +1 -0
  37. package/dist/{chunk-KQFQ3IS5.js → chunk-F3FY3D3S.js} +43 -7
  38. package/dist/chunk-F3FY3D3S.js.map +1 -0
  39. package/dist/{chunk-4R4KTDIE.js → chunk-FPNQF475.js} +1 -1
  40. package/dist/chunk-FPNQF475.js.map +1 -0
  41. package/dist/{chunk-UGEBPVNI.js → chunk-GE7Q7KXP.js} +2 -2
  42. package/dist/{chunk-GLWW3EJQ.js → chunk-KB4MFBF5.js} +3 -3
  43. package/dist/{chunk-5GOMXHLC.js → chunk-KKTXCFD7.js} +255 -1
  44. package/dist/chunk-KKTXCFD7.js.map +1 -0
  45. package/dist/{chunk-FH3PPO42.js → chunk-KVFYTRMV.js} +2 -2
  46. package/dist/{chunk-BNW5NJJH.js → chunk-LQYTQCXM.js} +2 -2
  47. package/dist/{chunk-AYHXQR53.js → chunk-MVQN73GT.js} +2 -2
  48. package/dist/{chunk-ZZPIJPPD.js → chunk-N5RGXWLQ.js} +2 -2
  49. package/dist/chunk-NDAH7BJ5.js +213 -0
  50. package/dist/chunk-NDAH7BJ5.js.map +1 -0
  51. package/dist/{chunk-R3OQGYOU.js → chunk-P2D2MM47.js} +2 -2
  52. package/dist/{chunk-PSUB67YB.js → chunk-PW6GURU3.js} +2 -2
  53. package/dist/{chunk-W3BKVM64.js → chunk-QDV6VAD4.js} +2 -2
  54. package/dist/{chunk-3QSU4NFF.js → chunk-QHXW3LZV.js} +3 -3
  55. package/dist/{chunk-I6UCUHLK.js → chunk-SHV5Y2WU.js} +182 -3
  56. package/dist/chunk-SHV5Y2WU.js.map +1 -0
  57. package/dist/{chunk-OZXVGYGZ.js → chunk-STDAAGH7.js} +2 -2
  58. package/dist/{chunk-FMGWXIES.js → chunk-TZDSNIRO.js} +5 -5
  59. package/dist/{chunk-2L54V4ZO.js → chunk-UELS6WWF.js} +2 -2
  60. package/dist/{chunk-PJGB7XRR.js → chunk-UGHUNQ74.js} +502 -134
  61. package/dist/chunk-UGHUNQ74.js.map +1 -0
  62. package/dist/{chunk-ZJSZNTEI.js → chunk-Y3TMFC6I.js} +140 -10
  63. package/dist/chunk-Y3TMFC6I.js.map +1 -0
  64. package/dist/{chunk-BPSGLMQ4.js → chunk-YQNADJCT.js} +2 -2
  65. package/dist/{cli-Cw729yLf.d.ts → cli-EZv6YE6_.d.ts} +3 -3
  66. package/dist/cli.d.ts +6 -6
  67. package/dist/cli.js +23 -21
  68. package/dist/compounding/engine.d.ts +1 -1
  69. package/dist/compounding/engine.js +3 -3
  70. package/dist/compounding/preference-consolidator.d.ts +1 -1
  71. package/dist/compression-optimizer.d.ts +1 -1
  72. package/dist/config.d.ts +1 -1
  73. package/dist/config.js +2 -1
  74. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  75. package/dist/connectors/codex-materialize-runner.js +3 -3
  76. package/dist/connectors/codex-materialize.d.ts +1 -1
  77. package/dist/connectors/index.d.ts +1 -1
  78. package/dist/connectors/index.js +3 -3
  79. package/dist/consolidation-provenance-check.d.ts +1 -1
  80. package/dist/consolidation-undo.d.ts +1 -1
  81. package/dist/contradiction/index.d.ts +2 -2
  82. package/dist/conversation-index/backend.d.ts +1 -1
  83. package/dist/conversation-index/chunker.d.ts +1 -1
  84. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  85. package/dist/conversation-index/indexer.d.ts +1 -1
  86. package/dist/conversation-index/search.d.ts +1 -1
  87. package/dist/day-summary.d.ts +1 -1
  88. package/dist/delinearize.d.ts +1 -1
  89. package/dist/direct-answer-wiring.d.ts +1 -1
  90. package/dist/direct-answer.d.ts +1 -1
  91. package/dist/embedding-fallback.d.ts +1 -1
  92. package/dist/enrichment/index.d.ts +1 -1
  93. package/dist/entity-retrieval.d.ts +1 -1
  94. package/dist/entity-retrieval.js +3 -3
  95. package/dist/entity-schema.d.ts +1 -1
  96. package/dist/explicit-capture.d.ts +4 -4
  97. package/dist/extraction-judge-telemetry.d.ts +1 -1
  98. package/dist/extraction-judge-training.d.ts +1 -1
  99. package/dist/extraction-judge.d.ts +1 -1
  100. package/dist/extraction.d.ts +1 -1
  101. package/dist/fallback-llm.d.ts +1 -1
  102. package/dist/identity-continuity.d.ts +1 -1
  103. package/dist/importance.d.ts +1 -1
  104. package/dist/index.d.ts +307 -9
  105. package/dist/index.js +155 -29
  106. package/dist/index.js.map +1 -1
  107. package/dist/intent.d.ts +1 -1
  108. package/dist/lcm/engine.d.ts +1 -1
  109. package/dist/lcm/index.d.ts +1 -1
  110. package/dist/lcm/tools.d.ts +1 -1
  111. package/dist/lifecycle.d.ts +1 -1
  112. package/dist/live-connectors-runner.d.ts +1 -1
  113. package/dist/local-llm.d.ts +1 -1
  114. package/dist/maintenance/memory-governance.d.ts +1 -1
  115. package/dist/maintenance/memory-governance.js +3 -3
  116. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  117. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  118. package/dist/mcp-memory-inspector-app.d.ts +5 -5
  119. package/dist/memory-action-policy.d.ts +1 -1
  120. package/dist/memory-cache.d.ts +1 -1
  121. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  122. package/dist/memory-projection-store.d.ts +1 -1
  123. package/dist/memory-provenance.d.ts +1 -1
  124. package/dist/memory-worth-outcomes.d.ts +1 -1
  125. package/dist/models-json.d.ts +1 -1
  126. package/dist/namespaces/migrate.d.ts +1 -1
  127. package/dist/namespaces/migrate.js +4 -4
  128. package/dist/namespaces/principal.d.ts +1 -1
  129. package/dist/namespaces/search.d.ts +1 -1
  130. package/dist/namespaces/storage.d.ts +1 -1
  131. package/dist/namespaces/storage.js +3 -3
  132. package/dist/native-knowledge.d.ts +1 -1
  133. package/dist/operator-toolkit.d.ts +1 -1
  134. package/dist/operator-toolkit.js +8 -7
  135. package/dist/{orchestrator-CqWOjfgl.d.ts → orchestrator-CEycaY3M.d.ts} +361 -4
  136. package/dist/orchestrator.d.ts +4 -4
  137. package/dist/orchestrator.js +13 -11
  138. package/dist/patterns-cli.d.ts +1 -1
  139. package/dist/policy-runtime.d.ts +1 -1
  140. package/dist/qmd-recall-cache.d.ts +1 -1
  141. package/dist/qmd.d.ts +1 -1
  142. package/dist/recall-disclosure-escalation.d.ts +1 -1
  143. package/dist/recall-explain-renderer.d.ts +1 -1
  144. package/dist/recall-explain-renderer.js +3 -3
  145. package/dist/recall-planner-llm.d.ts +1 -1
  146. package/dist/recall-state.d.ts +1 -1
  147. package/dist/recall-tag-filter.d.ts +1 -1
  148. package/dist/recall-xray-cli.d.ts +1 -1
  149. package/dist/recall-xray-cli.js +4 -4
  150. package/dist/recall-xray-renderer.d.ts +1 -1
  151. package/dist/recall-xray-renderer.js +3 -3
  152. package/dist/recall-xray.d.ts +1 -1
  153. package/dist/recall-xray.js +2 -2
  154. package/dist/resolve-auth-token.d.ts +1 -1
  155. package/dist/resume-bundles.js +3 -2
  156. package/dist/retrieval-agents.d.ts +1 -1
  157. package/dist/retrieval-tiers.d.ts +1 -1
  158. package/dist/routing/engine.d.ts +1 -1
  159. package/dist/routing/store.d.ts +1 -1
  160. package/dist/schemas.d.ts +32 -32
  161. package/dist/search/embed-helper.d.ts +1 -1
  162. package/dist/search/factory.d.ts +1 -1
  163. package/dist/search/index.d.ts +1 -1
  164. package/dist/search/lancedb-backend.d.ts +1 -1
  165. package/dist/search/meilisearch-backend.d.ts +1 -1
  166. package/dist/search/noop-backend.d.ts +1 -1
  167. package/dist/search/orama-backend.d.ts +1 -1
  168. package/dist/search/port.d.ts +1 -1
  169. package/dist/search/remote-backend.d.ts +1 -1
  170. package/dist/{semantic-SLAa_prH.d.ts → semantic-DJR8_DMQ.d.ts} +1 -1
  171. package/dist/{semantic-consolidation-4HkHWgeI.d.ts → semantic-consolidation-FbhPeJjB.d.ts} +1 -1
  172. package/dist/semantic-consolidation.d.ts +2 -2
  173. package/dist/semantic-consolidation.js +4 -4
  174. package/dist/semantic-rule-promotion.js +3 -3
  175. package/dist/semantic-rule-verifier.d.ts +1 -1
  176. package/dist/semantic-rule-verifier.js +3 -3
  177. package/dist/session-observer-bands.d.ts +1 -1
  178. package/dist/session-observer-state.d.ts +1 -1
  179. package/dist/shared-context/manager.d.ts +5 -5
  180. package/dist/signal.d.ts +1 -1
  181. package/dist/storage.d.ts +19 -1
  182. package/dist/storage.js +2 -2
  183. package/dist/summarizer.d.ts +1 -1
  184. package/dist/summary-snapshot.d.ts +1 -1
  185. package/dist/temporal-supersession.d.ts +1 -1
  186. package/dist/temporal-validity.d.ts +1 -1
  187. package/dist/threading.d.ts +1 -1
  188. package/dist/tier-migration.d.ts +1 -1
  189. package/dist/tier-routing.d.ts +1 -1
  190. package/dist/topics.d.ts +1 -1
  191. package/dist/transcript.d.ts +1 -1
  192. package/dist/transfer/types.d.ts +12 -12
  193. package/dist/types-D5VRAI04.d.ts +3134 -0
  194. package/dist/types.d.ts +3 -2862
  195. package/dist/types.js +1 -1
  196. package/dist/utility-runtime.d.ts +1 -1
  197. package/dist/verified-recall.js +3 -3
  198. package/package.json +1 -1
  199. package/src/access-http.ts +182 -8
  200. package/src/access-mcp.ts +198 -0
  201. package/src/access-service.ts +65 -0
  202. package/src/cli.ts +187 -0
  203. package/src/config.ts +7 -0
  204. package/src/index.ts +7 -0
  205. package/src/orchestrator.ts +42 -0
  206. package/src/storage.ts +106 -0
  207. package/src/types.ts +5 -0
  208. package/src/wearables/cleanup.test.ts +134 -0
  209. package/src/wearables/cleanup.ts +188 -0
  210. package/src/wearables/cli.test.ts +170 -0
  211. package/src/wearables/cli.ts +441 -0
  212. package/src/wearables/config.test.ts +143 -0
  213. package/src/wearables/config.ts +332 -0
  214. package/src/wearables/corrections.test.ts +118 -0
  215. package/src/wearables/corrections.ts +211 -0
  216. package/src/wearables/day-store.test.ts +143 -0
  217. package/src/wearables/day-store.ts +238 -0
  218. package/src/wearables/errors.test.ts +32 -0
  219. package/src/wearables/errors.ts +29 -0
  220. package/src/wearables/index.ts +114 -0
  221. package/src/wearables/memory-gen.test.ts +342 -0
  222. package/src/wearables/memory-gen.ts +413 -0
  223. package/src/wearables/pipeline.test.ts +608 -0
  224. package/src/wearables/pipeline.ts +519 -0
  225. package/src/wearables/redaction.test.ts +94 -0
  226. package/src/wearables/redaction.ts +156 -0
  227. package/src/wearables/registry.test.ts +62 -0
  228. package/src/wearables/registry.ts +133 -0
  229. package/src/wearables/service.test.ts +425 -0
  230. package/src/wearables/service.ts +691 -0
  231. package/src/wearables/speakers.test.ts +110 -0
  232. package/src/wearables/speakers.ts +174 -0
  233. package/src/wearables/storage-io.test.ts +105 -0
  234. package/src/wearables/sync-state.test.ts +134 -0
  235. package/src/wearables/sync-state.ts +186 -0
  236. package/src/wearables/types.ts +285 -0
  237. package/dist/chunk-4R4KTDIE.js.map +0 -1
  238. package/dist/chunk-5GOMXHLC.js.map +0 -1
  239. package/dist/chunk-C4PZTWTG.js.map +0 -1
  240. package/dist/chunk-I6UCUHLK.js.map +0 -1
  241. package/dist/chunk-KQFQ3IS5.js.map +0 -1
  242. package/dist/chunk-PJGB7XRR.js.map +0 -1
  243. package/dist/chunk-RKW6QR7W.js.map +0 -1
  244. package/dist/chunk-ZJSZNTEI.js.map +0 -1
  245. /package/dist/{chunk-GYTVOLNX.js.map → chunk-3MNBW7R7.js.map} +0 -0
  246. /package/dist/{chunk-QFQQFX2H.js.map → chunk-3R2UZV3U.js.map} +0 -0
  247. /package/dist/{chunk-O4UNM6OR.js.map → chunk-532VCWYW.js.map} +0 -0
  248. /package/dist/{chunk-2UFQYU5F.js.map → chunk-57QXN2CS.js.map} +0 -0
  249. /package/dist/{chunk-UGEBPVNI.js.map → chunk-GE7Q7KXP.js.map} +0 -0
  250. /package/dist/{chunk-GLWW3EJQ.js.map → chunk-KB4MFBF5.js.map} +0 -0
  251. /package/dist/{chunk-FH3PPO42.js.map → chunk-KVFYTRMV.js.map} +0 -0
  252. /package/dist/{chunk-BNW5NJJH.js.map → chunk-LQYTQCXM.js.map} +0 -0
  253. /package/dist/{chunk-AYHXQR53.js.map → chunk-MVQN73GT.js.map} +0 -0
  254. /package/dist/{chunk-ZZPIJPPD.js.map → chunk-N5RGXWLQ.js.map} +0 -0
  255. /package/dist/{chunk-R3OQGYOU.js.map → chunk-P2D2MM47.js.map} +0 -0
  256. /package/dist/{chunk-PSUB67YB.js.map → chunk-PW6GURU3.js.map} +0 -0
  257. /package/dist/{chunk-W3BKVM64.js.map → chunk-QDV6VAD4.js.map} +0 -0
  258. /package/dist/{chunk-3QSU4NFF.js.map → chunk-QHXW3LZV.js.map} +0 -0
  259. /package/dist/{chunk-OZXVGYGZ.js.map → chunk-STDAAGH7.js.map} +0 -0
  260. /package/dist/{chunk-FMGWXIES.js.map → chunk-TZDSNIRO.js.map} +0 -0
  261. /package/dist/{chunk-2L54V4ZO.js.map → chunk-UELS6WWF.js.map} +0 -0
  262. /package/dist/{chunk-BPSGLMQ4.js.map → chunk-YQNADJCT.js.map} +0 -0
@@ -0,0 +1,110 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import * as path from "node:path";
5
+ import { test } from "node:test";
6
+
7
+ import {
8
+ DEFAULT_SELF_NAME,
9
+ distinctSpeakerLabels,
10
+ emptySpeakerRegistry,
11
+ loadSpeakerRegistry,
12
+ resolveSpeaker,
13
+ saveSpeakerRegistry,
14
+ speakerRegistryKey,
15
+ } from "./speakers.js";
16
+
17
+ test("resolution precedence: override > wearer flag > provider name > raw key", () => {
18
+ const registry = emptySpeakerRegistry();
19
+ registry.selfName = "Jordan";
20
+ registry.speakers[speakerRegistryKey("bee", "1")] = {
21
+ name: "Alex Sample",
22
+ updatedAt: "2026-06-10T00:00:00Z",
23
+ };
24
+
25
+ // 1. Registry override wins even over a provider name.
26
+ assert.deepEqual(
27
+ resolveSpeaker("bee", { speakerKey: "1", speakerName: "Speaker 1" }, registry),
28
+ { label: "Alex Sample", isSelf: false },
29
+ );
30
+ // 2. Provider wearer flag.
31
+ assert.deepEqual(
32
+ resolveSpeaker("limitless", { speakerKey: "user", isWearer: true }, registry),
33
+ { label: "Jordan (you)", isSelf: true },
34
+ );
35
+ // 3. Provider display name.
36
+ assert.deepEqual(
37
+ resolveSpeaker("limitless", { speakerKey: "Speaker 2", speakerName: "Speaker 2" }, registry),
38
+ { label: "Speaker 2", isSelf: false },
39
+ );
40
+ // 4. Raw key fallbacks.
41
+ assert.deepEqual(resolveSpeaker("bee", { speakerKey: "0" }, registry), {
42
+ label: "Speaker 0",
43
+ isSelf: false,
44
+ });
45
+ assert.deepEqual(resolveSpeaker("omi", { speakerKey: "" }, registry), {
46
+ label: "Unknown speaker",
47
+ isSelf: false,
48
+ });
49
+ });
50
+
51
+ test("an override can mark a diarization label as the wearer", () => {
52
+ const registry = emptySpeakerRegistry();
53
+ registry.speakers[speakerRegistryKey("bee", "0")] = {
54
+ name: "Jordan",
55
+ isSelf: true,
56
+ updatedAt: "2026-06-10T00:00:00Z",
57
+ };
58
+ assert.deepEqual(resolveSpeaker("bee", { speakerKey: "0" }, registry), {
59
+ label: "Jordan (you)",
60
+ isSelf: true,
61
+ });
62
+ });
63
+
64
+ test("distinctSpeakerLabels keeps first-appearance order without duplicates", () => {
65
+ const registry = emptySpeakerRegistry();
66
+ const labels = distinctSpeakerLabels(
67
+ "limitless",
68
+ [
69
+ { speakerKey: "user", isWearer: true },
70
+ { speakerKey: "Speaker 2", speakerName: "Speaker 2" },
71
+ { speakerKey: "user", isWearer: true },
72
+ ],
73
+ registry,
74
+ );
75
+ assert.deepEqual(labels, [`${DEFAULT_SELF_NAME} (you)`, "Speaker 2"]);
76
+ });
77
+
78
+ test("registry round-trips through disk and tolerates absence", async () => {
79
+ const dir = mkdtempSync(path.join(tmpdir(), "remnic-speakers-"));
80
+ try {
81
+ const fresh = await loadSpeakerRegistry(dir);
82
+ assert.equal(fresh.selfName, DEFAULT_SELF_NAME);
83
+
84
+ fresh.selfName = "Jordan";
85
+ fresh.speakers[speakerRegistryKey("omi", "SPEAKER_01")] = {
86
+ name: "Casey Sample",
87
+ updatedAt: "2026-06-10T00:00:00Z",
88
+ };
89
+ await saveSpeakerRegistry(dir, fresh);
90
+
91
+ const loaded = await loadSpeakerRegistry(dir);
92
+ assert.equal(loaded.selfName, "Jordan");
93
+ assert.equal(loaded.speakers["omi:SPEAKER_01"].name, "Casey Sample");
94
+ } finally {
95
+ rmSync(dir, { recursive: true, force: true });
96
+ }
97
+ });
98
+
99
+ test("a malformed speakers file throws instead of silently resetting", async () => {
100
+ const dir = mkdtempSync(path.join(tmpdir(), "remnic-speakers-"));
101
+ try {
102
+ const { promises: fsPromises } = await import("node:fs");
103
+ const filePath = path.join(dir, "state", "wearables", "speakers.json");
104
+ await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
105
+ await fsPromises.writeFile(filePath, JSON.stringify({ speakers: null }), "utf-8");
106
+ await assert.rejects(loadSpeakerRegistry(dir), /unexpected shape/);
107
+ } finally {
108
+ rmSync(dir, { recursive: true, force: true });
109
+ }
110
+ });
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Wearable speaker registry — maps provider diarization labels to
3
+ * human names, persistently, per source.
4
+ *
5
+ * Providers expose speakers differently (Limitless: names + a "user"
6
+ * marker; Bee: opaque labels like "0"/"1"; Omi: "SPEAKER_00" + is_user
7
+ * + optional person ids). The registry stores operator-confirmed
8
+ * mappings keyed `<sourceId>:<speakerKey>` in
9
+ * `state/wearables/speakers.json`, plus the wearer's display name.
10
+ *
11
+ * Resolution precedence (most-authoritative first):
12
+ * 1. registry override for `<sourceId>:<speakerKey>`
13
+ * 2. provider-identified wearer -> selfName
14
+ * 3. provider-supplied speaker name
15
+ * 4. the raw speaker key, prefixed "Speaker" when it is bare digits
16
+ */
17
+
18
+ import { promises as fsPromises } from "node:fs";
19
+ import * as path from "node:path";
20
+
21
+ export interface SpeakerOverride {
22
+ name: string;
23
+ /** Mark this speaker as the wearer (their words become "you"). */
24
+ isSelf?: boolean;
25
+ updatedAt: string;
26
+ }
27
+
28
+ export interface SpeakerRegistry {
29
+ version: 1;
30
+ /** Display name used for the wearer across all sources. */
31
+ selfName: string;
32
+ /** Overrides keyed `<sourceId>:<speakerKey>`. */
33
+ speakers: Record<string, SpeakerOverride>;
34
+ }
35
+
36
+ export const DEFAULT_SELF_NAME = "Me";
37
+
38
+ export function emptySpeakerRegistry(): SpeakerRegistry {
39
+ return { version: 1, selfName: DEFAULT_SELF_NAME, speakers: {} };
40
+ }
41
+
42
+ export function speakersFilePath(memoryDir: string): string {
43
+ return path.join(memoryDir, "state", "wearables", "speakers.json");
44
+ }
45
+
46
+ export async function loadSpeakerRegistry(
47
+ memoryDir: string,
48
+ ): Promise<SpeakerRegistry> {
49
+ const filePath = speakersFilePath(memoryDir);
50
+ let raw: string;
51
+ try {
52
+ raw = await fsPromises.readFile(filePath, "utf-8");
53
+ } catch (err) {
54
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
55
+ return emptySpeakerRegistry();
56
+ }
57
+ throw err;
58
+ }
59
+ let parsed: unknown;
60
+ try {
61
+ parsed = JSON.parse(raw);
62
+ } catch (err) {
63
+ throw new Error(
64
+ `wearables speakers file is not valid JSON (state/wearables/speakers.json): ${
65
+ err instanceof Error ? err.message : String(err)
66
+ }`,
67
+ );
68
+ }
69
+ if (
70
+ typeof parsed !== "object" ||
71
+ parsed === null ||
72
+ Array.isArray(parsed) ||
73
+ typeof (parsed as SpeakerRegistry).speakers !== "object" ||
74
+ (parsed as SpeakerRegistry).speakers === null
75
+ ) {
76
+ throw new Error(
77
+ 'wearables speakers file has an unexpected shape (state/wearables/speakers.json); expected {"version":1,"selfName":"...","speakers":{}}',
78
+ );
79
+ }
80
+ const registry = parsed as SpeakerRegistry;
81
+ return {
82
+ version: 1,
83
+ selfName:
84
+ typeof registry.selfName === "string" && registry.selfName.trim().length > 0
85
+ ? registry.selfName.trim()
86
+ : DEFAULT_SELF_NAME,
87
+ speakers: registry.speakers,
88
+ };
89
+ }
90
+
91
+ export async function saveSpeakerRegistry(
92
+ memoryDir: string,
93
+ registry: SpeakerRegistry,
94
+ ): Promise<void> {
95
+ const filePath = speakersFilePath(memoryDir);
96
+ await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
97
+ const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now().toString(36)}`;
98
+ await fsPromises.writeFile(
99
+ tmpPath,
100
+ `${JSON.stringify(registry, null, 2)}\n`,
101
+ "utf-8",
102
+ );
103
+ try {
104
+ await fsPromises.rename(tmpPath, filePath);
105
+ } catch (err) {
106
+ await fsPromises.unlink(tmpPath).catch(() => undefined);
107
+ throw err;
108
+ }
109
+ }
110
+
111
+ export function speakerRegistryKey(
112
+ sourceId: string,
113
+ speakerKey: string,
114
+ ): string {
115
+ return `${sourceId}:${speakerKey}`;
116
+ }
117
+
118
+ export interface ResolvedSpeaker {
119
+ /** Display label used in transcripts, e.g. "Jane" or "Me (you)". */
120
+ label: string;
121
+ /** Whether this speaker is the wearer. */
122
+ isSelf: boolean;
123
+ }
124
+
125
+ export function resolveSpeaker(
126
+ sourceId: string,
127
+ segment: { speakerKey: string; speakerName?: string; isWearer?: boolean },
128
+ registry: SpeakerRegistry,
129
+ ): ResolvedSpeaker {
130
+ const override = registry.speakers[speakerRegistryKey(sourceId, segment.speakerKey)];
131
+ if (override) {
132
+ const isSelf = override.isSelf === true;
133
+ return {
134
+ label: isSelf ? `${override.name} (you)` : override.name,
135
+ isSelf,
136
+ };
137
+ }
138
+ if (segment.isWearer === true) {
139
+ return { label: `${registry.selfName} (you)`, isSelf: true };
140
+ }
141
+ if (
142
+ typeof segment.speakerName === "string" &&
143
+ segment.speakerName.trim().length > 0
144
+ ) {
145
+ return { label: segment.speakerName.trim(), isSelf: false };
146
+ }
147
+ const key = segment.speakerKey.trim();
148
+ // Bare diarization indexes read better with a prefix.
149
+ if (/^\d+$/.test(key)) {
150
+ return { label: `Speaker ${key}`, isSelf: false };
151
+ }
152
+ return { label: key.length > 0 ? key : "Unknown speaker", isSelf: false };
153
+ }
154
+
155
+ /**
156
+ * Distinct speaker labels for a set of segments, in first-appearance
157
+ * order — used for day-transcript frontmatter.
158
+ */
159
+ export function distinctSpeakerLabels(
160
+ sourceId: string,
161
+ segments: Array<{ speakerKey: string; speakerName?: string; isWearer?: boolean }>,
162
+ registry: SpeakerRegistry,
163
+ ): string[] {
164
+ const labels: string[] = [];
165
+ const seen = new Set<string>();
166
+ for (const segment of segments) {
167
+ const { label } = resolveSpeaker(sourceId, segment, registry);
168
+ if (!seen.has(label)) {
169
+ seen.add(label);
170
+ labels.push(label);
171
+ }
172
+ }
173
+ return labels;
174
+ }
@@ -0,0 +1,105 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import * as path from "node:path";
5
+ import { test } from "node:test";
6
+
7
+ import { StorageManager } from "../storage.js";
8
+
9
+ function makeStorage(): { storage: StorageManager; dir: string } {
10
+ const dir = mkdtempSync(path.join(tmpdir(), "remnic-wearable-io-"));
11
+ return { storage: new StorageManager(dir), dir };
12
+ }
13
+
14
+ test("wearable transcript write/read/list round-trips", async () => {
15
+ const { storage, dir } = makeStorage();
16
+ try {
17
+ assert.equal(await storage.readWearableDayTranscript("limitless", "2026-06-10"), null);
18
+ assert.deepEqual(await storage.listWearableTranscriptDays(), []);
19
+
20
+ await storage.writeWearableDayTranscript("limitless", "2026-06-10", "---\nkind: wearable-transcript\n---\n\nbody A\n");
21
+ await storage.writeWearableDayTranscript("limitless", "2026-06-11", "---\nkind: wearable-transcript\n---\n\nbody B\n");
22
+ await storage.writeWearableDayTranscript("bee", "2026-06-11", "---\nkind: wearable-transcript\n---\n\nbody C\n");
23
+
24
+ const raw = await storage.readWearableDayTranscript("limitless", "2026-06-10");
25
+ assert.ok(raw?.includes("body A"));
26
+
27
+ const allDays = await storage.listWearableTranscriptDays();
28
+ assert.deepEqual(allDays, [
29
+ { source: "bee", date: "2026-06-11" },
30
+ { source: "limitless", date: "2026-06-11" },
31
+ { source: "limitless", date: "2026-06-10" },
32
+ ]);
33
+ const scoped = await storage.listWearableTranscriptDays("limitless");
34
+ assert.equal(scoped.length, 2);
35
+ assert.ok(scoped.every((entry) => entry.source === "limitless"));
36
+ } finally {
37
+ rmSync(dir, { recursive: true, force: true });
38
+ }
39
+ });
40
+
41
+ test("rewriting a day replaces content atomically", async () => {
42
+ const { storage, dir } = makeStorage();
43
+ try {
44
+ await storage.writeWearableDayTranscript("omi", "2026-06-10", "first version\n");
45
+ await storage.writeWearableDayTranscript("omi", "2026-06-10", "second version\n");
46
+ const raw = await storage.readWearableDayTranscript("omi", "2026-06-10");
47
+ assert.equal(raw, "second version\n");
48
+ assert.equal((await storage.listWearableTranscriptDays("omi")).length, 1);
49
+ } finally {
50
+ rmSync(dir, { recursive: true, force: true });
51
+ }
52
+ });
53
+
54
+ test("malicious source ids and dates are rejected before any path math", async () => {
55
+ const { storage, dir } = makeStorage();
56
+ try {
57
+ await assert.rejects(
58
+ storage.writeWearableDayTranscript("../escape", "2026-06-10", "x"),
59
+ /invalid wearable source id/,
60
+ );
61
+ await assert.rejects(
62
+ storage.readWearableDayTranscript("limitless", "../../etc/passwd"),
63
+ /invalid wearable transcript date/,
64
+ );
65
+ await assert.rejects(
66
+ storage.readWearableDayTranscript("Limitless", "2026-06-10"),
67
+ /invalid wearable source id/,
68
+ );
69
+ assert.throws(
70
+ () => storage.wearableTranscriptPath("a/b", "2026-06-10"),
71
+ /invalid wearable source id/,
72
+ );
73
+ } finally {
74
+ rmSync(dir, { recursive: true, force: true });
75
+ }
76
+ });
77
+
78
+ test("transcript files never surface from readAllMemories", async () => {
79
+ const { storage, dir } = makeStorage();
80
+ try {
81
+ await storage.writeWearableDayTranscript(
82
+ "limitless",
83
+ "2026-06-10",
84
+ "---\nkind: wearable-transcript\nsource: \"limitless\"\n---\n\ntranscript body\n",
85
+ );
86
+ const memories = await storage.readAllMemories();
87
+ assert.equal(memories.length, 0, "wearables/ must stay outside memory scan roots");
88
+ } finally {
89
+ rmSync(dir, { recursive: true, force: true });
90
+ }
91
+ });
92
+
93
+ test("non-transcript files in the wearables tree are ignored by listing", async () => {
94
+ const { storage, dir } = makeStorage();
95
+ try {
96
+ const { promises: fsPromises } = await import("node:fs");
97
+ await fsPromises.mkdir(path.join(dir, "wearables", "limitless"), { recursive: true });
98
+ await fsPromises.writeFile(path.join(dir, "wearables", "limitless", "notes.md"), "x", "utf-8");
99
+ await fsPromises.writeFile(path.join(dir, "wearables", "limitless", "2026-06-10.md.tmp"), "x", "utf-8");
100
+ await fsPromises.mkdir(path.join(dir, "wearables", "NotASource"), { recursive: true });
101
+ assert.deepEqual(await storage.listWearableTranscriptDays(), []);
102
+ } finally {
103
+ rmSync(dir, { recursive: true, force: true });
104
+ }
105
+ });
@@ -0,0 +1,134 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import * as path from "node:path";
5
+ import { test } from "node:test";
6
+
7
+ import {
8
+ emptySyncState,
9
+ loadSyncState,
10
+ saveSyncState,
11
+ syncStateFilePath,
12
+ updateSourceSyncState,
13
+ } from "./sync-state.js";
14
+
15
+ test("round-trips through disk and tolerates absence", async () => {
16
+ const dir = mkdtempSync(path.join(tmpdir(), "remnic-syncstate-"));
17
+ try {
18
+ assert.deepEqual(await loadSyncState(dir), emptySyncState());
19
+ const updated = updateSourceSyncState(emptySyncState(), "limitless", {
20
+ syncedAt: "2026-06-11T01:00:00.000Z",
21
+ days: ["2026-06-10", "2026-06-11"],
22
+ dayHashes: { "2026-06-10": "aaa", "2026-06-11": "bbb" },
23
+ importedNativeMemoryIds: ["n1", "n2"],
24
+ });
25
+ await saveSyncState(dir, updated);
26
+ const loaded = await loadSyncState(dir);
27
+ assert.equal(loaded.sources.limitless.lastDateSynced, "2026-06-11");
28
+ assert.equal(loaded.sources.limitless.dayHashes["2026-06-10"], "aaa");
29
+ assert.deepEqual(loaded.sources.limitless.importedNativeMemoryIds, ["n1", "n2"]);
30
+ } finally {
31
+ rmSync(dir, { recursive: true, force: true });
32
+ }
33
+ });
34
+
35
+ test("a corrupt state file cold-starts instead of bricking sync", async () => {
36
+ const dir = mkdtempSync(path.join(tmpdir(), "remnic-syncstate-"));
37
+ try {
38
+ const { promises: fsPromises } = await import("node:fs");
39
+ const filePath = syncStateFilePath(dir);
40
+ await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
41
+ await fsPromises.writeFile(filePath, "{broken", "utf-8");
42
+ assert.deepEqual(await loadSyncState(dir), emptySyncState());
43
+ await fsPromises.writeFile(filePath, "null", "utf-8");
44
+ assert.deepEqual(await loadSyncState(dir), emptySyncState());
45
+ } finally {
46
+ rmSync(dir, { recursive: true, force: true });
47
+ }
48
+ });
49
+
50
+ test("lastDateSynced never moves backwards", () => {
51
+ let state = updateSourceSyncState(emptySyncState(), "bee", {
52
+ syncedAt: "2026-06-11T01:00:00.000Z",
53
+ days: ["2026-06-11"],
54
+ dayHashes: { "2026-06-11": "x" },
55
+ });
56
+ state = updateSourceSyncState(state, "bee", {
57
+ syncedAt: "2026-06-12T01:00:00.000Z",
58
+ days: ["2026-06-01"],
59
+ dayHashes: { "2026-06-01": "y" },
60
+ });
61
+ assert.equal(state.sources.bee.lastDateSynced, "2026-06-11");
62
+ assert.equal(state.sources.bee.lastSyncAt, "2026-06-12T01:00:00.000Z");
63
+ });
64
+
65
+ test("native memory ids are deduplicated and bounded", () => {
66
+ const manyIds = Array.from({ length: 6_000 }, (_, index) => `id-${index}`);
67
+ let state = updateSourceSyncState(emptySyncState(), "omi", {
68
+ syncedAt: "2026-06-11T01:00:00.000Z",
69
+ days: [],
70
+ dayHashes: {},
71
+ importedNativeMemoryIds: manyIds,
72
+ });
73
+ state = updateSourceSyncState(state, "omi", {
74
+ syncedAt: "2026-06-11T02:00:00.000Z",
75
+ days: [],
76
+ dayHashes: {},
77
+ importedNativeMemoryIds: ["id-5999", "fresh"],
78
+ });
79
+ const ids = state.sources.omi.importedNativeMemoryIds;
80
+ assert.ok(ids.length <= 5_000);
81
+ assert.equal(new Set(ids).size, ids.length, "ids must be unique");
82
+ assert.ok(ids.includes("fresh"));
83
+ assert.ok(ids.includes("id-5999"));
84
+ });
85
+
86
+ test("clearMemoryDays removes stale completion records", () => {
87
+ let state = updateSourceSyncState(emptySyncState(), "limitless", {
88
+ syncedAt: "2026-06-11T01:00:00.000Z",
89
+ days: ["2026-06-10"],
90
+ dayHashes: { "2026-06-10": "h1" },
91
+ memoryDayHashes: { "2026-06-10": "h1" },
92
+ });
93
+ assert.equal(state.sources.limitless.memoryDayHashes?.["2026-06-10"], "h1");
94
+
95
+ // A later run where the day's memory pass failed must clear the
96
+ // earlier completion record even though the body hash is unchanged.
97
+ state = updateSourceSyncState(state, "limitless", {
98
+ syncedAt: "2026-06-12T01:00:00.000Z",
99
+ days: ["2026-06-10"],
100
+ dayHashes: { "2026-06-10": "h1" },
101
+ clearMemoryDays: ["2026-06-10"],
102
+ });
103
+ assert.equal(state.sources.limitless.memoryDayHashes?.["2026-06-10"], undefined);
104
+
105
+ // A clear for a day that completed in the same run is a no-op.
106
+ state = updateSourceSyncState(state, "limitless", {
107
+ syncedAt: "2026-06-13T01:00:00.000Z",
108
+ days: ["2026-06-10"],
109
+ dayHashes: { "2026-06-10": "h1" },
110
+ memoryDayHashes: { "2026-06-10": "h1" },
111
+ clearMemoryDays: ["2026-06-10"],
112
+ });
113
+ assert.equal(state.sources.limitless.memoryDayHashes?.["2026-06-10"], "h1");
114
+ });
115
+
116
+ test("day hashes are bounded to the most recent dates", () => {
117
+ const hashes: Record<string, string> = {};
118
+ for (let index = 0; index < 900; index++) {
119
+ const day = new Date(Date.UTC(2024, 0, 1) + index * 86_400_000)
120
+ .toISOString()
121
+ .slice(0, 10);
122
+ hashes[day] = `h${index}`;
123
+ }
124
+ const state = updateSourceSyncState(emptySyncState(), "limitless", {
125
+ syncedAt: "2026-06-11T01:00:00.000Z",
126
+ days: [],
127
+ dayHashes: hashes,
128
+ });
129
+ const keys = Object.keys(state.sources.limitless.dayHashes).sort();
130
+ assert.equal(keys.length, 800);
131
+ // The oldest dates were evicted, the newest retained.
132
+ assert.equal(keys.includes("2024-01-01"), false);
133
+ assert.ok(keys[keys.length - 1] > "2026-01-01");
134
+ });