@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,143 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+
4
+ import {
5
+ composeDayTranscriptBody,
6
+ composeDayTranscriptMeta,
7
+ hashTranscriptBody,
8
+ isValidTranscriptDate,
9
+ parseDayTranscript,
10
+ serializeDayTranscript,
11
+ } from "./day-store.js";
12
+ import { emptySpeakerRegistry } from "./speakers.js";
13
+ import type { WearableConversation } from "./types.js";
14
+
15
+ const REGISTRY = emptySpeakerRegistry();
16
+
17
+ const CONVERSATIONS: WearableConversation[] = [
18
+ {
19
+ id: "conv-2",
20
+ source: "limitless",
21
+ title: "Afternoon sync",
22
+ startIso: "2026-06-10T14:00:00-05:00",
23
+ endIso: "2026-06-10T14:30:00-05:00",
24
+ segments: [
25
+ { speakerKey: "user", isWearer: true, text: "Let's lock the agenda.", startIso: "2026-06-10T14:01:00-05:00" },
26
+ ],
27
+ },
28
+ {
29
+ id: "conv-1",
30
+ source: "limitless",
31
+ title: "Morning coffee",
32
+ startIso: "2026-06-10T09:00:00-05:00",
33
+ endIso: "2026-06-10T09:20:00-05:00",
34
+ location: "Coffee shop",
35
+ segments: [
36
+ { speakerKey: "Speaker 2", speakerName: "Speaker 2", text: "Try the east loop trail." },
37
+ { speakerKey: "user", isWearer: true, text: "I will this weekend." },
38
+ ],
39
+ },
40
+ ];
41
+
42
+ test("isValidTranscriptDate accepts real dates and rejects everything else", () => {
43
+ assert.equal(isValidTranscriptDate("2026-06-10"), true);
44
+ assert.equal(isValidTranscriptDate("2026-13-01"), false);
45
+ assert.equal(isValidTranscriptDate("2026-02-30"), false);
46
+ assert.equal(isValidTranscriptDate("06/10/2026"), false);
47
+ assert.equal(isValidTranscriptDate("../etc/passwd"), false);
48
+ });
49
+
50
+ test("body orders conversations chronologically with local clock times", () => {
51
+ const body = composeDayTranscriptBody(
52
+ "limitless",
53
+ "2026-06-10",
54
+ "America/Chicago",
55
+ CONVERSATIONS,
56
+ REGISTRY,
57
+ );
58
+ const morningIndex = body.indexOf("Morning coffee");
59
+ const afternoonIndex = body.indexOf("Afternoon sync");
60
+ assert.ok(morningIndex !== -1 && afternoonIndex !== -1);
61
+ assert.ok(morningIndex < afternoonIndex, "expected chronological order");
62
+ assert.match(body, /## 09:00–09:20 · Morning coffee \(conversation conv-1\)/);
63
+ // Segments without timestamps render the --:-- placeholder.
64
+ assert.match(body, /\*\*Me \(you\)\*\* \[--:--\]: I will this weekend\./);
65
+ // Segments with timestamps render a local clock time.
66
+ assert.match(body, /\*\*Me \(you\)\*\* \[14:01\]: Let's lock the agenda\./);
67
+ assert.match(body, /\*Location: Coffee shop\*/);
68
+ });
69
+
70
+ test("composition is deterministic (same input → same hash)", () => {
71
+ const compose = () =>
72
+ composeDayTranscriptBody("limitless", "2026-06-10", "UTC", CONVERSATIONS, REGISTRY);
73
+ assert.equal(hashTranscriptBody(compose()), hashTranscriptBody(compose()));
74
+ });
75
+
76
+ test("serialize → parse round-trips meta and body", () => {
77
+ const body = composeDayTranscriptBody(
78
+ "limitless",
79
+ "2026-06-10",
80
+ "America/Chicago",
81
+ CONVERSATIONS,
82
+ REGISTRY,
83
+ );
84
+ const meta = composeDayTranscriptMeta(
85
+ "limitless",
86
+ "2026-06-10",
87
+ "America/Chicago",
88
+ CONVERSATIONS,
89
+ REGISTRY,
90
+ body,
91
+ "2026-06-11T01:00:00.000Z",
92
+ );
93
+ const parsed = parseDayTranscript(serializeDayTranscript(meta, body));
94
+ assert.ok(parsed, "expected parseDayTranscript to succeed");
95
+ assert.deepEqual(parsed.meta, meta);
96
+ assert.equal(parsed.body, body);
97
+ assert.equal(parsed.meta.conversationCount, 2);
98
+ assert.equal(parsed.meta.segmentCount, 3);
99
+ assert.equal(parsed.meta.durationMinutes, 50);
100
+ assert.equal(parsed.meta.contentHash, hashTranscriptBody(body));
101
+ });
102
+
103
+ test("speakers list survives serialization including special characters", () => {
104
+ const registry = emptySpeakerRegistry();
105
+ registry.selfName = 'J "Quotes" O\'Sample: tester';
106
+ const conversations: WearableConversation[] = [
107
+ {
108
+ id: "c",
109
+ source: "bee",
110
+ startIso: "2026-06-10T08:00:00Z",
111
+ segments: [{ speakerKey: "0", isWearer: true, text: "hi there friend" }],
112
+ },
113
+ ];
114
+ const body = composeDayTranscriptBody("bee", "2026-06-10", "UTC", conversations, registry);
115
+ const meta = composeDayTranscriptMeta(
116
+ "bee",
117
+ "2026-06-10",
118
+ "UTC",
119
+ conversations,
120
+ registry,
121
+ body,
122
+ "2026-06-11T01:00:00.000Z",
123
+ );
124
+ const parsed = parseDayTranscript(serializeDayTranscript(meta, body));
125
+ assert.ok(parsed);
126
+ assert.deepEqual(parsed.meta.speakers, [`J "Quotes" O'Sample: tester (you)`]);
127
+ });
128
+
129
+ test("parseDayTranscript returns null for non-transcript content", () => {
130
+ assert.equal(parseDayTranscript("# just markdown\n"), null);
131
+ assert.equal(parseDayTranscript("---\nid: fact-1\ncategory: fact\n---\n\nx\n"), null);
132
+ });
133
+
134
+ test("invalid timezone falls back to UTC clock rendering instead of crashing", () => {
135
+ const body = composeDayTranscriptBody(
136
+ "limitless",
137
+ "2026-06-10",
138
+ "Not/AZone",
139
+ CONVERSATIONS,
140
+ REGISTRY,
141
+ );
142
+ assert.match(body, /## \d{2}:\d{2}–\d{2}:\d{2} · Morning coffee/);
143
+ });
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Wearable day-transcript composition and parsing.
3
+ *
4
+ * One markdown file per source per day, stored under
5
+ * `<memoryDir>/wearables/<source>/<YYYY-MM-DD>.md` with YAML
6
+ * frontmatter. The location is deliberate:
7
+ *
8
+ * - it is OUTSIDE the memory scan roots (facts/, procedures/,
9
+ * reasoning-traces/, corrections/), so transcripts never appear as
10
+ * memories in recall or governance passes;
11
+ * - it is INSIDE the QMD collection root (the memory dir), so day
12
+ * transcripts are full-text searchable after the next index update.
13
+ *
14
+ * Files are rebuilt idempotently from provider data on every sync; the
15
+ * body hash in frontmatter lets the pipeline skip rewriting (and
16
+ * re-extracting) unchanged days.
17
+ *
18
+ * This module is pure composition/parsing — file IO lives in
19
+ * `StorageManager` so encrypted-at-rest deployments and atomic write
20
+ * semantics are inherited from the same code paths memories use.
21
+ */
22
+
23
+ import { createHash } from "node:crypto";
24
+
25
+ import type { SpeakerRegistry } from "./speakers.js";
26
+ import { distinctSpeakerLabels, resolveSpeaker } from "./speakers.js";
27
+ import type {
28
+ WearableConversation,
29
+ WearableDayTranscript,
30
+ WearableDayTranscriptMeta,
31
+ } from "./types.js";
32
+
33
+ export const WEARABLES_DIR_NAME = "wearables";
34
+
35
+ const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
36
+
37
+ export function isValidTranscriptDate(date: string): boolean {
38
+ if (!DATE_PATTERN.test(date)) return false;
39
+ const parsed = new Date(`${date}T00:00:00Z`);
40
+ return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === date;
41
+ }
42
+
43
+ export function hashTranscriptBody(body: string): string {
44
+ return createHash("sha256").update(body, "utf-8").digest("hex");
45
+ }
46
+
47
+ function formatClockTime(iso: string | undefined, timezone: string): string {
48
+ if (!iso) return "--:--";
49
+ const ms = Date.parse(iso);
50
+ if (Number.isNaN(ms)) return "--:--";
51
+ try {
52
+ return new Intl.DateTimeFormat("en-US", {
53
+ timeZone: timezone,
54
+ hour12: false,
55
+ hour: "2-digit",
56
+ minute: "2-digit",
57
+ }).format(new Date(ms));
58
+ } catch {
59
+ // Unknown timezone identifiers fall back to UTC rather than
60
+ // crashing a sync that already fetched data.
61
+ return new Date(ms).toISOString().slice(11, 16);
62
+ }
63
+ }
64
+
65
+ function conversationDurationMinutes(conversation: WearableConversation): number {
66
+ const start = Date.parse(conversation.startIso);
67
+ const end = conversation.endIso ? Date.parse(conversation.endIso) : NaN;
68
+ if (Number.isNaN(start) || Number.isNaN(end) || end <= start) return 0;
69
+ return (end - start) / 60_000;
70
+ }
71
+
72
+ /** Compose the markdown body (no frontmatter) for one source/day. */
73
+ export function composeDayTranscriptBody(
74
+ sourceId: string,
75
+ date: string,
76
+ timezone: string,
77
+ conversations: WearableConversation[],
78
+ registry: SpeakerRegistry,
79
+ ): string {
80
+ const lines: string[] = [];
81
+ lines.push(`# ${sourceId} transcript — ${date}`);
82
+ lines.push("");
83
+ const ordered = [...conversations].sort((a, b) => {
84
+ const aMs = Date.parse(a.startIso);
85
+ const bMs = Date.parse(b.startIso);
86
+ if (aMs < bMs) return -1;
87
+ if (aMs > bMs) return 1;
88
+ // Stable secondary key so equal start times order deterministically.
89
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
90
+ });
91
+ for (const conversation of ordered) {
92
+ const start = formatClockTime(conversation.startIso, timezone);
93
+ const end = formatClockTime(conversation.endIso, timezone);
94
+ const title = conversation.title?.trim();
95
+ const heading = title && title.length > 0 ? ` · ${title}` : "";
96
+ lines.push(`## ${start}–${end}${heading} (conversation ${conversation.id})`);
97
+ if (conversation.location) {
98
+ lines.push(`*Location: ${conversation.location}*`);
99
+ }
100
+ lines.push("");
101
+ for (const segment of conversation.segments) {
102
+ const { label } = resolveSpeaker(sourceId, segment, registry);
103
+ const at = formatClockTime(segment.startIso, timezone);
104
+ lines.push(`**${label}** [${at}]: ${segment.text}`);
105
+ }
106
+ lines.push("");
107
+ }
108
+ return `${lines.join("\n").trimEnd()}\n`;
109
+ }
110
+
111
+ export function composeDayTranscriptMeta(
112
+ sourceId: string,
113
+ date: string,
114
+ timezone: string,
115
+ conversations: WearableConversation[],
116
+ registry: SpeakerRegistry,
117
+ body: string,
118
+ syncedAt: string,
119
+ ): WearableDayTranscriptMeta {
120
+ const allSegments = conversations.flatMap((c) => c.segments);
121
+ const durationMinutes = Math.round(
122
+ conversations.reduce((sum, c) => sum + conversationDurationMinutes(c), 0),
123
+ );
124
+ return {
125
+ kind: "wearable-transcript",
126
+ source: sourceId,
127
+ date,
128
+ timezone,
129
+ conversationCount: conversations.length,
130
+ segmentCount: allSegments.length,
131
+ speakers: distinctSpeakerLabels(sourceId, allSegments, registry),
132
+ durationMinutes,
133
+ contentHash: hashTranscriptBody(body),
134
+ syncedAt,
135
+ };
136
+ }
137
+
138
+ /** Serialize meta + body into the persisted file format. */
139
+ export function serializeDayTranscript(
140
+ meta: WearableDayTranscriptMeta,
141
+ body: string,
142
+ ): string {
143
+ const lines: string[] = ["---"];
144
+ lines.push(`kind: ${meta.kind}`);
145
+ lines.push(`source: ${JSON.stringify(meta.source)}`);
146
+ lines.push(`date: ${JSON.stringify(meta.date)}`);
147
+ lines.push(`timezone: ${JSON.stringify(meta.timezone)}`);
148
+ lines.push(`conversationCount: ${meta.conversationCount}`);
149
+ lines.push(`segmentCount: ${meta.segmentCount}`);
150
+ if (meta.speakers.length === 0) {
151
+ lines.push("speakers: []");
152
+ } else {
153
+ lines.push("speakers:");
154
+ for (const speaker of meta.speakers) {
155
+ lines.push(` - ${JSON.stringify(speaker)}`);
156
+ }
157
+ }
158
+ lines.push(`durationMinutes: ${meta.durationMinutes}`);
159
+ lines.push(`contentHash: ${JSON.stringify(meta.contentHash)}`);
160
+ lines.push(`syncedAt: ${JSON.stringify(meta.syncedAt)}`);
161
+ lines.push("---");
162
+ lines.push("");
163
+ return `${lines.join("\n")}${body}`;
164
+ }
165
+
166
+ /**
167
+ * Parse a persisted day-transcript file. Returns null when the content
168
+ * does not look like a wearable transcript (wrong kind, missing
169
+ * frontmatter) so callers can distinguish "not a transcript" from a
170
+ * read error.
171
+ */
172
+ export function parseDayTranscript(raw: string): WearableDayTranscript | null {
173
+ if (!raw.startsWith("---\n")) return null;
174
+ const closeIndex = raw.indexOf("\n---\n", 4);
175
+ if (closeIndex === -1) return null;
176
+ const header = raw.slice(4, closeIndex);
177
+ const body = raw.slice(closeIndex + 5).replace(/^\n/, "");
178
+
179
+ const scalars = new Map<string, string>();
180
+ const speakers: string[] = [];
181
+ let inSpeakers = false;
182
+ for (const line of header.split("\n")) {
183
+ if (inSpeakers) {
184
+ const item = line.match(/^ {2}- (.*)$/);
185
+ if (item) {
186
+ speakers.push(parseYamlScalar(item[1]));
187
+ continue;
188
+ }
189
+ inSpeakers = false;
190
+ }
191
+ if (line === "speakers:") {
192
+ inSpeakers = true;
193
+ continue;
194
+ }
195
+ if (line === "speakers: []") continue;
196
+ const match = line.match(/^([A-Za-z][A-Za-z0-9]*): (.*)$/);
197
+ if (match) scalars.set(match[1], parseYamlScalar(match[2]));
198
+ }
199
+
200
+ if (scalars.get("kind") !== "wearable-transcript") return null;
201
+ const source = scalars.get("source");
202
+ const date = scalars.get("date");
203
+ if (!source || !date) return null;
204
+
205
+ const meta: WearableDayTranscriptMeta = {
206
+ kind: "wearable-transcript",
207
+ source,
208
+ date,
209
+ timezone: scalars.get("timezone") ?? "UTC",
210
+ conversationCount: parseNonNegativeInt(scalars.get("conversationCount")),
211
+ segmentCount: parseNonNegativeInt(scalars.get("segmentCount")),
212
+ speakers,
213
+ durationMinutes: parseNonNegativeInt(scalars.get("durationMinutes")),
214
+ contentHash: scalars.get("contentHash") ?? "",
215
+ syncedAt: scalars.get("syncedAt") ?? "",
216
+ };
217
+ return { meta, body };
218
+ }
219
+
220
+ function parseYamlScalar(value: string): string {
221
+ const trimmed = value.trim();
222
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2) {
223
+ try {
224
+ const parsed = JSON.parse(trimmed);
225
+ if (typeof parsed === "string") return parsed;
226
+ } catch {
227
+ // Fall through to the raw value below.
228
+ }
229
+ }
230
+ return trimmed;
231
+ }
232
+
233
+ function parseNonNegativeInt(value: string | undefined): number {
234
+ if (value === undefined) return 0;
235
+ const parsed = Number(value);
236
+ if (!Number.isFinite(parsed) || parsed < 0) return 0;
237
+ return Math.floor(parsed);
238
+ }
@@ -0,0 +1,32 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+
4
+ import { describeErrorForOperator, WearablesInputError } from "./errors.js";
5
+
6
+ test("foreign errors expose only the error class and errno code", () => {
7
+ const pathy = new Error(
8
+ "ENOENT: no such file or directory, open '/home/someone/.openclaw/workspace/memory/local/state/wearables/sync.json'",
9
+ );
10
+ (pathy as NodeJS.ErrnoException).code = "ENOENT";
11
+ assert.equal(describeErrorForOperator(pathy), "Error (ENOENT)");
12
+
13
+ class ProviderError extends Error {
14
+ constructor() {
15
+ super("secret-bearing message with /paths/and/such");
16
+ this.name = "ProviderError";
17
+ }
18
+ }
19
+ assert.equal(describeErrorForOperator(new ProviderError()), "ProviderError");
20
+ });
21
+
22
+ test("non-Error throws yield a generic marker", () => {
23
+ assert.equal(describeErrorForOperator("boom"), "unexpected non-Error failure");
24
+ assert.equal(describeErrorForOperator(undefined), "unexpected non-Error failure");
25
+ });
26
+
27
+ test("wearables' own input errors pass through verbatim", () => {
28
+ assert.equal(
29
+ describeErrorForOperator(new WearablesInputError("invalid days '0'")),
30
+ "invalid days '0'",
31
+ );
32
+ });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Error thrown by wearables surfaces for caller-correctable problems:
3
+ * invalid parameters, unknown sources, disabled subsystem, missing
4
+ * connector packages. Transport layers map this to 400-class responses;
5
+ * anything else is a backend fault and bubbles to the 500 handler.
6
+ */
7
+
8
+ import { displayErrorDetail } from "../runtime/better-sqlite.js";
9
+
10
+ export class WearablesInputError extends Error {
11
+ constructor(message: string) {
12
+ super(message);
13
+ this.name = "WearablesInputError";
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Render a caught error for operator-facing sync warnings and auth
19
+ * details. Warnings travel back through CLI/MCP/HTTP responses, so
20
+ * foreign error text never passes through — delegation to the
21
+ * project-standard `displayErrorDetail` exposes only the error class
22
+ * and Node errno code. Wearables' own input errors (authored messages,
23
+ * no foreign text) pass through verbatim.
24
+ */
25
+ export function describeErrorForOperator(err: unknown): string {
26
+ if (err instanceof WearablesInputError) return err.message;
27
+ const detail = displayErrorDetail(err);
28
+ return detail.length > 0 ? detail : "unexpected non-Error failure";
29
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Wearable transcript subsystem — public surface.
3
+ *
4
+ * Connector packages import the registry + types; hosts construct a
5
+ * `WearablesService` (usually via the orchestrator accessor).
6
+ */
7
+
8
+ export * from "./types.js";
9
+ export { describeErrorForOperator, WearablesInputError } from "./errors.js";
10
+ export {
11
+ defaultWearableCleanupSettings,
12
+ defaultWearableSourceSettings,
13
+ defaultWearablesConfig,
14
+ KNOWN_WEARABLE_SOURCE_IDS,
15
+ parseWearablesConfig,
16
+ } from "./config.js";
17
+ export {
18
+ registerWearableConnector,
19
+ getWearableConnector,
20
+ listWearableConnectors,
21
+ clearWearableConnectors,
22
+ ensureBuiltInWearableConnectors,
23
+ type WearableConnectorFactory,
24
+ type WearableConnectorFactoryOptions,
25
+ type WearableConnectorRegistration,
26
+ } from "./registry.js";
27
+ export {
28
+ cleanConversation,
29
+ collapseImmediateRepeats,
30
+ isLowQualitySegment,
31
+ stripFillerTokens,
32
+ type CleanupResult,
33
+ } from "./cleanup.js";
34
+ export {
35
+ applyOffTheRecord,
36
+ compileRedactionPatterns,
37
+ redactText,
38
+ REDACTION_PLACEHOLDER,
39
+ } from "./redaction.js";
40
+ export {
41
+ applyCorrections,
42
+ compileCorrectionRule,
43
+ compileCorrectionRules,
44
+ correctionsFilePath,
45
+ loadCorrectionsFile,
46
+ saveCorrectionsFile,
47
+ type CompiledCorrectionRule,
48
+ } from "./corrections.js";
49
+ export {
50
+ DEFAULT_SELF_NAME,
51
+ distinctSpeakerLabels,
52
+ emptySpeakerRegistry,
53
+ loadSpeakerRegistry,
54
+ resolveSpeaker,
55
+ saveSpeakerRegistry,
56
+ speakerRegistryKey,
57
+ speakersFilePath,
58
+ type ResolvedSpeaker,
59
+ type SpeakerOverride,
60
+ type SpeakerRegistry,
61
+ } from "./speakers.js";
62
+ export {
63
+ composeDayTranscriptBody,
64
+ composeDayTranscriptMeta,
65
+ hashTranscriptBody,
66
+ isValidTranscriptDate,
67
+ parseDayTranscript,
68
+ serializeDayTranscript,
69
+ WEARABLES_DIR_NAME,
70
+ } from "./day-store.js";
71
+ export {
72
+ emptySyncState,
73
+ loadSyncState,
74
+ saveSyncState,
75
+ syncStateFilePath,
76
+ updateSourceSyncState,
77
+ type WearableSourceSyncState,
78
+ type WearableSyncStateFile,
79
+ } from "./sync-state.js";
80
+ export {
81
+ buildExtractionTurns,
82
+ generateWearableMemories,
83
+ importNativeMemories,
84
+ memoryStatusForMode,
85
+ WEARABLE_SOURCE_PREFIX,
86
+ wearableDayTag,
87
+ wearableSourceLabel,
88
+ writeDailyDigestMemory,
89
+ type WearableMemoryGenDeps,
90
+ type WearableMemoryGenResult,
91
+ type WearableMemoryWriter,
92
+ } from "./memory-gen.js";
93
+ export {
94
+ dateInTimezone,
95
+ defaultTimezone,
96
+ resolveSyncDates,
97
+ syncWearableSource,
98
+ type WearableSyncDeps,
99
+ type WearableSyncOptions,
100
+ } from "./pipeline.js";
101
+ export {
102
+ runWearablesCliCommand,
103
+ type WearablesCliIo,
104
+ } from "./cli.js";
105
+ export {
106
+ locateTranscriptPath,
107
+ WearablesService,
108
+ type WearableDayTranscriptView,
109
+ type WearableMemorySearchResult,
110
+ type WearableSearchBackend,
111
+ type WearableStorageIo,
112
+ type WearableTranscriptSearchResult,
113
+ type WearablesServiceDeps,
114
+ } from "./service.js";