@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,691 @@
1
+ /**
2
+ * WearablesService — the single implementation behind every wearables
3
+ * access surface (CLI, MCP tools, HTTP routes). Surfaces stay thin and
4
+ * delegate here; formatting differences live with the surface, behavior
5
+ * lives here (same renderer-sharing rule as recall explain/xray).
6
+ */
7
+
8
+ import {
9
+ correctionsFilePath,
10
+ loadCorrectionsFile,
11
+ saveCorrectionsFile,
12
+ compileCorrectionRule,
13
+ } from "./corrections.js";
14
+ import { describeErrorForOperator, WearablesInputError } from "./errors.js";
15
+ import { isValidTranscriptDate, parseDayTranscript } from "./day-store.js";
16
+ import type { WearableMemoryGenDeps } from "./memory-gen.js";
17
+ import { WEARABLE_SOURCE_PREFIX, wearableSourceLabel } from "./memory-gen.js";
18
+ import {
19
+ defaultTimezone,
20
+ syncWearableSource,
21
+ type WearableSyncOptions,
22
+ } from "./pipeline.js";
23
+ import {
24
+ ensureBuiltInWearableConnectors,
25
+ getWearableConnector,
26
+ listWearableConnectors,
27
+ } from "./registry.js";
28
+ import {
29
+ loadSpeakerRegistry,
30
+ saveSpeakerRegistry,
31
+ speakerRegistryKey,
32
+ type SpeakerRegistry,
33
+ } from "./speakers.js";
34
+ import { loadSyncState } from "./sync-state.js";
35
+ import type {
36
+ WearableCorrectionRule,
37
+ WearableDayTranscript,
38
+ WearableSourceSettings,
39
+ WearableSourceStatus,
40
+ WearableSyncSummary,
41
+ WearablesConfig,
42
+ } from "./types.js";
43
+
44
+ /** Storage capabilities the service needs (satisfied by StorageManager). */
45
+ export interface WearableStorageIo {
46
+ readonly dir: string;
47
+ writeWearableDayTranscript(
48
+ sourceId: string,
49
+ date: string,
50
+ serialized: string,
51
+ ): Promise<void>;
52
+ readWearableDayTranscript(
53
+ sourceId: string,
54
+ date: string,
55
+ ): Promise<string | null>;
56
+ listWearableTranscriptDays(
57
+ sourceId?: string,
58
+ ): Promise<Array<{ source: string; date: string }>>;
59
+ readAllMemories(): Promise<
60
+ Array<{
61
+ path: string;
62
+ frontmatter: {
63
+ id: string;
64
+ source: string;
65
+ created: string;
66
+ tags: string[];
67
+ status?: string;
68
+ structuredAttributes?: Record<string, string>;
69
+ };
70
+ content: string;
71
+ }>
72
+ >;
73
+ writeMemory: WearableMemoryGenDeps["writer"]["writeMemory"];
74
+ hasFactContentHash(content: string): Promise<boolean>;
75
+ }
76
+
77
+ export interface WearableSearchBackend {
78
+ /** Full-text search over the memory dir; null when unavailable. */
79
+ search(
80
+ query: string,
81
+ maxResults: number,
82
+ ): Promise<Array<{ path: string; score: number; preview: string }> | null>;
83
+ }
84
+
85
+ export interface WearablesServiceDeps {
86
+ config: WearablesConfig;
87
+ getStorage(): Promise<WearableStorageIo>;
88
+ /** Extraction hook; null when no engine is available. */
89
+ extract: WearableMemoryGenDeps["extract"] | null;
90
+ /** Search backend (QMD); null disables indexed search. */
91
+ searchBackend: WearableSearchBackend | null;
92
+ /** Fired after transcript writes so the search index refreshes. */
93
+ reindexSearch?: () => Promise<void>;
94
+ }
95
+
96
+ export interface WearableTranscriptSearchResult {
97
+ source: string;
98
+ date: string;
99
+ score: number;
100
+ snippet: string;
101
+ /** "indexed" (QMD) or "scan" (substring fallback). */
102
+ backend: "indexed" | "scan";
103
+ }
104
+
105
+ export interface WearableMemorySearchResult {
106
+ id: string;
107
+ source: string;
108
+ date?: string;
109
+ conversationId?: string;
110
+ status?: string;
111
+ content: string;
112
+ created: string;
113
+ }
114
+
115
+ export interface WearableDayTranscriptView {
116
+ source: string;
117
+ date: string;
118
+ meta: WearableDayTranscript["meta"] | null;
119
+ body: string;
120
+ /** Other sources that also recorded during this day (overlap hint). */
121
+ overlapsWith: string[];
122
+ }
123
+
124
+ /**
125
+ * Build the memory writer used by wearable syncs. The storage fact
126
+ * hash index only covers category "fact", so dedup for the other
127
+ * categories wearables write (moment digests, decisions, preferences,
128
+ * commitments) additionally scans existing wearable-tagged memories for
129
+ * an exact content match — without this, a forced or retried day
130
+ * re-writes identical digests and candidates (Codex P2 on PR #1458).
131
+ * The scan is bounded to wearable-sourced memories and sits on the
132
+ * cached readAllMemories() path.
133
+ */
134
+ export function createWearableMemoryWriter(
135
+ storage: WearableStorageIo,
136
+ ): WearableMemoryGenDeps["writer"] {
137
+ return {
138
+ writeMemory: storage.writeMemory.bind(storage),
139
+ hasFactContentHash: async (content: string) => {
140
+ if (await storage.hasFactContentHash(content)) return true;
141
+ const needle = content.trim();
142
+ const memories = await storage.readAllMemories();
143
+ return memories.some(
144
+ (memory) =>
145
+ typeof memory.frontmatter.source === "string" &&
146
+ memory.frontmatter.source.startsWith(`${WEARABLE_SOURCE_PREFIX}:`) &&
147
+ memory.content.trim() === needle,
148
+ );
149
+ },
150
+ };
151
+ }
152
+
153
+ /** Mirrors the storage-layer guard so surface inputs fail as 400s. */
154
+ const SOURCE_ID_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
155
+
156
+ function assertValidSourceId(source: string): void {
157
+ if (!SOURCE_ID_PATTERN.test(source)) {
158
+ throw new WearablesInputError(
159
+ `invalid source id '${source}' — expected lowercase letters, digits, and dashes`,
160
+ );
161
+ }
162
+ }
163
+
164
+ const TRANSCRIPT_SEARCH_DEFAULT_LIMIT = 10;
165
+ const TRANSCRIPT_SEARCH_MAX_LIMIT = 50;
166
+ const MEMORY_LIST_DEFAULT_LIMIT = 50;
167
+ const MEMORY_LIST_MAX_LIMIT = 200;
168
+
169
+ export class WearablesService {
170
+ constructor(private readonly deps: WearablesServiceDeps) {}
171
+
172
+ get enabled(): boolean {
173
+ return this.deps.config.enabled;
174
+ }
175
+
176
+ private assertEnabled(): void {
177
+ if (!this.deps.config.enabled) {
178
+ throw new WearablesInputError(
179
+ "wearables are not enabled — set `wearables.enabled: true` (and configure at least one source) in the plugin config",
180
+ );
181
+ }
182
+ }
183
+
184
+ private timezone(): string {
185
+ return this.deps.config.timezone ?? defaultTimezone();
186
+ }
187
+
188
+ private enabledSources(): Array<[string, WearableSourceSettings]> {
189
+ return Object.entries(this.deps.config.sources).filter(
190
+ ([, settings]) => settings.enabled,
191
+ );
192
+ }
193
+
194
+ /** Status for every configured source (and connector availability). */
195
+ async status(): Promise<{
196
+ enabled: boolean;
197
+ timezone: string;
198
+ sources: WearableSourceStatus[];
199
+ connectorsInstalled: string[];
200
+ }> {
201
+ await ensureBuiltInWearableConnectors();
202
+ const storage = await this.deps.getStorage();
203
+ const syncState = await loadSyncState(storage.dir);
204
+ const sources: WearableSourceStatus[] = [];
205
+ for (const [sourceId, settings] of Object.entries(this.deps.config.sources)) {
206
+ const registration = getWearableConnector(sourceId);
207
+ const days = await storage.listWearableTranscriptDays(sourceId).catch(() => []);
208
+ const state = syncState.sources[sourceId];
209
+ sources.push({
210
+ source: sourceId,
211
+ displayName: registration?.displayName ?? sourceId,
212
+ enabled: settings.enabled,
213
+ connectorInstalled: registration !== undefined,
214
+ memoryMode: settings.memoryMode,
215
+ lastSyncAt: state?.lastSyncAt ?? null,
216
+ lastDateSynced: state?.lastDateSynced ?? null,
217
+ transcriptDays: days.length,
218
+ });
219
+ }
220
+ return {
221
+ enabled: this.deps.config.enabled,
222
+ timezone: this.timezone(),
223
+ sources,
224
+ connectorsInstalled: listWearableConnectors(),
225
+ };
226
+ }
227
+
228
+ /** Run a sync for one source or all enabled sources. */
229
+ async sync(
230
+ options: WearableSyncOptions & { source?: string },
231
+ ): Promise<WearableSyncSummary[]> {
232
+ this.assertEnabled();
233
+ await ensureBuiltInWearableConnectors();
234
+ const storage = await this.deps.getStorage();
235
+
236
+ let targets: Array<[string, WearableSourceSettings]>;
237
+ if (options.source !== undefined) {
238
+ assertValidSourceId(options.source);
239
+ const settings = this.deps.config.sources[options.source];
240
+ if (!settings) {
241
+ throw new WearablesInputError(
242
+ `unknown wearable source '${options.source}' — configured sources: ${
243
+ Object.keys(this.deps.config.sources).join(", ") || "(none)"
244
+ }`,
245
+ );
246
+ }
247
+ if (!settings.enabled) {
248
+ throw new WearablesInputError(
249
+ `wearable source '${options.source}' is configured but disabled — set wearables.sources.${options.source}.enabled: true`,
250
+ );
251
+ }
252
+ targets = [[options.source, settings]];
253
+ } else {
254
+ targets = this.enabledSources();
255
+ if (targets.length === 0) {
256
+ throw new WearablesInputError(
257
+ "no wearable sources are enabled — configure wearables.sources.<id>.enabled: true",
258
+ );
259
+ }
260
+ }
261
+
262
+ const memoryGen: WearableMemoryGenDeps | null = this.deps.extract
263
+ ? {
264
+ extract: this.deps.extract,
265
+ writer: createWearableMemoryWriter(storage),
266
+ }
267
+ : null;
268
+
269
+ const summaries: WearableSyncSummary[] = [];
270
+ for (const [sourceId, settings] of targets) {
271
+ const registration = getWearableConnector(sourceId);
272
+ if (!registration) {
273
+ throw new WearablesInputError(
274
+ `wearable source '${sourceId}' is enabled but its connector package is not installed.\n` +
275
+ `Install it alongside Remnic:\n npm install @remnic/connector-${sourceId}`,
276
+ );
277
+ }
278
+ const connector = registration.factory({
279
+ settings,
280
+ timezone: this.timezone(),
281
+ });
282
+ const summary = await syncWearableSource(
283
+ connector,
284
+ settings,
285
+ this.deps.config,
286
+ options,
287
+ {
288
+ memoryDir: storage.dir,
289
+ readDayContentHash: async (source, date) => {
290
+ const raw = await storage.readWearableDayTranscript(source, date);
291
+ if (raw === null) return null;
292
+ return parseDayTranscript(raw)?.meta.contentHash ?? null;
293
+ },
294
+ writeDayTranscript: (source, date, serialized) =>
295
+ storage.writeWearableDayTranscript(source, date, serialized),
296
+ afterTranscriptsWritten: this.deps.reindexSearch,
297
+ memoryGen,
298
+ },
299
+ );
300
+ summaries.push(summary);
301
+ }
302
+ return summaries;
303
+ }
304
+
305
+ /** Verify connectivity/credentials for one source. */
306
+ async checkAuth(sourceId: string): Promise<{ ok: boolean; detail?: string }> {
307
+ this.assertEnabled();
308
+ await ensureBuiltInWearableConnectors();
309
+ assertValidSourceId(sourceId);
310
+ const settings = this.deps.config.sources[sourceId];
311
+ if (!settings) {
312
+ throw new WearablesInputError(`unknown wearable source '${sourceId}'`);
313
+ }
314
+ const registration = getWearableConnector(sourceId);
315
+ if (!registration) {
316
+ return {
317
+ ok: false,
318
+ detail: `connector package @remnic/connector-${sourceId} is not installed`,
319
+ };
320
+ }
321
+ const connector = registration.factory({
322
+ settings,
323
+ timezone: this.timezone(),
324
+ });
325
+ try {
326
+ // Connector detail strings are authored guidance (plus
327
+ // name+errno network summaries) — safe to pass through verbatim.
328
+ return await connector.verifyAuth();
329
+ } catch (err) {
330
+ return {
331
+ ok: false,
332
+ detail: describeErrorForOperator(err),
333
+ };
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Full transcript(s) for a day. Without `source`, returns every
339
+ * source that recorded that day, annotated with overlap hints.
340
+ */
341
+ async dayTranscript(
342
+ date: string,
343
+ sourceId?: string,
344
+ ): Promise<WearableDayTranscriptView[]> {
345
+ if (!isValidTranscriptDate(date)) {
346
+ throw new WearablesInputError(`invalid date '${date}' — expected YYYY-MM-DD`);
347
+ }
348
+ if (sourceId !== undefined) assertValidSourceId(sourceId);
349
+ const storage = await this.deps.getStorage();
350
+ const targets =
351
+ sourceId !== undefined
352
+ ? [sourceId]
353
+ : (await storage.listWearableTranscriptDays())
354
+ .filter((entry) => entry.date === date)
355
+ .map((entry) => entry.source);
356
+ const views: WearableDayTranscriptView[] = [];
357
+ for (const source of [...new Set(targets)]) {
358
+ const raw = await storage.readWearableDayTranscript(source, date);
359
+ if (raw === null) continue;
360
+ const parsed = parseDayTranscript(raw);
361
+ views.push({
362
+ source,
363
+ date,
364
+ meta: parsed?.meta ?? null,
365
+ body: parsed?.body ?? raw,
366
+ overlapsWith: [],
367
+ });
368
+ }
369
+ for (const view of views) {
370
+ view.overlapsWith = views
371
+ .map((other) => other.source)
372
+ .filter((other) => other !== view.source);
373
+ }
374
+ return views;
375
+ }
376
+
377
+ /** List days that have stored transcripts. */
378
+ async listDays(
379
+ sourceId?: string,
380
+ ): Promise<Array<{ source: string; date: string }>> {
381
+ if (sourceId !== undefined) assertValidSourceId(sourceId);
382
+ const storage = await this.deps.getStorage();
383
+ return storage.listWearableTranscriptDays(sourceId);
384
+ }
385
+
386
+ /**
387
+ * Search stored transcripts. Uses the indexed backend when available
388
+ * and falls back to a bounded substring scan otherwise — the two
389
+ * paths are distinguishable in the result (`backend`) so callers can
390
+ * tell "no hits" from "weaker search ran".
391
+ */
392
+ async searchTranscripts(
393
+ query: string,
394
+ options: {
395
+ source?: string;
396
+ from?: string;
397
+ to?: string;
398
+ limit?: number;
399
+ } = {},
400
+ ): Promise<WearableTranscriptSearchResult[]> {
401
+ const trimmed = query.trim();
402
+ if (trimmed.length === 0) {
403
+ throw new WearablesInputError("transcript search requires a non-empty query");
404
+ }
405
+ if (options.source !== undefined) assertValidSourceId(options.source);
406
+ for (const [name, value] of [
407
+ ["from", options.from],
408
+ ["to", options.to],
409
+ ] as const) {
410
+ if (value !== undefined && !isValidTranscriptDate(value)) {
411
+ throw new WearablesInputError(`invalid ${name} date '${value}' — expected YYYY-MM-DD`);
412
+ }
413
+ }
414
+ const limit = clampLimit(
415
+ options.limit,
416
+ TRANSCRIPT_SEARCH_DEFAULT_LIMIT,
417
+ TRANSCRIPT_SEARCH_MAX_LIMIT,
418
+ "limit",
419
+ );
420
+
421
+ const matchesScope = (source: string, date: string): boolean => {
422
+ if (options.source !== undefined && source !== options.source) return false;
423
+ if (options.from !== undefined && date < options.from) return false;
424
+ // Half-open scan semantics aren't meaningful for whole-day files;
425
+ // `to` is inclusive of the named day.
426
+ if (options.to !== undefined && date > options.to) return false;
427
+ return true;
428
+ };
429
+
430
+ if (this.deps.searchBackend) {
431
+ const hits = await this.deps.searchBackend.search(trimmed, limit * 5);
432
+ if (hits !== null) {
433
+ const results: WearableTranscriptSearchResult[] = [];
434
+ for (const hit of hits) {
435
+ const located = locateTranscriptPath(hit.path);
436
+ if (!located) continue;
437
+ if (!matchesScope(located.source, located.date)) continue;
438
+ results.push({
439
+ source: located.source,
440
+ date: located.date,
441
+ score: hit.score,
442
+ snippet: hit.preview,
443
+ backend: "indexed",
444
+ });
445
+ if (results.length >= limit) break;
446
+ }
447
+ // The index spans the whole memory dir, so ordinary memory
448
+ // files can crowd transcripts out of the top hits entirely.
449
+ // Zero in-scope hits therefore doesn't mean "no transcript
450
+ // matches" — fall through to the bounded scan in that case
451
+ // (Codex P2 on PR #1458). Partial result sets stay indexed-only
452
+ // so the two backends never interleave in one response.
453
+ if (results.length > 0) {
454
+ return results;
455
+ }
456
+ }
457
+ }
458
+
459
+ // Fallback scan: newest days first, bounded, case-insensitive.
460
+ const storage = await this.deps.getStorage();
461
+ const days = await storage.listWearableTranscriptDays(options.source);
462
+ const needle = trimmed.toLowerCase();
463
+ const results: WearableTranscriptSearchResult[] = [];
464
+ for (const { source, date } of days) {
465
+ if (!matchesScope(source, date)) continue;
466
+ const raw = await storage.readWearableDayTranscript(source, date);
467
+ if (raw === null) continue;
468
+ const body = parseDayTranscript(raw)?.body ?? raw;
469
+ const lower = body.toLowerCase();
470
+ const index = lower.indexOf(needle);
471
+ if (index === -1) continue;
472
+ results.push({
473
+ source,
474
+ date,
475
+ score: 0,
476
+ snippet: extractSnippet(body, index, needle.length),
477
+ backend: "scan",
478
+ });
479
+ if (results.length >= limit) break;
480
+ }
481
+ return results;
482
+ }
483
+
484
+ /**
485
+ * Memories created from wearable transcripts, filterable by source
486
+ * and/or day. Includes pending_review candidates — the whole point of
487
+ * review mode is seeing what's queued.
488
+ */
489
+ async transcriptMemories(
490
+ options: {
491
+ source?: string;
492
+ date?: string;
493
+ limit?: number;
494
+ } = {},
495
+ ): Promise<WearableMemorySearchResult[]> {
496
+ if (options.date !== undefined && !isValidTranscriptDate(options.date)) {
497
+ throw new WearablesInputError(`invalid date '${options.date}' — expected YYYY-MM-DD`);
498
+ }
499
+ if (options.source !== undefined) assertValidSourceId(options.source);
500
+ const limit = clampLimit(
501
+ options.limit,
502
+ MEMORY_LIST_DEFAULT_LIMIT,
503
+ MEMORY_LIST_MAX_LIMIT,
504
+ "limit",
505
+ );
506
+ const storage = await this.deps.getStorage();
507
+ const memories = await storage.readAllMemories();
508
+ const results: WearableMemorySearchResult[] = [];
509
+ for (const memory of memories) {
510
+ const source = memory.frontmatter.source;
511
+ if (typeof source !== "string" || !source.startsWith(`${WEARABLE_SOURCE_PREFIX}:`)) {
512
+ continue;
513
+ }
514
+ const attrs = memory.frontmatter.structuredAttributes ?? {};
515
+ const sourceId = attrs.wearableSource;
516
+ if (options.source !== undefined) {
517
+ if (
518
+ sourceId !== options.source &&
519
+ source !== wearableSourceLabel(options.source) &&
520
+ source !== `${wearableSourceLabel(options.source)}:native`
521
+ ) {
522
+ continue;
523
+ }
524
+ }
525
+ if (options.date !== undefined && attrs.wearableDate !== options.date) {
526
+ continue;
527
+ }
528
+ results.push({
529
+ id: memory.frontmatter.id,
530
+ source: sourceId ?? source,
531
+ date: attrs.wearableDate,
532
+ conversationId: attrs.wearableConversationId,
533
+ status: memory.frontmatter.status,
534
+ content: memory.content,
535
+ created: memory.frontmatter.created,
536
+ });
537
+ }
538
+ results.sort((a, b) => {
539
+ if (a.created > b.created) return -1;
540
+ if (a.created < b.created) return 1;
541
+ if (a.id < b.id) return -1;
542
+ if (a.id > b.id) return 1;
543
+ return 0;
544
+ });
545
+ return results.slice(0, limit);
546
+ }
547
+
548
+ // -- speakers -------------------------------------------------------------
549
+
550
+ async listSpeakers(): Promise<SpeakerRegistry> {
551
+ const storage = await this.deps.getStorage();
552
+ return loadSpeakerRegistry(storage.dir);
553
+ }
554
+
555
+ async setSpeaker(
556
+ sourceId: string,
557
+ speakerKey: string,
558
+ name: string,
559
+ opts: { isSelf?: boolean } = {},
560
+ ): Promise<SpeakerRegistry> {
561
+ if (typeof name !== "string" || name.trim().length === 0) {
562
+ throw new WearablesInputError("speaker name must be a non-empty string");
563
+ }
564
+ if (typeof speakerKey !== "string" || speakerKey.trim().length === 0) {
565
+ throw new WearablesInputError("speaker key must be a non-empty string");
566
+ }
567
+ const storage = await this.deps.getStorage();
568
+ const registry = await loadSpeakerRegistry(storage.dir);
569
+ registry.speakers[speakerRegistryKey(sourceId, speakerKey.trim())] = {
570
+ name: name.trim(),
571
+ ...(opts.isSelf === true ? { isSelf: true } : {}),
572
+ updatedAt: new Date().toISOString(),
573
+ };
574
+ await saveSpeakerRegistry(storage.dir, registry);
575
+ return registry;
576
+ }
577
+
578
+ async setSelfName(name: string): Promise<SpeakerRegistry> {
579
+ if (typeof name !== "string" || name.trim().length === 0) {
580
+ throw new WearablesInputError("self name must be a non-empty string");
581
+ }
582
+ const storage = await this.deps.getStorage();
583
+ const registry = await loadSpeakerRegistry(storage.dir);
584
+ registry.selfName = name.trim();
585
+ await saveSpeakerRegistry(storage.dir, registry);
586
+ return registry;
587
+ }
588
+
589
+ async removeSpeaker(
590
+ sourceId: string,
591
+ speakerKey: string,
592
+ ): Promise<SpeakerRegistry> {
593
+ const storage = await this.deps.getStorage();
594
+ const registry = await loadSpeakerRegistry(storage.dir);
595
+ const key = speakerRegistryKey(sourceId, speakerKey.trim());
596
+ if (!(key in registry.speakers)) {
597
+ throw new WearablesInputError(`no speaker override stored for '${key}'`);
598
+ }
599
+ delete registry.speakers[key];
600
+ await saveSpeakerRegistry(storage.dir, registry);
601
+ return registry;
602
+ }
603
+
604
+ // -- corrections ----------------------------------------------------------
605
+
606
+ async listCorrections(): Promise<{
607
+ fromConfig: WearableCorrectionRule[];
608
+ fromState: WearableCorrectionRule[];
609
+ stateFilePath: string;
610
+ }> {
611
+ const storage = await this.deps.getStorage();
612
+ return {
613
+ fromConfig: this.deps.config.corrections,
614
+ fromState: await loadCorrectionsFile(storage.dir),
615
+ stateFilePath: correctionsFilePath(storage.dir),
616
+ };
617
+ }
618
+
619
+ async addCorrection(rule: WearableCorrectionRule): Promise<void> {
620
+ // Validate before persisting so a bad rule fails the command, not
621
+ // the next sync.
622
+ compileCorrectionRule(rule, "correction");
623
+ const storage = await this.deps.getStorage();
624
+ const rules = await loadCorrectionsFile(storage.dir);
625
+ const duplicate = rules.some(
626
+ (existing) =>
627
+ existing.match === rule.match &&
628
+ existing.replace === rule.replace &&
629
+ (existing.regex === true) === (rule.regex === true),
630
+ );
631
+ if (duplicate) {
632
+ throw new WearablesInputError(
633
+ `an identical correction rule already exists (match: ${JSON.stringify(rule.match)})`,
634
+ );
635
+ }
636
+ rules.push(rule);
637
+ await saveCorrectionsFile(storage.dir, rules);
638
+ }
639
+
640
+ async removeCorrection(index: number): Promise<WearableCorrectionRule> {
641
+ if (!Number.isInteger(index) || index < 0) {
642
+ throw new WearablesInputError(`invalid correction index '${index}'`);
643
+ }
644
+ const storage = await this.deps.getStorage();
645
+ const rules = await loadCorrectionsFile(storage.dir);
646
+ if (index >= rules.length) {
647
+ throw new WearablesInputError(
648
+ `correction index ${index} is out of range (have ${rules.length} state rule${rules.length === 1 ? "" : "s"})`,
649
+ );
650
+ }
651
+ const [removed] = rules.splice(index, 1);
652
+ await saveCorrectionsFile(storage.dir, rules);
653
+ return removed;
654
+ }
655
+ }
656
+
657
+ function clampLimit(
658
+ value: number | undefined,
659
+ fallback: number,
660
+ max: number,
661
+ label: string,
662
+ ): number {
663
+ if (value === undefined) return fallback;
664
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1 || value > max) {
665
+ throw new WearablesInputError(
666
+ `invalid ${label} '${value}' — expected an integer between 1 and ${max}`,
667
+ );
668
+ }
669
+ return value;
670
+ }
671
+
672
+ /** Map an indexed-search hit path back to (source, date), or null. */
673
+ export function locateTranscriptPath(
674
+ hitPath: string,
675
+ ): { source: string; date: string } | null {
676
+ const normalized = hitPath.replace(/\\/g, "/");
677
+ const match = normalized.match(
678
+ /(?:^|\/)wearables\/([a-z][a-z0-9-]{0,63})\/(\d{4}-\d{2}-\d{2})\.md$/,
679
+ );
680
+ if (!match) return null;
681
+ if (!isValidTranscriptDate(match[2])) return null;
682
+ return { source: match[1], date: match[2] };
683
+ }
684
+
685
+ function extractSnippet(body: string, index: number, matchLength: number): string {
686
+ const start = Math.max(0, index - 80);
687
+ const end = Math.min(body.length, index + matchLength + 80);
688
+ const prefix = start > 0 ? "…" : "";
689
+ const suffix = end < body.length ? "…" : "";
690
+ return `${prefix}${body.slice(start, end).replace(/\s+/g, " ").trim()}${suffix}`;
691
+ }