@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,332 @@
1
+ /**
2
+ * Wearables config parsing — strict, loud, and default-safe.
3
+ *
4
+ * Mirrors the `procedural` block conventions in config.ts: shape
5
+ * violations and unparseable values throw with actionable messages
6
+ * (CLAUDE.md rule 51); boolean-ish strings coerce via the shared
7
+ * helpers (rule 36); every numeric knob is bounds-checked; the
8
+ * memory-creation default is the least-privileged mode (rule 48).
9
+ */
10
+
11
+ import { coerceBool, coerceNumber } from "../connectors/coerce.js";
12
+ import type { ImportanceLevel } from "../types.js";
13
+ import { compileCorrectionRules } from "./corrections.js";
14
+ import { compileRedactionPatterns } from "./redaction.js";
15
+ import type {
16
+ WearableCleanupSettings,
17
+ WearableCorrectionRule,
18
+ WearableMemoryMode,
19
+ WearableSourceSettings,
20
+ WearablesConfig,
21
+ } from "./types.js";
22
+
23
+ export const KNOWN_WEARABLE_SOURCE_IDS = ["limitless", "bee", "omi"] as const;
24
+
25
+ const MEMORY_MODES: WearableMemoryMode[] = ["off", "review", "auto"];
26
+ const IMPORTANCE_LEVELS: ImportanceLevel[] = [
27
+ "trivial",
28
+ "low",
29
+ "normal",
30
+ "high",
31
+ "critical",
32
+ ];
33
+ const NATIVE_IMPORT_MODES = ["off", "review"] as const;
34
+
35
+ const DEFAULT_MIN_CONFIDENCE = 0.6;
36
+ const DEFAULT_MIN_IMPORTANCE: ImportanceLevel = "low";
37
+ const DEFAULT_MAX_MEMORIES_PER_DAY = 20;
38
+ const MAX_MEMORIES_PER_DAY_CEILING = 500;
39
+
40
+ export function defaultWearableCleanupSettings(): WearableCleanupSettings {
41
+ return {
42
+ mergeSameSpeaker: true,
43
+ stripFillers: true,
44
+ collapseRepeats: true,
45
+ dropLowQuality: true,
46
+ };
47
+ }
48
+
49
+ export function defaultWearableSourceSettings(): WearableSourceSettings {
50
+ return {
51
+ enabled: false,
52
+ memoryMode: "review",
53
+ minConfidence: DEFAULT_MIN_CONFIDENCE,
54
+ minImportance: DEFAULT_MIN_IMPORTANCE,
55
+ maxMemoriesPerDay: DEFAULT_MAX_MEMORIES_PER_DAY,
56
+ importNativeMemories: "off",
57
+ cleanup: defaultWearableCleanupSettings(),
58
+ };
59
+ }
60
+
61
+ export function defaultWearablesConfig(): WearablesConfig {
62
+ return {
63
+ enabled: false,
64
+ redactionEnabled: true,
65
+ redactionPatterns: [],
66
+ offTheRecordEnabled: false,
67
+ digestEnabled: false,
68
+ corrections: [],
69
+ sources: {},
70
+ };
71
+ }
72
+
73
+ function requireObject(
74
+ value: unknown,
75
+ keyPath: string,
76
+ ): Record<string, unknown> {
77
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
78
+ throw new Error(
79
+ `${keyPath} must be an object (got ${JSON.stringify(value)})`,
80
+ );
81
+ }
82
+ return value as Record<string, unknown>;
83
+ }
84
+
85
+ function parseBool(
86
+ value: unknown,
87
+ keyPath: string,
88
+ fallback: boolean,
89
+ ): boolean {
90
+ if (value === undefined) return fallback;
91
+ const coerced = coerceBool(value);
92
+ if (coerced === undefined) {
93
+ throw new Error(
94
+ `${keyPath} must be a boolean or one of "true"/"false"/"1"/"0"/"yes"/"no"/"on"/"off" (got ${JSON.stringify(value)})`,
95
+ );
96
+ }
97
+ return coerced;
98
+ }
99
+
100
+ function parseEnum<T extends string>(
101
+ value: unknown,
102
+ keyPath: string,
103
+ allowed: readonly T[],
104
+ fallback: T,
105
+ ): T {
106
+ if (value === undefined) return fallback;
107
+ if (typeof value === "string" && (allowed as readonly string[]).includes(value)) {
108
+ return value as T;
109
+ }
110
+ throw new Error(
111
+ `${keyPath} must be one of ${allowed.map((entry) => `"${entry}"`).join(", ")} (got ${JSON.stringify(value)})`,
112
+ );
113
+ }
114
+
115
+ function parseOptionalString(
116
+ value: unknown,
117
+ keyPath: string,
118
+ ): string | undefined {
119
+ if (value === undefined) return undefined;
120
+ if (typeof value !== "string" || value.trim().length === 0) {
121
+ throw new Error(`${keyPath} must be a non-empty string when set`);
122
+ }
123
+ return value.trim();
124
+ }
125
+
126
+ function parseCorrectionRules(
127
+ value: unknown,
128
+ keyPath: string,
129
+ ): WearableCorrectionRule[] {
130
+ if (value === undefined) return [];
131
+ if (!Array.isArray(value)) {
132
+ throw new Error(`${keyPath} must be an array of correction rules`);
133
+ }
134
+ const rules = value.map((entry, index) => {
135
+ const raw = requireObject(entry, `${keyPath}[${index}]`);
136
+ const rule: WearableCorrectionRule = {
137
+ match: typeof raw.match === "string" ? raw.match : "",
138
+ replace: typeof raw.replace === "string" ? raw.replace : "",
139
+ };
140
+ if (raw.regex !== undefined) {
141
+ rule.regex = parseBool(raw.regex, `${keyPath}[${index}].regex`, false);
142
+ }
143
+ if (raw.caseInsensitive !== undefined) {
144
+ rule.caseInsensitive = parseBool(
145
+ raw.caseInsensitive,
146
+ `${keyPath}[${index}].caseInsensitive`,
147
+ true,
148
+ );
149
+ }
150
+ if (raw.sources !== undefined) {
151
+ if (
152
+ !Array.isArray(raw.sources) ||
153
+ raw.sources.some((source) => typeof source !== "string")
154
+ ) {
155
+ throw new Error(`${keyPath}[${index}].sources must be an array of strings`);
156
+ }
157
+ rule.sources = raw.sources as string[];
158
+ }
159
+ return rule;
160
+ });
161
+ // Compile now so bad rules fail at config load, not mid-sync.
162
+ compileCorrectionRules(rules, keyPath);
163
+ return rules;
164
+ }
165
+
166
+ function parseSourceSettings(
167
+ value: unknown,
168
+ keyPath: string,
169
+ ): WearableSourceSettings {
170
+ const raw = requireObject(value, keyPath);
171
+ const defaults = defaultWearableSourceSettings();
172
+
173
+ const minConfidenceRaw = coerceNumber(raw.minConfidence);
174
+ // Out-of-range values reject loudly instead of clamping — silently
175
+ // turning minConfidence 7 into 1 would change the memory gate in a
176
+ // way the operator never asked for (Codex P2 on PR #1458, round 3).
177
+ if (
178
+ raw.minConfidence !== undefined &&
179
+ (minConfidenceRaw === undefined || minConfidenceRaw < 0 || minConfidenceRaw > 1)
180
+ ) {
181
+ throw new Error(
182
+ `${keyPath}.minConfidence must be a number between 0 and 1 (got ${JSON.stringify(raw.minConfidence)})`,
183
+ );
184
+ }
185
+ const minConfidence = minConfidenceRaw ?? defaults.minConfidence;
186
+
187
+ const maxPerDayRaw = coerceNumber(raw.maxMemoriesPerDay);
188
+ // Reject non-integers instead of flooring them: 0.5 would floor to 0,
189
+ // and 0 is the documented "disable the cap" value — a fractional typo
190
+ // must not silently remove the cap (Codex P2 on PR #1458).
191
+ if (
192
+ raw.maxMemoriesPerDay !== undefined &&
193
+ (maxPerDayRaw === undefined ||
194
+ !Number.isInteger(maxPerDayRaw) ||
195
+ maxPerDayRaw < 0 ||
196
+ maxPerDayRaw > MAX_MEMORIES_PER_DAY_CEILING)
197
+ ) {
198
+ throw new Error(
199
+ `${keyPath}.maxMemoriesPerDay must be an integer between 0 and ${MAX_MEMORIES_PER_DAY_CEILING} (0 disables the cap); got ${JSON.stringify(raw.maxMemoriesPerDay)}`,
200
+ );
201
+ }
202
+ // 0 is the documented "disable the cap" value — honored here AND in
203
+ // the schema minimum (CLAUDE.md rule 45).
204
+ const maxMemoriesPerDay = maxPerDayRaw ?? defaults.maxMemoriesPerDay;
205
+
206
+ const rawCleanup =
207
+ raw.cleanup === undefined ? {} : requireObject(raw.cleanup, `${keyPath}.cleanup`);
208
+ const cleanup: WearableCleanupSettings = {
209
+ mergeSameSpeaker: parseBool(
210
+ rawCleanup.mergeSameSpeaker,
211
+ `${keyPath}.cleanup.mergeSameSpeaker`,
212
+ defaults.cleanup.mergeSameSpeaker,
213
+ ),
214
+ stripFillers: parseBool(
215
+ rawCleanup.stripFillers,
216
+ `${keyPath}.cleanup.stripFillers`,
217
+ defaults.cleanup.stripFillers,
218
+ ),
219
+ collapseRepeats: parseBool(
220
+ rawCleanup.collapseRepeats,
221
+ `${keyPath}.cleanup.collapseRepeats`,
222
+ defaults.cleanup.collapseRepeats,
223
+ ),
224
+ dropLowQuality: parseBool(
225
+ rawCleanup.dropLowQuality,
226
+ `${keyPath}.cleanup.dropLowQuality`,
227
+ defaults.cleanup.dropLowQuality,
228
+ ),
229
+ };
230
+
231
+ return {
232
+ enabled: parseBool(raw.enabled, `${keyPath}.enabled`, false),
233
+ apiKey: parseOptionalString(raw.apiKey, `${keyPath}.apiKey`),
234
+ baseUrl: parseOptionalString(raw.baseUrl, `${keyPath}.baseUrl`),
235
+ appId: parseOptionalString(raw.appId, `${keyPath}.appId`),
236
+ userId: parseOptionalString(raw.userId, `${keyPath}.userId`),
237
+ memoryMode: parseEnum(
238
+ raw.memoryMode,
239
+ `${keyPath}.memoryMode`,
240
+ MEMORY_MODES,
241
+ defaults.memoryMode,
242
+ ),
243
+ minConfidence,
244
+ minImportance: parseEnum(
245
+ raw.minImportance,
246
+ `${keyPath}.minImportance`,
247
+ IMPORTANCE_LEVELS,
248
+ defaults.minImportance,
249
+ ),
250
+ maxMemoriesPerDay,
251
+ importNativeMemories: parseEnum(
252
+ raw.importNativeMemories,
253
+ `${keyPath}.importNativeMemories`,
254
+ NATIVE_IMPORT_MODES,
255
+ defaults.importNativeMemories,
256
+ ),
257
+ cleanup,
258
+ };
259
+ }
260
+
261
+ /**
262
+ * Parse the `wearables` config block. `undefined` yields the disabled
263
+ * default config; any present-but-malformed value throws.
264
+ */
265
+ export function parseWearablesConfig(value: unknown): WearablesConfig {
266
+ if (value === undefined) return defaultWearablesConfig();
267
+ const raw = requireObject(value, "wearables");
268
+ const defaults = defaultWearablesConfig();
269
+
270
+ const timezone = parseOptionalString(raw.timezone, "wearables.timezone");
271
+ if (timezone !== undefined) {
272
+ try {
273
+ new Intl.DateTimeFormat("en-US", { timeZone: timezone });
274
+ } catch {
275
+ throw new Error(
276
+ `wearables.timezone must be a valid IANA timezone identifier (got ${JSON.stringify(timezone)})`,
277
+ );
278
+ }
279
+ }
280
+
281
+ let redactionPatterns: string[] = defaults.redactionPatterns;
282
+ if (raw.redactionPatterns !== undefined) {
283
+ if (
284
+ !Array.isArray(raw.redactionPatterns) ||
285
+ raw.redactionPatterns.some((pattern) => typeof pattern !== "string")
286
+ ) {
287
+ throw new Error("wearables.redactionPatterns must be an array of strings");
288
+ }
289
+ redactionPatterns = raw.redactionPatterns as string[];
290
+ // Compile now so invalid regexes fail at config load.
291
+ compileRedactionPatterns(redactionPatterns);
292
+ }
293
+
294
+ const sources: Record<string, WearableSourceSettings> = {};
295
+ if (raw.sources !== undefined) {
296
+ const rawSources = requireObject(raw.sources, "wearables.sources");
297
+ for (const [sourceId, sourceValue] of Object.entries(rawSources)) {
298
+ if (!/^[a-z][a-z0-9-]{0,63}$/.test(sourceId)) {
299
+ throw new Error(
300
+ `wearables.sources keys must be lowercase source ids (letters, digits, dashes); got ${JSON.stringify(sourceId)}`,
301
+ );
302
+ }
303
+ sources[sourceId] = parseSourceSettings(
304
+ sourceValue,
305
+ `wearables.sources.${sourceId}`,
306
+ );
307
+ }
308
+ }
309
+
310
+ return {
311
+ enabled: parseBool(raw.enabled, "wearables.enabled", defaults.enabled),
312
+ ...(timezone !== undefined ? { timezone } : {}),
313
+ redactionEnabled: parseBool(
314
+ raw.redactionEnabled,
315
+ "wearables.redactionEnabled",
316
+ defaults.redactionEnabled,
317
+ ),
318
+ redactionPatterns,
319
+ offTheRecordEnabled: parseBool(
320
+ raw.offTheRecordEnabled,
321
+ "wearables.offTheRecordEnabled",
322
+ defaults.offTheRecordEnabled,
323
+ ),
324
+ digestEnabled: parseBool(
325
+ raw.digestEnabled,
326
+ "wearables.digestEnabled",
327
+ defaults.digestEnabled,
328
+ ),
329
+ corrections: parseCorrectionRules(raw.corrections, "wearables.corrections"),
330
+ sources,
331
+ };
332
+ }
@@ -0,0 +1,118 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import * as path from "node:path";
5
+ import { test } from "node:test";
6
+
7
+ import {
8
+ applyCorrections,
9
+ compileCorrectionRule,
10
+ compileCorrectionRules,
11
+ loadCorrectionsFile,
12
+ saveCorrectionsFile,
13
+ } from "./corrections.js";
14
+
15
+ function compiled(rules: Parameters<typeof compileCorrectionRules>[0]) {
16
+ return compileCorrectionRules(rules, "test");
17
+ }
18
+
19
+ test("literal rules match on word boundaries, case-insensitive by default", () => {
20
+ const rules = compiled([{ match: "remnick", replace: "Remnic" }]);
21
+ assert.equal(
22
+ applyCorrections("Remnick and REMNICK but not remnickson", rules, "limitless").text,
23
+ "Remnic and Remnic but not remnickson",
24
+ );
25
+ });
26
+
27
+ test("literal rules escape regex metacharacters", () => {
28
+ const rules = compiled([{ match: "node.js (v22)", replace: "Node 22" }]);
29
+ const result = applyCorrections("we run node.js (v22) here", rules, "limitless");
30
+ assert.equal(result.text, "we run Node 22 here");
31
+ assert.equal(result.applied, 1);
32
+ // The dot must not act as a wildcard.
33
+ assert.equal(
34
+ applyCorrections("nodeXjs (v22)", rules, "limitless").applied,
35
+ 0,
36
+ );
37
+ });
38
+
39
+ test("dollar signs in replacements stay literal", () => {
40
+ const rules = compiled([{ match: "five bucks", replace: "$5 ($$ saved)" }]);
41
+ assert.equal(
42
+ applyCorrections("that cost five bucks", rules, "limitless").text,
43
+ "that cost $5 ($$ saved)",
44
+ );
45
+ });
46
+
47
+ test("regex rules work and case sensitivity is honored", () => {
48
+ const rules = compiled([
49
+ { match: "colou?r", replace: "color", regex: true, caseInsensitive: false },
50
+ ]);
51
+ const result = applyCorrections("colour Colour", rules, "limitless");
52
+ assert.equal(result.text, "color Colour");
53
+ });
54
+
55
+ test("rules scoped to sources skip other sources", () => {
56
+ const rules = compiled([
57
+ { match: "panda", replace: "Pendant", sources: ["limitless"] },
58
+ ]);
59
+ assert.equal(applyCorrections("my panda", rules, "limitless").applied, 1);
60
+ assert.equal(applyCorrections("my panda", rules, "bee").applied, 0);
61
+ });
62
+
63
+ test("invalid rules are rejected loudly at compile time", () => {
64
+ assert.throws(() => compileCorrectionRule({ match: "", replace: "x" }, "r"), /non-empty/);
65
+ assert.throws(
66
+ () => compileCorrectionRule({ match: "(", replace: "x", regex: true }, "r"),
67
+ /not a valid regular expression/,
68
+ );
69
+ assert.throws(
70
+ () => compileCorrectionRule({ match: "a*", replace: "x", regex: true }, "r"),
71
+ /matches the empty string/,
72
+ );
73
+ });
74
+
75
+ test("corrections file round-trips, validates, and tolerates absence", async () => {
76
+ const dir = mkdtempSync(path.join(tmpdir(), "remnic-corrections-"));
77
+ try {
78
+ assert.deepEqual(await loadCorrectionsFile(dir), []);
79
+ const rules = [
80
+ { match: "remnick", replace: "Remnic" },
81
+ { match: "acme corp", replace: "ACME Corp", sources: ["bee"] },
82
+ ];
83
+ await saveCorrectionsFile(dir, rules);
84
+ assert.deepEqual(await loadCorrectionsFile(dir), rules);
85
+ } finally {
86
+ rmSync(dir, { recursive: true, force: true });
87
+ }
88
+ });
89
+
90
+ test("a corrupt corrections file throws instead of silently dropping rules", async () => {
91
+ const dir = mkdtempSync(path.join(tmpdir(), "remnic-corrections-"));
92
+ try {
93
+ const { promises: fsPromises } = await import("node:fs");
94
+ const filePath = path.join(dir, "state", "wearables", "corrections.json");
95
+ await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
96
+ await fsPromises.writeFile(filePath, "{not json", "utf-8");
97
+ await assert.rejects(loadCorrectionsFile(dir), /not valid JSON/);
98
+ await fsPromises.writeFile(filePath, JSON.stringify({ rules: "nope" }), "utf-8");
99
+ await assert.rejects(loadCorrectionsFile(dir), /unexpected shape/);
100
+ } finally {
101
+ rmSync(dir, { recursive: true, force: true });
102
+ }
103
+ });
104
+
105
+ test("saving an invalid rule set is rejected before touching the file", async () => {
106
+ const dir = mkdtempSync(path.join(tmpdir(), "remnic-corrections-"));
107
+ try {
108
+ await saveCorrectionsFile(dir, [{ match: "good", replace: "fine" }]);
109
+ await assert.rejects(
110
+ saveCorrectionsFile(dir, [{ match: "(", replace: "x", regex: true }]),
111
+ /not a valid regular expression/,
112
+ );
113
+ // The previously-saved rules are still intact.
114
+ assert.equal((await loadCorrectionsFile(dir)).length, 1);
115
+ } finally {
116
+ rmSync(dir, { recursive: true, force: true });
117
+ }
118
+ });
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Wearable transcript corrections — user-specific replacement rules.
3
+ *
4
+ * ASR engines consistently mishear the same proper nouns for the same
5
+ * person ("remnick" for "Remnic", a colleague's name, product jargon).
6
+ * Rules come from two places, merged at sync time:
7
+ *
8
+ * 1. `wearables.corrections` in plugin config (declarative, versioned
9
+ * with the operator's config).
10
+ * 2. A CLI-managed rules file at `state/wearables/corrections.json`
11
+ * (added interactively via `remnic wearables corrections add`).
12
+ *
13
+ * Literal rules are regex-escaped before compilation and replacements
14
+ * are applied via a function (never a replacement string) so `$` in
15
+ * either side can't corrupt output.
16
+ */
17
+
18
+ import { promises as fsPromises } from "node:fs";
19
+ import * as path from "node:path";
20
+
21
+ import type { WearableCorrectionRule } from "./types.js";
22
+
23
+ export interface CompiledCorrectionRule {
24
+ rule: WearableCorrectionRule;
25
+ pattern: RegExp;
26
+ }
27
+
28
+ export interface CorrectionApplication {
29
+ text: string;
30
+ applied: number;
31
+ }
32
+
33
+ /** Hard cap on rule pattern length (bounds hostile/pathological regexes). */
34
+ const MAX_PATTERN_LENGTH = 256;
35
+
36
+ function escapeRegExp(value: string): string {
37
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
38
+ }
39
+
40
+ /**
41
+ * Validate and compile a correction rule. Throws a descriptive error on
42
+ * invalid input (empty match, regex that doesn't compile, regex that
43
+ * matches the empty string) — callers surface this at config parse or
44
+ * CLI time rather than skipping the rule silently.
45
+ */
46
+ export function compileCorrectionRule(
47
+ rule: WearableCorrectionRule,
48
+ label: string,
49
+ ): CompiledCorrectionRule {
50
+ if (typeof rule.match !== "string" || rule.match.length === 0) {
51
+ throw new Error(`${label}: match must be a non-empty string`);
52
+ }
53
+ if (rule.match.length > MAX_PATTERN_LENGTH) {
54
+ throw new Error(
55
+ `${label}: match exceeds ${MAX_PATTERN_LENGTH} characters — correction patterns must stay short`,
56
+ );
57
+ }
58
+ if (typeof rule.replace !== "string") {
59
+ throw new Error(`${label}: replace must be a string`);
60
+ }
61
+ const flags = rule.caseInsensitive === false ? "g" : "gi";
62
+ let pattern: RegExp;
63
+ if (rule.regex === true) {
64
+ try {
65
+ // Operator-supplied regexes are the documented feature here
66
+ // (rules live in the operator's own config / state file, never in
67
+ // request input); the length cap above bounds pathological
68
+ // patterns. CodeQL js/regex-injection is dismissed by design for
69
+ // this site.
70
+ pattern = new RegExp(rule.match, flags);
71
+ } catch (err) {
72
+ throw new Error(
73
+ `${label}: match is not a valid regular expression: ${
74
+ err instanceof Error ? err.message : String(err)
75
+ }`,
76
+ );
77
+ }
78
+ } else {
79
+ // Literal rules match on word boundaries when both edges of the
80
+ // match are word characters, so "remnick" doesn't fire inside
81
+ // "remnickson" unless the user opts into regex mode.
82
+ const escaped = escapeRegExp(rule.match);
83
+ const leading = /^[\p{L}\p{N}_]/u.test(rule.match) ? "\\b" : "";
84
+ const trailing = /[\p{L}\p{N}_]$/u.test(rule.match) ? "\\b" : "";
85
+ pattern = new RegExp(`${leading}${escaped}${trailing}`, flags);
86
+ }
87
+ if (pattern.test("")) {
88
+ throw new Error(
89
+ `${label}: pattern matches the empty string and would corrupt every transcript`,
90
+ );
91
+ }
92
+ pattern.lastIndex = 0;
93
+ return { rule, pattern };
94
+ }
95
+
96
+ /** Compile a rule list, labeling errors with their index. */
97
+ export function compileCorrectionRules(
98
+ rules: WearableCorrectionRule[],
99
+ labelPrefix: string,
100
+ ): CompiledCorrectionRule[] {
101
+ return rules.map((rule, index) =>
102
+ compileCorrectionRule(rule, `${labelPrefix}[${index}]`),
103
+ );
104
+ }
105
+
106
+ /** Apply every applicable rule to a piece of transcript text. */
107
+ export function applyCorrections(
108
+ text: string,
109
+ rules: CompiledCorrectionRule[],
110
+ sourceId: string,
111
+ ): CorrectionApplication {
112
+ let applied = 0;
113
+ let result = text;
114
+ for (const { rule, pattern } of rules) {
115
+ if (
116
+ Array.isArray(rule.sources) &&
117
+ rule.sources.length > 0 &&
118
+ !rule.sources.includes(sourceId)
119
+ ) {
120
+ continue;
121
+ }
122
+ pattern.lastIndex = 0;
123
+ result = result.replace(pattern, () => {
124
+ applied += 1;
125
+ // Replacement via function: `$` in rule.replace stays literal.
126
+ return rule.replace;
127
+ });
128
+ }
129
+ return { text: result, applied };
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // CLI-managed rules file
134
+ // ---------------------------------------------------------------------------
135
+
136
+ interface CorrectionsFileShape {
137
+ version: 1;
138
+ rules: WearableCorrectionRule[];
139
+ }
140
+
141
+ export function correctionsFilePath(memoryDir: string): string {
142
+ return path.join(memoryDir, "state", "wearables", "corrections.json");
143
+ }
144
+
145
+ /**
146
+ * Load CLI-managed correction rules. A missing file means no rules; a
147
+ * malformed file throws (operators should know their corrections are
148
+ * not being applied rather than silently losing them).
149
+ */
150
+ export async function loadCorrectionsFile(
151
+ memoryDir: string,
152
+ ): Promise<WearableCorrectionRule[]> {
153
+ const filePath = correctionsFilePath(memoryDir);
154
+ let raw: string;
155
+ try {
156
+ raw = await fsPromises.readFile(filePath, "utf-8");
157
+ } catch (err) {
158
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return [];
159
+ throw err;
160
+ }
161
+ let parsed: unknown;
162
+ try {
163
+ parsed = JSON.parse(raw);
164
+ } catch (err) {
165
+ throw new Error(
166
+ `wearables corrections file is not valid JSON (state/wearables/corrections.json): ${
167
+ err instanceof Error ? err.message : String(err)
168
+ }`,
169
+ );
170
+ }
171
+ if (
172
+ typeof parsed !== "object" ||
173
+ parsed === null ||
174
+ Array.isArray(parsed) ||
175
+ !Array.isArray((parsed as CorrectionsFileShape).rules)
176
+ ) {
177
+ throw new Error(
178
+ 'wearables corrections file has an unexpected shape (state/wearables/corrections.json); expected {"version":1,"rules":[...]}',
179
+ );
180
+ }
181
+ const rules = (parsed as CorrectionsFileShape).rules;
182
+ // Validate every persisted rule up front so a hand-edited bad rule
183
+ // fails at load with its index, not mid-sync.
184
+ compileCorrectionRules(rules, "state corrections");
185
+ return rules;
186
+ }
187
+
188
+ /** Persist CLI-managed rules atomically (temp file + rename). */
189
+ export async function saveCorrectionsFile(
190
+ memoryDir: string,
191
+ rules: WearableCorrectionRule[],
192
+ ): Promise<void> {
193
+ compileCorrectionRules(rules, "state corrections");
194
+ const filePath = correctionsFilePath(memoryDir);
195
+ await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
196
+ const payload: CorrectionsFileShape = { version: 1, rules };
197
+ const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now().toString(36)}`;
198
+ await fsPromises.writeFile(
199
+ tmpPath,
200
+ `${JSON.stringify(payload, null, 2)}\n`,
201
+ "utf-8",
202
+ );
203
+ try {
204
+ await fsPromises.rename(tmpPath, filePath);
205
+ } catch (err) {
206
+ // Clean up the temp file on rename failure; the original (if any)
207
+ // is untouched.
208
+ await fsPromises.unlink(tmpPath).catch(() => undefined);
209
+ throw err;
210
+ }
211
+ }