@remnic/core 9.3.624 → 9.3.626

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 (261) 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-7TPH6UZL.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-FG76RDVI.js → chunk-Y3TMFC6I.js} +136 -4
  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 +10 -10
  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/types-D5VRAI04.d.ts +3134 -0
  193. package/dist/types.d.ts +3 -2862
  194. package/dist/types.js +1 -1
  195. package/dist/utility-runtime.d.ts +1 -1
  196. package/dist/verified-recall.js +3 -3
  197. package/package.json +1 -1
  198. package/src/access-http.ts +167 -0
  199. package/src/access-mcp.ts +198 -0
  200. package/src/access-service.ts +65 -0
  201. package/src/cli.ts +187 -0
  202. package/src/config.ts +7 -0
  203. package/src/index.ts +7 -0
  204. package/src/orchestrator.ts +42 -0
  205. package/src/storage.ts +106 -0
  206. package/src/types.ts +5 -0
  207. package/src/wearables/cleanup.test.ts +134 -0
  208. package/src/wearables/cleanup.ts +188 -0
  209. package/src/wearables/cli.test.ts +170 -0
  210. package/src/wearables/cli.ts +441 -0
  211. package/src/wearables/config.test.ts +143 -0
  212. package/src/wearables/config.ts +332 -0
  213. package/src/wearables/corrections.test.ts +118 -0
  214. package/src/wearables/corrections.ts +211 -0
  215. package/src/wearables/day-store.test.ts +143 -0
  216. package/src/wearables/day-store.ts +238 -0
  217. package/src/wearables/errors.test.ts +32 -0
  218. package/src/wearables/errors.ts +29 -0
  219. package/src/wearables/index.ts +114 -0
  220. package/src/wearables/memory-gen.test.ts +342 -0
  221. package/src/wearables/memory-gen.ts +413 -0
  222. package/src/wearables/pipeline.test.ts +608 -0
  223. package/src/wearables/pipeline.ts +519 -0
  224. package/src/wearables/redaction.test.ts +94 -0
  225. package/src/wearables/redaction.ts +156 -0
  226. package/src/wearables/registry.test.ts +62 -0
  227. package/src/wearables/registry.ts +133 -0
  228. package/src/wearables/service.test.ts +425 -0
  229. package/src/wearables/service.ts +691 -0
  230. package/src/wearables/speakers.test.ts +110 -0
  231. package/src/wearables/speakers.ts +174 -0
  232. package/src/wearables/storage-io.test.ts +105 -0
  233. package/src/wearables/sync-state.test.ts +134 -0
  234. package/src/wearables/sync-state.ts +186 -0
  235. package/src/wearables/types.ts +285 -0
  236. package/dist/chunk-4R4KTDIE.js.map +0 -1
  237. package/dist/chunk-5GOMXHLC.js.map +0 -1
  238. package/dist/chunk-7TPH6UZL.js.map +0 -1
  239. package/dist/chunk-FG76RDVI.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-GYTVOLNX.js.map → chunk-3MNBW7R7.js.map} +0 -0
  245. /package/dist/{chunk-QFQQFX2H.js.map → chunk-3R2UZV3U.js.map} +0 -0
  246. /package/dist/{chunk-O4UNM6OR.js.map → chunk-532VCWYW.js.map} +0 -0
  247. /package/dist/{chunk-2UFQYU5F.js.map → chunk-57QXN2CS.js.map} +0 -0
  248. /package/dist/{chunk-UGEBPVNI.js.map → chunk-GE7Q7KXP.js.map} +0 -0
  249. /package/dist/{chunk-GLWW3EJQ.js.map → chunk-KB4MFBF5.js.map} +0 -0
  250. /package/dist/{chunk-FH3PPO42.js.map → chunk-KVFYTRMV.js.map} +0 -0
  251. /package/dist/{chunk-BNW5NJJH.js.map → chunk-LQYTQCXM.js.map} +0 -0
  252. /package/dist/{chunk-AYHXQR53.js.map → chunk-MVQN73GT.js.map} +0 -0
  253. /package/dist/{chunk-ZZPIJPPD.js.map → chunk-N5RGXWLQ.js.map} +0 -0
  254. /package/dist/{chunk-R3OQGYOU.js.map → chunk-P2D2MM47.js.map} +0 -0
  255. /package/dist/{chunk-PSUB67YB.js.map → chunk-PW6GURU3.js.map} +0 -0
  256. /package/dist/{chunk-W3BKVM64.js.map → chunk-QDV6VAD4.js.map} +0 -0
  257. /package/dist/{chunk-3QSU4NFF.js.map → chunk-QHXW3LZV.js.map} +0 -0
  258. /package/dist/{chunk-OZXVGYGZ.js.map → chunk-STDAAGH7.js.map} +0 -0
  259. /package/dist/{chunk-FMGWXIES.js.map → chunk-TZDSNIRO.js.map} +0 -0
  260. /package/dist/{chunk-2L54V4ZO.js.map → chunk-UELS6WWF.js.map} +0 -0
  261. /package/dist/{chunk-BPSGLMQ4.js.map → chunk-YQNADJCT.js.map} +0 -0
@@ -0,0 +1,519 @@
1
+ /**
2
+ * Wearable sync pipeline — pull → clean → label → correct → store →
3
+ * (optionally) remember, for one source.
4
+ *
5
+ * Stage order per conversation is deliberate:
6
+ * 1. off-the-record elision (before anything can persist the span)
7
+ * 2. cleanup (merging first lets redaction see numbers
8
+ * that ASR split across segments)
9
+ * 3. redaction (built-in + user patterns)
10
+ * 4. corrections (user-specific word fixes)
11
+ *
12
+ * Day files are rebuilt idempotently; the per-day body hash recorded in
13
+ * sync state lets unchanged days skip both the rewrite and the
14
+ * (expensive) memory extraction. Sync state advances only after every
15
+ * write for the run has succeeded.
16
+ */
17
+
18
+ import { cleanConversation } from "./cleanup.js";
19
+ import { describeErrorForOperator, WearablesInputError } from "./errors.js";
20
+ import {
21
+ applyCorrections,
22
+ compileCorrectionRules,
23
+ loadCorrectionsFile,
24
+ type CompiledCorrectionRule,
25
+ } from "./corrections.js";
26
+ import {
27
+ composeDayTranscriptBody,
28
+ composeDayTranscriptMeta,
29
+ hashTranscriptBody,
30
+ isValidTranscriptDate,
31
+ serializeDayTranscript,
32
+ } from "./day-store.js";
33
+ import {
34
+ generateWearableMemories,
35
+ importNativeMemories,
36
+ writeDailyDigestMemory,
37
+ type WearableMemoryGenDeps,
38
+ } from "./memory-gen.js";
39
+ import { applyOffTheRecord, compileRedactionPatterns, redactText } from "./redaction.js";
40
+ import { loadSpeakerRegistry } from "./speakers.js";
41
+ import {
42
+ loadSyncState,
43
+ saveSyncState,
44
+ updateSourceSyncState,
45
+ } from "./sync-state.js";
46
+ import type {
47
+ WearableConversation,
48
+ WearableSourceConnector,
49
+ WearableSourceSettings,
50
+ WearableSyncSummary,
51
+ WearablesConfig,
52
+ } from "./types.js";
53
+
54
+ /** Safety cap on pages fetched per day window. */
55
+ const MAX_PAGES_PER_DAY = 50;
56
+ /** Safety cap on native-memory pages per sync. */
57
+ const MAX_NATIVE_PAGES = 20;
58
+ /** Default lookback window (today + yesterday) for unscoped syncs. */
59
+ const DEFAULT_SYNC_DAYS = 2;
60
+ const MAX_SYNC_DAYS = 90;
61
+
62
+ export interface WearableSyncOptions {
63
+ /** Sync exactly this day (YYYY-MM-DD). Overrides `days`. */
64
+ date?: string;
65
+ /** Lookback window in days ending today (default 2, max 90). */
66
+ days?: number;
67
+ /** Re-run memory extraction even for unchanged days. */
68
+ forceMemories?: boolean;
69
+ signal?: AbortSignal;
70
+ }
71
+
72
+ export interface WearableSyncDeps {
73
+ /** Memory dir for state files (speakers, corrections, sync ledger). */
74
+ memoryDir: string;
75
+ /** Read the stored content hash for a day file, if present. */
76
+ readDayContentHash(sourceId: string, date: string): Promise<string | null>;
77
+ /** Persist a serialized day-transcript file (atomic). */
78
+ writeDayTranscript(
79
+ sourceId: string,
80
+ date: string,
81
+ serialized: string,
82
+ ): Promise<void>;
83
+ /** Optional hook fired once after any day files changed (reindex). */
84
+ afterTranscriptsWritten?(): Promise<void>;
85
+ /**
86
+ * Memory-generation dependencies, or null when no extraction engine
87
+ * is available in this context (transcripts still sync; memory
88
+ * creation is skipped with a warning when the mode wanted it).
89
+ */
90
+ memoryGen: WearableMemoryGenDeps | null;
91
+ /** Clock injection for tests. */
92
+ now?: () => Date;
93
+ }
94
+
95
+ /** Format a Date as YYYY-MM-DD in the given IANA timezone. */
96
+ export function dateInTimezone(date: Date, timezone: string): string {
97
+ try {
98
+ const parts = new Intl.DateTimeFormat("en-CA", {
99
+ timeZone: timezone,
100
+ year: "numeric",
101
+ month: "2-digit",
102
+ day: "2-digit",
103
+ }).formatToParts(date);
104
+ const get = (type: string) =>
105
+ parts.find((part) => part.type === type)?.value ?? "";
106
+ return `${get("year")}-${get("month")}-${get("day")}`;
107
+ } catch {
108
+ return date.toISOString().slice(0, 10);
109
+ }
110
+ }
111
+
112
+ /** Resolve the list of days to sync, oldest first. */
113
+ export function resolveSyncDates(
114
+ options: WearableSyncOptions,
115
+ timezone: string,
116
+ now: Date,
117
+ ): string[] {
118
+ if (options.date !== undefined) {
119
+ if (!isValidTranscriptDate(options.date)) {
120
+ throw new WearablesInputError(
121
+ `wearables sync: invalid date '${options.date}' — expected YYYY-MM-DD`,
122
+ );
123
+ }
124
+ return [options.date];
125
+ }
126
+ let days = DEFAULT_SYNC_DAYS;
127
+ if (options.days !== undefined) {
128
+ if (
129
+ !Number.isFinite(options.days) ||
130
+ !Number.isInteger(options.days) ||
131
+ options.days < 1 ||
132
+ options.days > MAX_SYNC_DAYS
133
+ ) {
134
+ throw new WearablesInputError(
135
+ `wearables sync: invalid days '${options.days}' — expected an integer between 1 and ${MAX_SYNC_DAYS}`,
136
+ );
137
+ }
138
+ days = options.days;
139
+ }
140
+ // Walk back by CALENDAR days from today's local date — subtracting
141
+ // fixed 24h intervals from the wall clock can skip a local day
142
+ // around DST transitions (Codex P2 on PR #1458).
143
+ const dates: string[] = [];
144
+ let cursor = dateInTimezone(now, timezone);
145
+ for (let count = 0; count < days; count++) {
146
+ dates.unshift(cursor);
147
+ cursor = previousIsoDate(cursor);
148
+ }
149
+ return dates;
150
+ }
151
+
152
+ /** Previous calendar date in pure date arithmetic (no DST exposure). */
153
+ function previousIsoDate(date: string): string {
154
+ const parsed = new Date(`${date}T00:00:00Z`);
155
+ parsed.setUTCDate(parsed.getUTCDate() - 1);
156
+ return parsed.toISOString().slice(0, 10);
157
+ }
158
+
159
+ async function fetchAllConversationsForDate(
160
+ connector: WearableSourceConnector,
161
+ date: string,
162
+ timezone: string,
163
+ signal: AbortSignal | undefined,
164
+ warnings: string[],
165
+ ): Promise<{ conversations: WearableConversation[]; partial: boolean }> {
166
+ const conversations: WearableConversation[] = [];
167
+ let cursor: string | null | undefined = undefined;
168
+ for (let page = 0; page < MAX_PAGES_PER_DAY; page++) {
169
+ const result = await connector.fetchConversations({
170
+ date,
171
+ timezone,
172
+ cursor,
173
+ signal,
174
+ });
175
+ conversations.push(...result.conversations);
176
+ if (!result.nextCursor) return { conversations, partial: false };
177
+ cursor = result.nextCursor;
178
+ }
179
+ warnings.push(
180
+ `${connector.id}: stopped paginating ${date} after ${MAX_PAGES_PER_DAY} pages — day may be partially synced (every sync refetches and re-warns until the provider day fits the cap)`,
181
+ );
182
+ return { conversations, partial: true };
183
+ }
184
+
185
+ /** Visible marker appended to day files whose fetch hit the page cap. */
186
+ const PARTIAL_DAY_MARKER =
187
+ "\n*Note: pagination safety cap reached during sync — this day may be incomplete.*\n";
188
+
189
+ /**
190
+ * Explicit replacement body for a day whose provider data exists but
191
+ * whose every segment was elided or dropped (all off-the-record, all
192
+ * ASR garbage). Written so previously-stored content for the day stops
193
+ * being searchable instead of lingering as a stale transcript.
194
+ */
195
+ function emptyDayBody(sourceId: string, date: string): string {
196
+ return (
197
+ `# ${sourceId} transcript — ${date}\n\n` +
198
+ "_No storable conversation content for this day (all segments were elided or dropped)._\n"
199
+ );
200
+ }
201
+
202
+ interface CleanedDay {
203
+ conversations: WearableConversation[];
204
+ segmentsKept: number;
205
+ segmentsDropped: number;
206
+ redactions: number;
207
+ correctionsApplied: number;
208
+ }
209
+
210
+ function cleanDay(
211
+ raw: WearableConversation[],
212
+ sourceId: string,
213
+ settings: WearableSourceSettings,
214
+ config: WearablesConfig,
215
+ userRedaction: RegExp[],
216
+ correctionRules: CompiledCorrectionRule[],
217
+ ): CleanedDay {
218
+ const out: CleanedDay = {
219
+ conversations: [],
220
+ segmentsKept: 0,
221
+ segmentsDropped: 0,
222
+ redactions: 0,
223
+ correctionsApplied: 0,
224
+ };
225
+ for (const conversation of raw) {
226
+ let current = conversation;
227
+ if (config.offTheRecordEnabled) {
228
+ const otr = applyOffTheRecord(current);
229
+ current = otr.conversation;
230
+ out.segmentsDropped += otr.droppedSegments;
231
+ }
232
+ const cleaned = cleanConversation(current, settings.cleanup);
233
+ current = cleaned.conversation;
234
+ out.segmentsDropped += cleaned.droppedSegments;
235
+
236
+ const segments = current.segments.map((segment) => {
237
+ let text = segment.text;
238
+ if (config.redactionEnabled) {
239
+ const redacted = redactText(text, userRedaction);
240
+ text = redacted.text;
241
+ out.redactions += redacted.redactions;
242
+ }
243
+ const corrected = applyCorrections(text, correctionRules, sourceId);
244
+ out.correctionsApplied += corrected.applied;
245
+ return { ...segment, text: corrected.text };
246
+ });
247
+ current = { ...current, segments };
248
+
249
+ if (current.segments.length > 0) {
250
+ out.conversations.push(current);
251
+ out.segmentsKept += current.segments.length;
252
+ }
253
+ }
254
+ return out;
255
+ }
256
+
257
+ /** Sync one source. */
258
+ export async function syncWearableSource(
259
+ connector: WearableSourceConnector,
260
+ settings: WearableSourceSettings,
261
+ config: WearablesConfig,
262
+ options: WearableSyncOptions,
263
+ deps: WearableSyncDeps,
264
+ ): Promise<WearableSyncSummary> {
265
+ const now = deps.now ? deps.now() : new Date();
266
+ const timezone = config.timezone ?? defaultTimezone();
267
+ const dates = resolveSyncDates(options, timezone, now);
268
+
269
+ const summary: WearableSyncSummary = {
270
+ source: connector.id,
271
+ days: dates,
272
+ conversations: 0,
273
+ segmentsKept: 0,
274
+ segmentsDropped: 0,
275
+ redactions: 0,
276
+ correctionsApplied: 0,
277
+ transcriptsWritten: [],
278
+ memoriesCreated: 0,
279
+ memoriesSkipped: 0,
280
+ nativeMemoriesImported: 0,
281
+ warnings: [],
282
+ };
283
+
284
+ const registry = await loadSpeakerRegistry(deps.memoryDir);
285
+ const stateRules = await loadCorrectionsFile(deps.memoryDir);
286
+ const correctionRules = [
287
+ ...compileCorrectionRules(config.corrections, "wearables.corrections"),
288
+ ...compileCorrectionRules(stateRules, "state corrections"),
289
+ ];
290
+ const userRedaction = compileRedactionPatterns(config.redactionPatterns);
291
+
292
+ let syncState = await loadSyncState(deps.memoryDir);
293
+ const previousState = syncState.sources[connector.id];
294
+ const dayHashes: Record<string, string> = {};
295
+ const memoryDayHashes: Record<string, string> = {};
296
+ const failedMemoryDays: string[] = [];
297
+ const importedNativeIds: string[] = [];
298
+
299
+ for (const date of dates) {
300
+ const fetched = await fetchAllConversationsForDate(
301
+ connector,
302
+ date,
303
+ timezone,
304
+ options.signal,
305
+ summary.warnings,
306
+ );
307
+ const cleaned = cleanDay(
308
+ fetched.conversations,
309
+ connector.id,
310
+ settings,
311
+ config,
312
+ userRedaction,
313
+ correctionRules,
314
+ );
315
+ summary.conversations += cleaned.conversations.length;
316
+ summary.segmentsKept += cleaned.segmentsKept;
317
+ summary.segmentsDropped += cleaned.segmentsDropped;
318
+ summary.redactions += cleaned.redactions;
319
+ summary.correctionsApplied += cleaned.correctionsApplied;
320
+
321
+ if (fetched.conversations.length === 0) {
322
+ // No provider data at all. A transient provider hiccup can
323
+ // legitimately produce an empty result, so an existing stored
324
+ // transcript is never auto-deleted here — surface it instead.
325
+ const existing = await deps.readDayContentHash(connector.id, date);
326
+ if (existing !== null) {
327
+ summary.warnings.push(
328
+ `${connector.id}: provider returned no conversations for ${date} but a stored transcript exists — leaving it in place; delete the day file manually if the recordings were intentionally removed upstream`,
329
+ );
330
+ }
331
+ continue;
332
+ }
333
+
334
+ // Provider data exists but cleanup/off-the-record elided all of it:
335
+ // replace any stored transcript with an explicit empty-day file so
336
+ // elided content stops being stored and searchable (Codex P2 on PR
337
+ // #1458).
338
+ const allElided = cleaned.conversations.length === 0;
339
+ let body = allElided
340
+ ? emptyDayBody(connector.id, date)
341
+ : composeDayTranscriptBody(
342
+ connector.id,
343
+ date,
344
+ timezone,
345
+ cleaned.conversations,
346
+ registry,
347
+ );
348
+ if (fetched.partial && !allElided) {
349
+ body += PARTIAL_DAY_MARKER;
350
+ }
351
+ const bodyHash = hashTranscriptBody(body);
352
+ // The on-disk file is the authority for the skip decision — a hash
353
+ // remembered in sync state must never suppress recreating a day
354
+ // file that was deleted or lost (Cursor review on PR #1458). The
355
+ // state's dayHashes remain as bookkeeping only.
356
+ const existingHash = await deps.readDayContentHash(connector.id, date);
357
+ const changed = existingHash !== bodyHash;
358
+ // An all-elided day only writes a replacement over an existing
359
+ // file; it never creates an empty-day file from nothing.
360
+ const shouldWrite = changed && (!allElided || existingHash !== null);
361
+
362
+ if (shouldWrite) {
363
+ const meta = composeDayTranscriptMeta(
364
+ connector.id,
365
+ date,
366
+ timezone,
367
+ cleaned.conversations,
368
+ registry,
369
+ body,
370
+ now.toISOString(),
371
+ );
372
+ await deps.writeDayTranscript(
373
+ connector.id,
374
+ date,
375
+ serializeDayTranscript(meta, body),
376
+ );
377
+ summary.transcriptsWritten.push(date);
378
+ }
379
+ dayHashes[date] = bodyHash;
380
+
381
+ if (allElided) continue;
382
+
383
+ // The memory pass runs when the day changed, when forced, or when
384
+ // the last pass for this exact content didn't complete cleanly —
385
+ // a sync that stored the transcript but failed mid-memory-write
386
+ // self-heals on the next run instead of being frozen out by the
387
+ // unchanged-day skip (Cursor review on PR #1458).
388
+ const memoryPassComplete =
389
+ previousState?.memoryDayHashes?.[date] === bodyHash;
390
+ if (
391
+ settings.memoryMode !== "off" &&
392
+ (changed || options.forceMemories === true || !memoryPassComplete)
393
+ ) {
394
+ if (!deps.memoryGen) {
395
+ summary.warnings.push(
396
+ `${connector.id}: memoryMode is '${settings.memoryMode}' but no extraction engine is available in this context — transcripts synced, memories skipped`,
397
+ );
398
+ } else {
399
+ // The whole memory pass (extraction, fact writes, digest) is
400
+ // warn-and-retry rather than abort: the transcript is already
401
+ // stored, and aborting here would leave any stale completion
402
+ // record from an earlier clean run in place to mask the
403
+ // failure (Kilo review on PR #1458 for the digest case; fact
404
+ // writes share the same failure class). A clean pass records
405
+ // completion; anything else clears it so the next sync
406
+ // re-runs the day.
407
+ let passClean = false;
408
+ try {
409
+ const generated = await generateWearableMemories(
410
+ connector.id,
411
+ date,
412
+ cleaned.conversations,
413
+ settings,
414
+ registry,
415
+ deps.memoryGen,
416
+ );
417
+ summary.memoriesCreated += generated.created;
418
+ summary.memoriesSkipped += generated.skipped;
419
+ summary.warnings.push(...generated.warnings);
420
+ passClean = generated.warnings.length === 0;
421
+ if (config.digestEnabled) {
422
+ const wrote = await writeDailyDigestMemory(
423
+ connector.id,
424
+ date,
425
+ cleaned.conversations,
426
+ settings,
427
+ registry,
428
+ deps.memoryGen.writer,
429
+ );
430
+ if (wrote) summary.memoriesCreated += 1;
431
+ }
432
+ } catch (err) {
433
+ passClean = false;
434
+ summary.warnings.push(
435
+ `${connector.id}: memory pass failed for ${date}: ${describeErrorForOperator(err)} — retries on the next sync`,
436
+ );
437
+ }
438
+ if (passClean) {
439
+ memoryDayHashes[date] = bodyHash;
440
+ } else {
441
+ failedMemoryDays.push(date);
442
+ }
443
+ }
444
+ } else if (settings.memoryMode !== "off" && memoryPassComplete) {
445
+ // Carry the completion record forward for unchanged days.
446
+ memoryDayHashes[date] = bodyHash;
447
+ }
448
+ }
449
+
450
+ if (
451
+ settings.importNativeMemories === "review" &&
452
+ typeof connector.fetchNativeMemories === "function"
453
+ ) {
454
+ if (!deps.memoryGen) {
455
+ summary.warnings.push(
456
+ `${connector.id}: importNativeMemories is enabled but no memory writer is available in this context`,
457
+ );
458
+ } else {
459
+ const alreadyImported = new Set(
460
+ previousState?.importedNativeMemoryIds ?? [],
461
+ );
462
+ let cursor: string | null | undefined = undefined;
463
+ for (let page = 0; page < MAX_NATIVE_PAGES; page++) {
464
+ const result = await connector.fetchNativeMemories({
465
+ cursor,
466
+ signal: options.signal,
467
+ });
468
+ const imported = await importNativeMemories(
469
+ connector.id,
470
+ result.memories,
471
+ alreadyImported,
472
+ deps.memoryGen.writer,
473
+ );
474
+ summary.nativeMemoriesImported += imported.imported;
475
+ importedNativeIds.push(...imported.importedIds);
476
+ for (const id of imported.importedIds) alreadyImported.add(id);
477
+ if (!result.nextCursor) break;
478
+ cursor = result.nextCursor;
479
+ if (page === MAX_NATIVE_PAGES - 1) {
480
+ summary.warnings.push(
481
+ `${connector.id}: stopped native-memory import after ${MAX_NATIVE_PAGES} pages — remaining items import on the next sync`,
482
+ );
483
+ }
484
+ }
485
+ }
486
+ }
487
+
488
+ if (summary.transcriptsWritten.length > 0 && deps.afterTranscriptsWritten) {
489
+ try {
490
+ await deps.afterTranscriptsWritten();
491
+ } catch (err) {
492
+ summary.warnings.push(
493
+ `search reindex failed (transcripts are stored and will index on the next update): ${describeErrorForOperator(err)}`,
494
+ );
495
+ }
496
+ }
497
+
498
+ // Watermark advances only now — after transcript writes, memory
499
+ // writes, and native imports all succeeded.
500
+ syncState = updateSourceSyncState(syncState, connector.id, {
501
+ syncedAt: now.toISOString(),
502
+ days: dates,
503
+ dayHashes,
504
+ memoryDayHashes,
505
+ clearMemoryDays: failedMemoryDays,
506
+ importedNativeMemoryIds: importedNativeIds,
507
+ });
508
+ await saveSyncState(deps.memoryDir, syncState);
509
+
510
+ return summary;
511
+ }
512
+
513
+ export function defaultTimezone(): string {
514
+ try {
515
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
516
+ } catch {
517
+ return "UTC";
518
+ }
519
+ }
@@ -0,0 +1,94 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+
4
+ import {
5
+ applyOffTheRecord,
6
+ compileRedactionPatterns,
7
+ redactText,
8
+ REDACTION_PLACEHOLDER,
9
+ } from "./redaction.js";
10
+ import type { WearableConversation } from "./types.js";
11
+
12
+ test("redacts SSN-formatted numbers", () => {
13
+ const result = redactText("my social is 123-45-6789 okay", []);
14
+ assert.equal(result.text, `my social is ${REDACTION_PLACEHOLDER} okay`);
15
+ assert.equal(result.redactions, 1);
16
+ });
17
+
18
+ test("redacts payment-card-like digit runs (spaced and contiguous)", () => {
19
+ assert.equal(
20
+ redactText("card 4111 1111 1111 1111 exp soon", []).text,
21
+ `card ${REDACTION_PLACEHOLDER} exp soon`,
22
+ );
23
+ assert.equal(
24
+ redactText("use 4111111111111111 today", []).text,
25
+ `use ${REDACTION_PLACEHOLDER} today`,
26
+ );
27
+ });
28
+
29
+ test("keeps short and ordinary numbers intact", () => {
30
+ const text = "call 555 0125 about the 2026 budget of $1,250";
31
+ const result = redactText(text, []);
32
+ assert.equal(result.text, text);
33
+ assert.equal(result.redactions, 0);
34
+ });
35
+
36
+ test("applies user patterns case-insensitively", () => {
37
+ const patterns = compileRedactionPatterns(["secret project \\w+"]);
38
+ const result = redactText("the Secret Project Falcon update", patterns);
39
+ assert.equal(result.text, `the ${REDACTION_PLACEHOLDER} update`);
40
+ });
41
+
42
+ test("compileRedactionPatterns rejects invalid regexes loudly", () => {
43
+ assert.throws(() => compileRedactionPatterns(["valid", "("]), /redactionPatterns\[1\]/);
44
+ assert.throws(() => compileRedactionPatterns([" "]), /non-empty/);
45
+ });
46
+
47
+ function conversation(texts: string[]): WearableConversation {
48
+ return {
49
+ id: "c1",
50
+ source: "testsource",
51
+ startIso: "2026-06-10T10:00:00Z",
52
+ segments: texts.map((text, index) => ({
53
+ speakerKey: index % 2 === 0 ? "a" : "b",
54
+ text,
55
+ })),
56
+ };
57
+ }
58
+
59
+ test("off the record drops the span until back on the record", () => {
60
+ const result = applyOffTheRecord(
61
+ conversation([
62
+ "Let me say this off the record for a second.",
63
+ "The merger closes Friday.",
64
+ "Seriously, do not repeat that.",
65
+ "Okay, back on the record now.",
66
+ "Lunch was great.",
67
+ ]),
68
+ );
69
+ const texts = result.conversation.segments.map((segment) => segment.text);
70
+ assert.deepEqual(texts, [
71
+ "[off the record — segment elided]",
72
+ "[back on the record]",
73
+ "Lunch was great.",
74
+ ]);
75
+ assert.equal(result.droppedSegments, 2);
76
+ });
77
+
78
+ test("off the record without a closing marker drops through conversation end", () => {
79
+ const result = applyOffTheRecord(
80
+ conversation(["This is off the record.", "Private thing one.", "Private thing two."]),
81
+ );
82
+ assert.equal(result.conversation.segments.length, 1);
83
+ assert.equal(result.droppedSegments, 2);
84
+ });
85
+
86
+ test("conversations without the marker pass through untouched", () => {
87
+ const input = conversation(["Plain talk.", "More plain talk."]);
88
+ const result = applyOffTheRecord(input);
89
+ assert.deepEqual(
90
+ result.conversation.segments.map((segment) => segment.text),
91
+ ["Plain talk.", "More plain talk."],
92
+ );
93
+ assert.equal(result.droppedSegments, 0);
94
+ });