@remnic/core 9.3.629 → 9.3.631

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 (221) hide show
  1. package/dist/access-cli.js +15 -13
  2. package/dist/access-cli.js.map +1 -1
  3. package/dist/access-http.d.ts +5 -4
  4. package/dist/access-http.js +7 -6
  5. package/dist/access-mcp.d.ts +5 -4
  6. package/dist/access-mcp.js +6 -5
  7. package/dist/{access-service-BdThkfIE.d.ts → access-service-C9_EpVHd.d.ts} +2 -2
  8. package/dist/access-service.d.ts +5 -4
  9. package/dist/access-service.js +5 -4
  10. package/dist/action-confidence.d.ts +1 -1
  11. package/dist/active-memory-bridge.d.ts +1 -1
  12. package/dist/active-recall.d.ts +1 -1
  13. package/dist/active-recall.js +1 -1
  14. package/dist/auto-sync-RFADEHIQ.js +75 -0
  15. package/dist/auto-sync-RFADEHIQ.js.map +1 -0
  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 -3
  19. package/dist/briefing.d.ts +1 -1
  20. package/dist/briefing.js +3 -2
  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 -3
  27. package/dist/causal-consolidation.js.map +1 -1
  28. package/dist/{chunk-532VCWYW.js → chunk-242XFZ36.js} +2 -2
  29. package/dist/{chunk-XXO5TI3B.js → chunk-32U3N7H5.js} +3 -3
  30. package/dist/{chunk-KB4MFBF5.js → chunk-3RDYU3JS.js} +3 -3
  31. package/dist/{chunk-57QXN2CS.js → chunk-4S3N6HFG.js} +2 -2
  32. package/dist/{chunk-OLNNOHBC.js → chunk-5PT5I6JQ.js} +20 -14
  33. package/dist/{chunk-OLNNOHBC.js.map → chunk-5PT5I6JQ.js.map} +1 -1
  34. package/dist/{chunk-GE7Q7KXP.js → chunk-7A2QKUUA.js} +2 -2
  35. package/dist/{chunk-KKTXCFD7.js → chunk-7H5WCPBS.js} +95 -11
  36. package/dist/{chunk-KKTXCFD7.js.map → chunk-7H5WCPBS.js.map} +1 -1
  37. package/dist/{chunk-3MNBW7R7.js → chunk-C4KKM62E.js} +2 -2
  38. package/dist/{chunk-NKCW223V.js → chunk-CMN5AWAZ.js} +2 -2
  39. package/dist/{chunk-JXHMAQYT.js → chunk-DOBJH4I6.js} +4 -4
  40. package/dist/{chunk-TZDSNIRO.js → chunk-IFVFQRZ2.js} +5 -5
  41. package/dist/{chunk-LQYTQCXM.js → chunk-JCLECECB.js} +2 -2
  42. package/dist/chunk-KVDUDYEN.js +1164 -0
  43. package/dist/chunk-KVDUDYEN.js.map +1 -0
  44. package/dist/{chunk-QDV6VAD4.js → chunk-LEG7XWS2.js} +2 -2
  45. package/dist/chunk-M7XQSUBB.js +280 -0
  46. package/dist/chunk-M7XQSUBB.js.map +1 -0
  47. package/dist/{chunk-N5RGXWLQ.js → chunk-PUEAEQSN.js} +2 -2
  48. package/dist/{chunk-UGHUNQ74.js → chunk-QYGIQ5NM.js} +212 -417
  49. package/dist/chunk-QYGIQ5NM.js.map +1 -0
  50. package/dist/{chunk-JKCDQBDW.js → chunk-UXFOGILU.js} +2 -2
  51. package/dist/{chunk-MVQN73GT.js → chunk-VTR3MNYF.js} +2 -2
  52. package/dist/{chunk-KVFYTRMV.js → chunk-W25I7G6U.js} +2 -2
  53. package/dist/{chunk-3GLCUPXP.js → chunk-WLZBVYC6.js} +192 -889
  54. package/dist/chunk-WLZBVYC6.js.map +1 -0
  55. package/dist/{chunk-3R2UZV3U.js → chunk-X7EJF46S.js} +2 -2
  56. package/dist/{chunk-54KDA6UK.js → chunk-XG4NAWAV.js} +3 -3
  57. package/dist/{chunk-P2D2MM47.js → chunk-YROCXMCK.js} +2 -2
  58. package/dist/{cli-DAsHklrf.d.ts → cli-CuVEQWKr.d.ts} +3 -3
  59. package/dist/cli.d.ts +6 -5
  60. package/dist/cli.js +18 -17
  61. package/dist/compounding/engine.d.ts +1 -1
  62. package/dist/compounding/engine.js +3 -2
  63. package/dist/compounding/preference-consolidator.d.ts +1 -1
  64. package/dist/compression-optimizer.d.ts +1 -1
  65. package/dist/config.d.ts +1 -1
  66. package/dist/config.js +1 -1
  67. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  68. package/dist/connectors/codex-materialize-runner.js +3 -2
  69. package/dist/connectors/codex-materialize.d.ts +1 -1
  70. package/dist/connectors/index.d.ts +1 -1
  71. package/dist/connectors/index.js +3 -2
  72. package/dist/consolidation-provenance-check.d.ts +1 -1
  73. package/dist/consolidation-undo.d.ts +1 -1
  74. package/dist/contradiction/index.d.ts +1 -1
  75. package/dist/contradiction/index.js +4 -4
  76. package/dist/conversation-index/backend.d.ts +1 -1
  77. package/dist/conversation-index/chunker.d.ts +1 -1
  78. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  79. package/dist/conversation-index/indexer.d.ts +1 -1
  80. package/dist/conversation-index/search.d.ts +1 -1
  81. package/dist/day-summary.d.ts +1 -1
  82. package/dist/delinearize.d.ts +1 -1
  83. package/dist/direct-answer-wiring.d.ts +1 -1
  84. package/dist/direct-answer.d.ts +1 -1
  85. package/dist/embedding-fallback.d.ts +1 -1
  86. package/dist/enrichment/index.d.ts +1 -1
  87. package/dist/entity-retrieval.d.ts +1 -1
  88. package/dist/entity-retrieval.js +3 -2
  89. package/dist/entity-schema.d.ts +1 -1
  90. package/dist/explicit-capture.d.ts +4 -3
  91. package/dist/extraction-judge-telemetry.d.ts +1 -1
  92. package/dist/extraction-judge-training.d.ts +1 -1
  93. package/dist/extraction-judge.d.ts +1 -1
  94. package/dist/extraction.d.ts +1 -1
  95. package/dist/fallback-llm.d.ts +1 -1
  96. package/dist/identity-continuity.d.ts +1 -1
  97. package/dist/importance.d.ts +1 -1
  98. package/dist/index.d.ts +8 -8
  99. package/dist/index.js +49 -45
  100. package/dist/index.js.map +1 -1
  101. package/dist/intent.d.ts +1 -1
  102. package/dist/lcm/engine.d.ts +1 -1
  103. package/dist/lcm/index.d.ts +1 -1
  104. package/dist/lcm/tools.d.ts +1 -1
  105. package/dist/lifecycle.d.ts +1 -1
  106. package/dist/live-connectors-runner.d.ts +1 -1
  107. package/dist/local-llm.d.ts +1 -1
  108. package/dist/maintenance/memory-governance.d.ts +1 -1
  109. package/dist/maintenance/memory-governance.js +3 -2
  110. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -2
  111. package/dist/maintenance/rebuild-memory-projection.js +4 -3
  112. package/dist/mcp-memory-inspector-app.d.ts +5 -4
  113. package/dist/memory-action-policy.d.ts +1 -1
  114. package/dist/memory-cache.d.ts +1 -1
  115. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  116. package/dist/memory-projection-store.d.ts +1 -1
  117. package/dist/memory-provenance.d.ts +1 -1
  118. package/dist/memory-worth-outcomes.d.ts +1 -1
  119. package/dist/models-json.d.ts +1 -1
  120. package/dist/namespaces/migrate.d.ts +1 -1
  121. package/dist/namespaces/migrate.js +4 -3
  122. package/dist/namespaces/principal.d.ts +1 -1
  123. package/dist/namespaces/search.d.ts +1 -1
  124. package/dist/namespaces/storage.d.ts +1 -1
  125. package/dist/namespaces/storage.js +3 -2
  126. package/dist/native-knowledge.d.ts +1 -1
  127. package/dist/operator-toolkit.d.ts +1 -1
  128. package/dist/operator-toolkit.js +7 -6
  129. package/dist/{orchestrator-BexeSJ2j.d.ts → orchestrator-CoqytbK_.d.ts} +102 -10
  130. package/dist/orchestrator.d.ts +4 -3
  131. package/dist/orchestrator.js +12 -10
  132. package/dist/patterns-cli.d.ts +1 -1
  133. package/dist/policy-runtime.d.ts +1 -1
  134. package/dist/qmd-recall-cache.d.ts +1 -1
  135. package/dist/qmd.d.ts +1 -1
  136. package/dist/recall-disclosure-escalation.d.ts +1 -1
  137. package/dist/recall-explain-renderer.d.ts +1 -1
  138. package/dist/recall-planner-llm.d.ts +1 -1
  139. package/dist/recall-state.d.ts +1 -1
  140. package/dist/recall-tag-filter.d.ts +1 -1
  141. package/dist/recall-xray-cli.d.ts +1 -1
  142. package/dist/recall-xray-renderer.d.ts +1 -1
  143. package/dist/recall-xray.d.ts +1 -1
  144. package/dist/resolve-auth-token.d.ts +1 -1
  145. package/dist/resume-bundles.js +2 -2
  146. package/dist/retrieval-agents.d.ts +1 -1
  147. package/dist/retrieval-tiers.d.ts +1 -1
  148. package/dist/routing/engine.d.ts +1 -1
  149. package/dist/routing/store.d.ts +1 -1
  150. package/dist/search/embed-helper.d.ts +1 -1
  151. package/dist/search/factory.d.ts +1 -1
  152. package/dist/search/index.d.ts +1 -1
  153. package/dist/search/lancedb-backend.d.ts +1 -1
  154. package/dist/search/meilisearch-backend.d.ts +1 -1
  155. package/dist/search/noop-backend.d.ts +1 -1
  156. package/dist/search/orama-backend.d.ts +1 -1
  157. package/dist/search/port.d.ts +1 -1
  158. package/dist/search/remote-backend.d.ts +1 -1
  159. package/dist/{semantic-consolidation-PwkzNfdK.d.ts → semantic-consolidation-BPs6BURk.d.ts} +1 -1
  160. package/dist/semantic-consolidation.d.ts +2 -2
  161. package/dist/semantic-consolidation.js +4 -3
  162. package/dist/semantic-rule-promotion.js +3 -2
  163. package/dist/semantic-rule-verifier.d.ts +1 -1
  164. package/dist/semantic-rule-verifier.js +3 -2
  165. package/dist/session-observer-bands.d.ts +1 -1
  166. package/dist/session-observer-state.d.ts +1 -1
  167. package/dist/shared-context/manager.d.ts +1 -1
  168. package/dist/signal.d.ts +1 -1
  169. package/dist/storage.d.ts +38 -2
  170. package/dist/storage.js +6 -3
  171. package/dist/summarizer.d.ts +1 -1
  172. package/dist/summary-snapshot.d.ts +1 -1
  173. package/dist/temporal-supersession.d.ts +1 -1
  174. package/dist/temporal-validity.d.ts +1 -1
  175. package/dist/threading.d.ts +1 -1
  176. package/dist/tier-migration.d.ts +1 -1
  177. package/dist/tier-routing.d.ts +1 -1
  178. package/dist/topics.d.ts +1 -1
  179. package/dist/transcript.d.ts +1 -1
  180. package/dist/{types-BCF2wqKa.d.ts → types-CpMPD8xl.d.ts} +59 -11
  181. package/dist/types.d.ts +1 -1
  182. package/dist/utility-runtime.d.ts +1 -1
  183. package/dist/verified-recall.js +3 -2
  184. package/package.json +1 -1
  185. package/src/orchestrator.ts +74 -0
  186. package/src/storage.ts +100 -0
  187. package/src/wearables/auto-sync.test.ts +181 -0
  188. package/src/wearables/auto-sync.ts +129 -0
  189. package/src/wearables/cli.ts +6 -0
  190. package/src/wearables/config.test.ts +90 -11
  191. package/src/wearables/config.ts +113 -11
  192. package/src/wearables/memory-gen.test.ts +416 -1
  193. package/src/wearables/memory-gen.ts +381 -23
  194. package/src/wearables/pipeline.test.ts +396 -5
  195. package/src/wearables/pipeline.ts +174 -22
  196. package/src/wearables/service.test.ts +172 -0
  197. package/src/wearables/service.ts +84 -3
  198. package/src/wearables/storage-io.test.ts +81 -0
  199. package/src/wearables/trust.test.ts +123 -0
  200. package/src/wearables/trust.ts +168 -0
  201. package/src/wearables/types.ts +57 -9
  202. package/dist/chunk-3GLCUPXP.js.map +0 -1
  203. package/dist/chunk-UGHUNQ74.js.map +0 -1
  204. /package/dist/{chunk-532VCWYW.js.map → chunk-242XFZ36.js.map} +0 -0
  205. /package/dist/{chunk-XXO5TI3B.js.map → chunk-32U3N7H5.js.map} +0 -0
  206. /package/dist/{chunk-KB4MFBF5.js.map → chunk-3RDYU3JS.js.map} +0 -0
  207. /package/dist/{chunk-57QXN2CS.js.map → chunk-4S3N6HFG.js.map} +0 -0
  208. /package/dist/{chunk-GE7Q7KXP.js.map → chunk-7A2QKUUA.js.map} +0 -0
  209. /package/dist/{chunk-3MNBW7R7.js.map → chunk-C4KKM62E.js.map} +0 -0
  210. /package/dist/{chunk-NKCW223V.js.map → chunk-CMN5AWAZ.js.map} +0 -0
  211. /package/dist/{chunk-JXHMAQYT.js.map → chunk-DOBJH4I6.js.map} +0 -0
  212. /package/dist/{chunk-TZDSNIRO.js.map → chunk-IFVFQRZ2.js.map} +0 -0
  213. /package/dist/{chunk-LQYTQCXM.js.map → chunk-JCLECECB.js.map} +0 -0
  214. /package/dist/{chunk-QDV6VAD4.js.map → chunk-LEG7XWS2.js.map} +0 -0
  215. /package/dist/{chunk-N5RGXWLQ.js.map → chunk-PUEAEQSN.js.map} +0 -0
  216. /package/dist/{chunk-JKCDQBDW.js.map → chunk-UXFOGILU.js.map} +0 -0
  217. /package/dist/{chunk-MVQN73GT.js.map → chunk-VTR3MNYF.js.map} +0 -0
  218. /package/dist/{chunk-KVFYTRMV.js.map → chunk-W25I7G6U.js.map} +0 -0
  219. /package/dist/{chunk-3R2UZV3U.js.map → chunk-X7EJF46S.js.map} +0 -0
  220. /package/dist/{chunk-54KDA6UK.js.map → chunk-XG4NAWAV.js.map} +0 -0
  221. /package/dist/{chunk-P2D2MM47.js.map → chunk-YROCXMCK.js.map} +0 -0
@@ -36,6 +36,7 @@ import {
36
36
  writeDailyDigestMemory,
37
37
  type WearableMemoryGenDeps,
38
38
  } from "./memory-gen.js";
39
+ import { tokenizeDayBody, type CorroborationContext } from "./trust.js";
39
40
  import { applyOffTheRecord, compileRedactionPatterns, redactText } from "./redaction.js";
40
41
  import { loadSpeakerRegistry } from "./speakers.js";
41
42
  import {
@@ -51,10 +52,13 @@ import type {
51
52
  WearablesConfig,
52
53
  } from "./types.js";
53
54
 
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;
55
+ /**
56
+ * Pathological-provider backstop on pagination loops. Day fetches are
57
+ * intentionally UNLIMITED for real data a full day must never be
58
+ * truncated — so runaway protection comes from cursor-cycle detection
59
+ * plus this far-out-of-band ceiling, not from a content-sized cap.
60
+ */
61
+ const PAGE_SAFETY_CEILING = 10_000;
58
62
  /** Default lookback window (today + yesterday) for unscoped syncs. */
59
63
  const DEFAULT_SYNC_DAYS = 2;
60
64
  const MAX_SYNC_DAYS = 90;
@@ -80,14 +84,36 @@ export interface WearableSyncDeps {
80
84
  date: string,
81
85
  serialized: string,
82
86
  ): Promise<void>;
83
- /** Optional hook fired once after any day files changed (reindex). */
84
- afterTranscriptsWritten?(): Promise<void>;
87
+ /**
88
+ * Optional hook fired once after the sync wrote ANYTHING the search
89
+ * index should see: day transcripts, created memories, in-place
90
+ * promotions, or native imports. (A cross-source invalidation can
91
+ * promote memories on a run with zero transcript writes — the index
92
+ * must still refresh.)
93
+ */
94
+ afterWrites?(): Promise<void>;
85
95
  /**
86
96
  * Memory-generation dependencies, or null when no extraction engine
87
97
  * is available in this context (transcripts still sync; memory
88
98
  * creation is skipped with a warning when the mode wanted it).
89
99
  */
90
100
  memoryGen: WearableMemoryGenDeps | null;
101
+ /**
102
+ * Same-day transcript bodies from OTHER sources, for cross-device
103
+ * corroboration in smart mode. Absent disables the boost.
104
+ */
105
+ readOtherSourceDayBodies?(
106
+ date: string,
107
+ excludeSource: string,
108
+ ): Promise<Map<string, string>>;
109
+ /**
110
+ * Existing memories usable as support evidence: status "active" or
111
+ * "pending_review" (explicit allow-list — a borderline fact observed
112
+ * again on a later day is repetition signal, and the +0.10 boost is
113
+ * how it earns promotion). Rejected/quarantined/superseded/archived/
114
+ * forgotten rows are never support evidence.
115
+ */
116
+ listSupportMemories?(): Promise<Array<{ id: string; content: string }>>;
91
117
  /** Clock injection for tests. */
92
118
  now?: () => Date;
93
119
  }
@@ -163,23 +189,42 @@ async function fetchAllConversationsForDate(
163
189
  signal: AbortSignal | undefined,
164
190
  warnings: string[],
165
191
  ): Promise<{ conversations: WearableConversation[]; partial: boolean }> {
166
- const conversations: WearableConversation[] = [];
192
+ // Keyed by conversation id: a looping or overlapping provider can
193
+ // re-serve rows it already returned, and appending blindly would
194
+ // store the same conversation twice in the day file (Cursor review
195
+ // on PR #1464). Map keeps first-seen order; a re-served id replaces
196
+ // its entry in place, so the provider's LATEST version of a
197
+ // conversation wins — exactly what the current-day refresh wants.
198
+ const byId = new Map<string, WearableConversation>();
167
199
  let cursor: string | null | undefined = undefined;
168
- for (let page = 0; page < MAX_PAGES_PER_DAY; page++) {
200
+ const seenCursors = new Set<string>();
201
+ const collect = () => [...byId.values()];
202
+ for (let page = 0; page < PAGE_SAFETY_CEILING; page++) {
169
203
  const result = await connector.fetchConversations({
170
204
  date,
171
205
  timezone,
172
206
  cursor,
173
207
  signal,
174
208
  });
175
- conversations.push(...result.conversations);
176
- if (!result.nextCursor) return { conversations, partial: false };
209
+ for (const conversation of result.conversations) {
210
+ byId.set(conversation.id, conversation);
211
+ }
212
+ if (!result.nextCursor) return { conversations: collect(), partial: false };
213
+ // A repeated cursor means the provider's pagination is looping —
214
+ // following it again would refetch the same page forever.
215
+ if (seenCursors.has(result.nextCursor)) {
216
+ warnings.push(
217
+ `${connector.id}: provider pagination repeated cursor on ${date} — stopped to avoid an infinite loop; day may be partially synced (every sync refetches and re-warns while the provider misbehaves)`,
218
+ );
219
+ return { conversations: collect(), partial: true };
220
+ }
221
+ seenCursors.add(result.nextCursor);
177
222
  cursor = result.nextCursor;
178
223
  }
179
224
  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)`,
225
+ `${connector.id}: stopped paginating ${date} after the ${PAGE_SAFETY_CEILING}-page safety ceiling — day may be partially synced (every sync refetches and re-warns while this persists)`,
181
226
  );
182
- return { conversations, partial: true };
227
+ return { conversations: collect(), partial: true };
183
228
  }
184
229
 
185
230
  /** Visible marker appended to day files whose fetch hit the page cap. */
@@ -276,6 +321,8 @@ export async function syncWearableSource(
276
321
  correctionsApplied: 0,
277
322
  transcriptsWritten: [],
278
323
  memoriesCreated: 0,
324
+ memoriesPromoted: 0,
325
+ memoriesDemoted: 0,
279
326
  memoriesSkipped: 0,
280
327
  nativeMemoriesImported: 0,
281
328
  warnings: [],
@@ -380,6 +427,9 @@ export async function syncWearableSource(
380
427
 
381
428
  if (allElided) continue;
382
429
 
430
+ const needsSmartContext =
431
+ settings.memoryMode === "smart" || settings.importNativeMemories === "smart";
432
+
383
433
  // The memory pass runs when the day changed, when forced, or when
384
434
  // the last pass for this exact content didn't complete cleanly —
385
435
  // a sync that stored the transcript but failed mid-memory-write
@@ -406,18 +456,31 @@ export async function syncWearableSource(
406
456
  // re-runs the day.
407
457
  let passClean = false;
408
458
  try {
459
+ const corroboration = needsSmartContext
460
+ ? await buildCorroborationContext(connector.id, date, deps)
461
+ : undefined;
462
+ const dayMemoryGen: WearableMemoryGenDeps = {
463
+ ...deps.memoryGen,
464
+ ...(corroboration !== undefined ? { corroboration } : {}),
465
+ };
409
466
  const generated = await generateWearableMemories(
410
467
  connector.id,
411
468
  date,
412
469
  cleaned.conversations,
413
470
  settings,
414
471
  registry,
415
- deps.memoryGen,
472
+ dayMemoryGen,
416
473
  );
417
474
  summary.memoriesCreated += generated.created;
475
+ summary.memoriesPromoted += generated.promoted;
476
+ summary.memoriesDemoted += generated.demoted;
418
477
  summary.memoriesSkipped += generated.skipped;
419
478
  summary.warnings.push(...generated.warnings);
420
- passClean = generated.warnings.length === 0;
479
+ // Degraded-but-complete passes (e.g. judge unavailable) still
480
+ // record completion — only an aborted extraction should force
481
+ // the day to re-run on the next sync (Cursor review on PR
482
+ // #1462).
483
+ passClean = generated.completed;
421
484
  if (config.digestEnabled) {
422
485
  const wrote = await writeDailyDigestMemory(
423
486
  connector.id,
@@ -448,7 +511,7 @@ export async function syncWearableSource(
448
511
  }
449
512
 
450
513
  if (
451
- settings.importNativeMemories === "review" &&
514
+ settings.importNativeMemories !== "off" &&
452
515
  typeof connector.fetchNativeMemories === "function"
453
516
  ) {
454
517
  if (!deps.memoryGen) {
@@ -459,8 +522,29 @@ export async function syncWearableSource(
459
522
  const alreadyImported = new Set(
460
523
  previousState?.importedNativeMemoryIds ?? [],
461
524
  );
525
+ // Native memories carry no day, so same-day cross-device
526
+ // corroboration does not apply to them — scoring a provider fact
527
+ // against an arbitrary day's tokens would be wrong-day evidence
528
+ // (Cursor review on PR #1462). They keep only the day-independent
529
+ // existing-memory support boost.
530
+ const nativeCorroboration =
531
+ settings.importNativeMemories === "smart"
532
+ ? {
533
+ otherSourceDayTokens: new Map<string, Set<string>>(),
534
+ existingMemories: deps.listSupportMemories
535
+ ? await deps.listSupportMemories()
536
+ : [],
537
+ }
538
+ : undefined;
539
+ const nativeMemoryGen: WearableMemoryGenDeps = {
540
+ ...deps.memoryGen,
541
+ ...(nativeCorroboration !== undefined
542
+ ? { corroboration: nativeCorroboration }
543
+ : {}),
544
+ };
462
545
  let cursor: string | null | undefined = undefined;
463
- for (let page = 0; page < MAX_NATIVE_PAGES; page++) {
546
+ const seenNativeCursors = new Set<string>();
547
+ for (let page = 0; page < PAGE_SAFETY_CEILING; page++) {
464
548
  const result = await connector.fetchNativeMemories({
465
549
  cursor,
466
550
  signal: options.signal,
@@ -469,28 +553,43 @@ export async function syncWearableSource(
469
553
  connector.id,
470
554
  result.memories,
471
555
  alreadyImported,
472
- deps.memoryGen.writer,
556
+ settings,
557
+ nativeMemoryGen,
473
558
  );
559
+ summary.warnings.push(...imported.warnings);
474
560
  summary.nativeMemoriesImported += imported.imported;
475
561
  importedNativeIds.push(...imported.importedIds);
476
562
  for (const id of imported.importedIds) alreadyImported.add(id);
477
563
  if (!result.nextCursor) break;
564
+ if (seenNativeCursors.has(result.nextCursor)) {
565
+ summary.warnings.push(
566
+ `${connector.id}: provider pagination repeated cursor during native-memory import — stopped to avoid an infinite loop; remaining items import on the next sync`,
567
+ );
568
+ break;
569
+ }
570
+ seenNativeCursors.add(result.nextCursor);
478
571
  cursor = result.nextCursor;
479
- if (page === MAX_NATIVE_PAGES - 1) {
572
+ if (page === PAGE_SAFETY_CEILING - 1) {
480
573
  summary.warnings.push(
481
- `${connector.id}: stopped native-memory import after ${MAX_NATIVE_PAGES} pages — remaining items import on the next sync`,
574
+ `${connector.id}: stopped native-memory import at the ${PAGE_SAFETY_CEILING}-page safety ceiling — remaining items import on the next sync`,
482
575
  );
483
576
  }
484
577
  }
485
578
  }
486
579
  }
487
580
 
488
- if (summary.transcriptsWritten.length > 0 && deps.afterTranscriptsWritten) {
581
+ const wroteAnything =
582
+ summary.transcriptsWritten.length > 0 ||
583
+ summary.memoriesCreated > 0 ||
584
+ summary.memoriesPromoted > 0 ||
585
+ summary.memoriesDemoted > 0 ||
586
+ summary.nativeMemoriesImported > 0;
587
+ if (wroteAnything && deps.afterWrites) {
489
588
  try {
490
- await deps.afterTranscriptsWritten();
589
+ await deps.afterWrites();
491
590
  } catch (err) {
492
591
  summary.warnings.push(
493
- `search reindex failed (transcripts are stored and will index on the next update): ${describeErrorForOperator(err)}`,
592
+ `search reindex failed (writes are stored and will index on the next update): ${describeErrorForOperator(err)}`,
494
593
  );
495
594
  }
496
595
  }
@@ -505,11 +604,64 @@ export async function syncWearableSource(
505
604
  clearMemoryDays: failedMemoryDays,
506
605
  importedNativeMemoryIds: importedNativeIds,
507
606
  });
607
+ // New same-day evidence invalidates OTHER sources' memory-pass
608
+ // completion for the days this source just (re)wrote: their next
609
+ // sync re-scores with this transcript available, and the promotion
610
+ // path upgrades earlier borderline writes in place (Cursor review on
611
+ // PR #1462).
612
+ if (summary.transcriptsWritten.length > 0) {
613
+ const cleared: typeof syncState.sources = {};
614
+ for (const [otherId, otherState] of Object.entries(syncState.sources)) {
615
+ if (otherId === connector.id || otherState.memoryDayHashes === undefined) {
616
+ cleared[otherId] = otherState;
617
+ continue;
618
+ }
619
+ const memoryDays = { ...otherState.memoryDayHashes };
620
+ let touched = false;
621
+ for (const date of summary.transcriptsWritten) {
622
+ if (date in memoryDays) {
623
+ delete memoryDays[date];
624
+ touched = true;
625
+ }
626
+ }
627
+ cleared[otherId] = touched
628
+ ? { ...otherState, memoryDayHashes: memoryDays }
629
+ : otherState;
630
+ }
631
+ syncState = { version: 1, sources: cleared };
632
+ }
508
633
  await saveSyncState(deps.memoryDir, syncState);
509
634
 
510
635
  return summary;
511
636
  }
512
637
 
638
+ /**
639
+ * Assemble smart-mode corroboration evidence: other sources' same-day
640
+ * transcript tokens + existing active memories. The memory list loads
641
+ * fresh per day (not per run) so facts written on earlier days of a
642
+ * multi-day backfill are visible as support evidence on later days —
643
+ * the underlying readAllMemories is cached in storage and invalidated
644
+ * by writes, so the per-day refresh is cheap (Cursor review on PR
645
+ * #1462).
646
+ */
647
+ async function buildCorroborationContext(
648
+ sourceId: string,
649
+ date: string,
650
+ deps: WearableSyncDeps,
651
+ ): Promise<CorroborationContext> {
652
+ const otherSourceDayTokens = new Map<string, Set<string>>();
653
+ if (deps.readOtherSourceDayBodies) {
654
+ const bodies = await deps.readOtherSourceDayBodies(date, sourceId);
655
+ for (const [otherSource, body] of bodies) {
656
+ otherSourceDayTokens.set(otherSource, tokenizeDayBody(body));
657
+ }
658
+ }
659
+ const existingMemories = deps.listSupportMemories
660
+ ? await deps.listSupportMemories()
661
+ : [];
662
+ return { otherSourceDayTokens, existingMemories };
663
+ }
664
+
513
665
  export function defaultTimezone(): string {
514
666
  try {
515
667
  return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
@@ -74,6 +74,33 @@ function makeStorage(memoryDir: string): WearableStorageIo & {
74
74
  async hasFactContentHash() {
75
75
  return false;
76
76
  },
77
+ async findWearableMemoryByContent(content: string) {
78
+ const needle = content.trim();
79
+ const match = storage.memories.find(
80
+ (memory) =>
81
+ memory.frontmatter.source.startsWith("wearable:") &&
82
+ memory.content.trim() === needle,
83
+ );
84
+ return match
85
+ ? { id: match.frontmatter.id, status: match.frontmatter.status }
86
+ : null;
87
+ },
88
+ async promoteWearableMemory(id: string) {
89
+ const match = storage.memories.find((memory) => memory.frontmatter.id === id);
90
+ if (!match || match.frontmatter.status !== "pending_review") return false;
91
+ match.frontmatter.status = "active";
92
+ return true;
93
+ },
94
+ async demoteWearableMemory(id: string, attrs: Record<string, string>) {
95
+ const match = storage.memories.find((memory) => memory.frontmatter.id === id);
96
+ if (!match || match.frontmatter.status !== "pending_review") return false;
97
+ match.frontmatter.status = "rejected";
98
+ match.frontmatter.structuredAttributes = {
99
+ ...(match.frontmatter.structuredAttributes ?? {}),
100
+ ...attrs,
101
+ };
102
+ return true;
103
+ },
77
104
  };
78
105
  return storage;
79
106
  }
@@ -313,6 +340,151 @@ test("transcriptMemories filters by wearable source and day", async () => {
313
340
  }
314
341
  });
315
342
 
343
+ test("support corpus includes pending_review rows and excludes terminal statuses", async () => {
344
+ const { registerWearableConnector, clearWearableConnectors } = await import("./registry.js");
345
+ const dir = mkdtempSync(path.join(tmpdir(), "remnic-service-"));
346
+ const borderlineFact =
347
+ "The launch moved to September twelfth after the vendor call.";
348
+ const makeRow = (
349
+ id: string,
350
+ status: string | undefined,
351
+ content: string,
352
+ archivedAt?: string,
353
+ ) => ({
354
+ path: `facts/${id}.md`,
355
+ frontmatter: {
356
+ id,
357
+ source: "wearable:limitless",
358
+ created: "2026-06-09T16:00:00.000Z",
359
+ tags: ["wearable"],
360
+ ...(status !== undefined ? { status } : {}),
361
+ ...(archivedAt !== undefined ? { archivedAt } : {}),
362
+ structuredAttributes: { wearableSource: "limitless" },
363
+ },
364
+ content,
365
+ });
366
+ const runSmartSync = async (
367
+ rows: ReturnType<typeof makeRow>[],
368
+ ): Promise<Record<string, unknown>> => {
369
+ const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-service-mem-")));
370
+ storage.memories.push(...rows);
371
+ const writes: Array<{ options: Record<string, unknown> }> = [];
372
+ storage.writeMemory = (async (
373
+ _category: string,
374
+ _content: string,
375
+ options: Record<string, unknown>,
376
+ ) => {
377
+ writes.push({ options });
378
+ return `mem-${writes.length}`;
379
+ }) as WearableStorageIo["writeMemory"];
380
+ try {
381
+ registerWearableConnector({
382
+ id: "testsource",
383
+ displayName: "Test Source",
384
+ factory: () => ({
385
+ id: "testsource",
386
+ displayName: "Test Source",
387
+ verifyAuth: async () => ({ ok: true }),
388
+ fetchConversations: async () => ({
389
+ conversations: [
390
+ {
391
+ id: "c1",
392
+ source: "testsource",
393
+ startIso: "2026-06-10T15:00:00.000Z",
394
+ endIso: "2026-06-10T15:30:00.000Z",
395
+ segments: [
396
+ { speakerKey: "user", isWearer: true, text: "We are moving the launch to September twelfth after that vendor call wrapped up." },
397
+ { speakerKey: "guest", speakerName: "guest", text: "Confirmed, the vendor is aligned on the September date for the launch." },
398
+ ],
399
+ },
400
+ ],
401
+ nextCursor: null,
402
+ }),
403
+ }),
404
+ });
405
+ const service = new WearablesService({
406
+ config: {
407
+ ...defaultWearablesConfig(),
408
+ enabled: true,
409
+ digestEnabled: false,
410
+ sources: {
411
+ testsource: { ...defaultWearableSourceSettings(), enabled: true, memoryMode: "smart" },
412
+ },
413
+ },
414
+ getStorage: async () => storage,
415
+ // Borderline: 0.75 * 0.8 = 0.6 — active only with +0.10 support.
416
+ extract: async () => ({
417
+ facts: [{ category: "fact", content: borderlineFact, confidence: 0.75, tags: [] }],
418
+ profileUpdates: [],
419
+ entities: [],
420
+ questions: [],
421
+ }),
422
+ searchBackend: null,
423
+ });
424
+ await service.sync({ date: "2026-06-10" });
425
+ assert.equal(writes.length, 1);
426
+ return writes[0].options;
427
+ } finally {
428
+ clearWearableConnectors();
429
+ }
430
+ };
431
+
432
+ try {
433
+ // A pending_review row with matching content IS support evidence.
434
+ // (Similar wording, not identical — identical content would be
435
+ // consumed by the duplicate-existing dedup before scoring.)
436
+ const supported = await runSmartSync([
437
+ makeRow(
438
+ "pending-1",
439
+ "pending_review",
440
+ "The launch moved to September twelfth after the vendor call, noted earlier.",
441
+ ),
442
+ ]);
443
+ assert.equal(supported.status, "active");
444
+ assert.equal(
445
+ (supported.structuredAttributes as Record<string, string>).supportingMemoryId,
446
+ "pending-1",
447
+ );
448
+
449
+ // Terminal statuses with the same content are NOT support evidence.
450
+ const similar =
451
+ "The launch moved to September twelfth after the vendor call, noted earlier.";
452
+ const unsupported = await runSmartSync([
453
+ makeRow("rejected-1", "rejected", similar),
454
+ makeRow("quarantined-1", "quarantined", similar),
455
+ makeRow("superseded-1", "superseded", similar),
456
+ makeRow("archived-1", "archived", similar),
457
+ makeRow("forgotten-1", "forgotten", similar),
458
+ // Archived via archivedAt with NO explicit status — the
459
+ // canonical inferMemoryStatus must resolve this to archived.
460
+ makeRow("archived-implicit-1", undefined, similar, "2026-06-09T00:00:00.000Z"),
461
+ ]);
462
+ assert.equal(unsupported.status, "pending_review");
463
+ assert.equal(
464
+ (unsupported.structuredAttributes as Record<string, string>).supportingMemoryId,
465
+ undefined,
466
+ );
467
+
468
+ // Content matching ONLY through the "[Attributes: ...]" enrichment
469
+ // suffix is not corroboration — the suffix is stripped before
470
+ // token matching, so attribute metadata never grants the boost.
471
+ const suffixOnly = await runSmartSync([
472
+ makeRow(
473
+ "pending-2",
474
+ "pending_review",
475
+ "Unrelated note about quarterly budget planning.\n[Attributes: context: launch moved to September twelfth after the vendor call]",
476
+ ),
477
+ ]);
478
+ assert.equal(suffixOnly.status, "pending_review");
479
+ assert.equal(
480
+ (suffixOnly.structuredAttributes as Record<string, string>).supportingMemoryId,
481
+ undefined,
482
+ );
483
+ } finally {
484
+ rmSync(dir, { recursive: true, force: true });
485
+ }
486
+ });
487
+
316
488
  test("the wearable memory writer dedups non-fact categories by content scan", async () => {
317
489
  const dir = mkdtempSync(path.join(tmpdir(), "remnic-service-"));
318
490
  try {
@@ -12,7 +12,10 @@ import {
12
12
  compileCorrectionRule,
13
13
  } from "./corrections.js";
14
14
  import { describeErrorForOperator, WearablesInputError } from "./errors.js";
15
+ import { inferMemoryStatus } from "../memory-lifecycle-ledger-utils.js";
15
16
  import { isValidTranscriptDate, parseDayTranscript } from "./day-store.js";
17
+ import { stripAttributesSuffix } from "../storage.js";
18
+ import type { MemoryFrontmatter } from "../types.js";
16
19
  import type { WearableMemoryGenDeps } from "./memory-gen.js";
17
20
  import { WEARABLE_SOURCE_PREFIX, wearableSourceLabel } from "./memory-gen.js";
18
21
  import {
@@ -65,6 +68,8 @@ export interface WearableStorageIo {
65
68
  created: string;
66
69
  tags: string[];
67
70
  status?: string;
71
+ /** Archival timestamp — rows with this set are not support. */
72
+ archivedAt?: string;
68
73
  structuredAttributes?: Record<string, string>;
69
74
  };
70
75
  content: string;
@@ -72,6 +77,18 @@ export interface WearableStorageIo {
72
77
  >;
73
78
  writeMemory: WearableMemoryGenDeps["writer"]["writeMemory"];
74
79
  hasFactContentHash(content: string): Promise<boolean>;
80
+ findWearableMemoryByContent(
81
+ content: string,
82
+ ): Promise<{ id: string; status: string | undefined } | null>;
83
+ promoteWearableMemory(
84
+ id: string,
85
+ attributeUpdates: Record<string, string>,
86
+ confidence?: number,
87
+ ): Promise<boolean>;
88
+ demoteWearableMemory(
89
+ id: string,
90
+ attributeUpdates: Record<string, string>,
91
+ ): Promise<boolean>;
75
92
  }
76
93
 
77
94
  export interface WearableSearchBackend {
@@ -87,6 +104,12 @@ export interface WearablesServiceDeps {
87
104
  getStorage(): Promise<WearableStorageIo>;
88
105
  /** Extraction hook; null when no engine is available. */
89
106
  extract: WearableMemoryGenDeps["extract"] | null;
107
+ /**
108
+ * LLM-as-judge hook for smart memoryMode (the orchestrator wires the
109
+ * existing extraction judge here). Absent degrades smart mode to
110
+ * confidence x sourceTrust + corroboration scoring.
111
+ */
112
+ judgeFacts?: WearableMemoryGenDeps["judgeFacts"];
90
113
  /** Search backend (QMD); null disables indexed search. */
91
114
  searchBackend: WearableSearchBackend | null;
92
115
  /** Fired after transcript writes so the search index refreshes. */
@@ -136,15 +159,25 @@ export function createWearableMemoryWriter(
136
159
  ): WearableMemoryGenDeps["writer"] {
137
160
  return {
138
161
  writeMemory: storage.writeMemory.bind(storage),
162
+ findWearableMemoryByContent: async (content: string) =>
163
+ (await storage.findWearableMemoryByContent(content)) as
164
+ | { id: string; status: import("../types.js").MemoryStatus | undefined }
165
+ | null,
166
+ promoteWearableMemory: storage.promoteWearableMemory.bind(storage),
167
+ demoteWearableMemory: storage.demoteWearableMemory.bind(storage),
139
168
  hasFactContentHash: async (content: string) => {
140
169
  if (await storage.hasFactContentHash(content)) return true;
141
- const needle = content.trim();
170
+ // Compare with the "[Attributes: ...]" enrichment suffix removed
171
+ // on BOTH sides — stored wearable bodies carry it, callers pass
172
+ // raw fact text. Without the strip, digest/candidate dedup never
173
+ // matched attribute-bearing memories.
174
+ const needle = stripAttributesSuffix(content);
142
175
  const memories = await storage.readAllMemories();
143
176
  return memories.some(
144
177
  (memory) =>
145
178
  typeof memory.frontmatter.source === "string" &&
146
179
  memory.frontmatter.source.startsWith(`${WEARABLE_SOURCE_PREFIX}:`) &&
147
- memory.content.trim() === needle,
180
+ stripAttributesSuffix(memory.content) === needle,
148
181
  );
149
182
  },
150
183
  };
@@ -263,6 +296,9 @@ export class WearablesService {
263
296
  ? {
264
297
  extract: this.deps.extract,
265
298
  writer: createWearableMemoryWriter(storage),
299
+ ...(this.deps.judgeFacts !== undefined
300
+ ? { judgeFacts: this.deps.judgeFacts }
301
+ : {}),
266
302
  }
267
303
  : null;
268
304
 
@@ -293,8 +329,53 @@ export class WearablesService {
293
329
  },
294
330
  writeDayTranscript: (source, date, serialized) =>
295
331
  storage.writeWearableDayTranscript(source, date, serialized),
296
- afterTranscriptsWritten: this.deps.reindexSearch,
332
+ afterWrites: this.deps.reindexSearch,
297
333
  memoryGen,
334
+ // Cross-device corroboration evidence (smart mode): other
335
+ // sources' stored transcripts for the same day...
336
+ readOtherSourceDayBodies: async (date, excludeSource) => {
337
+ const bodies = new Map<string, string>();
338
+ const days = await storage.listWearableTranscriptDays();
339
+ for (const entry of days) {
340
+ if (entry.date !== date || entry.source === excludeSource) continue;
341
+ if (bodies.size >= 4) break;
342
+ const raw = await storage.readWearableDayTranscript(entry.source, entry.date);
343
+ if (raw === null) continue;
344
+ bodies.set(entry.source, parseDayTranscript(raw)?.body ?? raw);
345
+ }
346
+ return bodies;
347
+ },
348
+ // ...and existing memories for the support boost. Status
349
+ // resolves through the canonical inferMemoryStatus so rows
350
+ // archived via `archivedAt` (or an archive/ path) without an
351
+ // explicit status never count. Explicit allow-list: active
352
+ // rows AND pending_review rows — a borderline fact observed
353
+ // again on a later day is repetition signal and the support
354
+ // boost is how it earns promotion. Rejected/quarantined/
355
+ // superseded/archived/forgotten rows never count (CLAUDE.md
356
+ // rule 53). Bodies feed token matching with the
357
+ // "[Attributes: ...]" enrichment suffix stripped — attribute
358
+ // metadata must never grant corroboration.
359
+ listSupportMemories: async () => {
360
+ const memories = await storage.readAllMemories();
361
+ const support: Array<{ id: string; content: string }> = [];
362
+ for (const memory of memories) {
363
+ // WearableStorageIo narrows MemoryFrontmatter for
364
+ // testability; production hands us the real thing.
365
+ const status = inferMemoryStatus(
366
+ memory.frontmatter as MemoryFrontmatter,
367
+ memory.path,
368
+ );
369
+ if (status !== "active" && status !== "pending_review") {
370
+ continue;
371
+ }
372
+ support.push({
373
+ id: memory.frontmatter.id,
374
+ content: stripAttributesSuffix(memory.content),
375
+ });
376
+ }
377
+ return support;
378
+ },
298
379
  },
299
380
  );
300
381
  summaries.push(summary);