@remnic/core 9.3.640 → 9.3.641

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 (224) hide show
  1. package/dist/access-cli.js +16 -16
  2. package/dist/access-http.d.ts +5 -5
  3. package/dist/access-http.js +9 -9
  4. package/dist/access-mcp.d.ts +5 -5
  5. package/dist/access-mcp.js +8 -8
  6. package/dist/{access-service-jyYute38.d.ts → access-service-DFXIlGvZ.d.ts} +2 -2
  7. package/dist/access-service.d.ts +5 -5
  8. package/dist/access-service.js +7 -7
  9. package/dist/action-confidence.d.ts +2 -2
  10. package/dist/active-memory-bridge.d.ts +2 -2
  11. package/dist/active-recall.d.ts +2 -2
  12. package/dist/behavior-learner.d.ts +2 -2
  13. package/dist/behavior-signals.d.ts +2 -2
  14. package/dist/bootstrap.d.ts +4 -4
  15. package/dist/briefing.d.ts +2 -2
  16. package/dist/briefing.js +3 -3
  17. package/dist/buffer-surprise-report.d.ts +2 -2
  18. package/dist/buffer.d.ts +2 -2
  19. package/dist/bulk-import/index.d.ts +3 -3
  20. package/dist/bulk-import/index.js +2 -2
  21. package/dist/calibration.d.ts +2 -2
  22. package/dist/causal-behavior.d.ts +2 -2
  23. package/dist/causal-consolidation.d.ts +2 -2
  24. package/dist/causal-consolidation.js +4 -4
  25. package/dist/{chunk-WAKPKNEX.js → chunk-3UR4SBZF.js} +307 -70
  26. package/dist/chunk-3UR4SBZF.js.map +1 -0
  27. package/dist/{chunk-RJGPH2Q3.js → chunk-4HYLRROV.js} +2 -2
  28. package/dist/{chunk-3AZDLWDW.js → chunk-4HYWWO3W.js} +3 -3
  29. package/dist/{chunk-ISMZIMD3.js → chunk-4MNDVDBS.js} +2 -2
  30. package/dist/{chunk-JFHEEDRY.js → chunk-5IDVOWBE.js} +3 -3
  31. package/dist/{chunk-4GYGEKUB.js → chunk-6DDBW2V4.js} +4 -4
  32. package/dist/{chunk-LUDUFZTV.js → chunk-ABP6QMVM.js} +2 -2
  33. package/dist/chunk-ABP6QMVM.js.map +1 -0
  34. package/dist/{chunk-UZBK2JR6.js → chunk-ACNU2UBF.js} +16 -16
  35. package/dist/{chunk-UDZYZ5Q5.js → chunk-AK4DSORQ.js} +7 -7
  36. package/dist/{chunk-CMZLMIZI.js → chunk-BEMWL2FZ.js} +2 -2
  37. package/dist/{chunk-3F7RMHC4.js → chunk-C43KEWEV.js} +1 -1
  38. package/dist/{chunk-3F7RMHC4.js.map → chunk-C43KEWEV.js.map} +1 -1
  39. package/dist/{chunk-H2NCNXMS.js → chunk-CKITWVWJ.js} +2 -2
  40. package/dist/{chunk-C4YAIWDG.js → chunk-DM4US77W.js} +2 -2
  41. package/dist/{chunk-SBHQBC37.js → chunk-ECMMM2VE.js} +2 -2
  42. package/dist/{chunk-YMI4NRV7.js → chunk-EGBH5IPJ.js} +4 -4
  43. package/dist/{chunk-SHHT4JTC.js → chunk-GYITUZFT.js} +2 -2
  44. package/dist/{chunk-EKIQBGAY.js → chunk-IENGGY2C.js} +2 -2
  45. package/dist/{chunk-UU5LHD6J.js → chunk-NLPH23IQ.js} +2 -2
  46. package/dist/{chunk-A3UPMOJZ.js → chunk-NV2MGIW7.js} +2 -2
  47. package/dist/{chunk-PU44GBL4.js → chunk-OUELRE5E.js} +1 -1
  48. package/dist/chunk-OUELRE5E.js.map +1 -0
  49. package/dist/{chunk-J2KFC47S.js → chunk-RFSCIBCB.js} +2 -2
  50. package/dist/{chunk-IXSTQGYV.js → chunk-RREI7CNM.js} +2 -2
  51. package/dist/{chunk-C6XO6ONP.js → chunk-SZL2GGFY.js} +2 -2
  52. package/dist/{chunk-7XRSLMUD.js → chunk-THWT7RJU.js} +2 -2
  53. package/dist/{chunk-H4RWKZE2.js → chunk-TKNTAUA3.js} +2 -2
  54. package/dist/{chunk-7OH7KPF5.js → chunk-TTOYT5RB.js} +2 -2
  55. package/dist/{chunk-SEOTBRO4.js → chunk-U3GQ33JC.js} +35 -6
  56. package/dist/chunk-U3GQ33JC.js.map +1 -0
  57. package/dist/{chunk-C3EJ3UIS.js → chunk-WLGE6KEO.js} +3 -3
  58. package/dist/{cli-A4AIhv9e.d.ts → cli-DrL2Nv4j.d.ts} +5 -5
  59. package/dist/cli.d.ts +7 -7
  60. package/dist/cli.js +21 -21
  61. package/dist/compounding/engine.d.ts +2 -2
  62. package/dist/compounding/engine.js +3 -3
  63. package/dist/compounding/preference-consolidator.d.ts +2 -2
  64. package/dist/compression-optimizer.d.ts +2 -2
  65. package/dist/config.d.ts +2 -2
  66. package/dist/connectors/codex-materialize-runner.d.ts +2 -2
  67. package/dist/connectors/codex-materialize-runner.js +3 -3
  68. package/dist/connectors/codex-materialize.d.ts +2 -2
  69. package/dist/connectors/index.d.ts +2 -2
  70. package/dist/connectors/index.js +3 -3
  71. package/dist/consolidation-provenance-check.d.ts +2 -2
  72. package/dist/consolidation-undo.d.ts +2 -2
  73. package/dist/contradiction/index.d.ts +2 -2
  74. package/dist/conversation-index/backend.d.ts +2 -2
  75. package/dist/conversation-index/chunker.d.ts +2 -2
  76. package/dist/conversation-index/faiss-adapter.d.ts +2 -2
  77. package/dist/conversation-index/indexer.d.ts +2 -2
  78. package/dist/conversation-index/search.d.ts +2 -2
  79. package/dist/day-summary.d.ts +2 -2
  80. package/dist/delinearize.d.ts +2 -2
  81. package/dist/direct-answer-wiring.d.ts +2 -2
  82. package/dist/direct-answer.d.ts +2 -2
  83. package/dist/embedding-fallback.d.ts +2 -2
  84. package/dist/enrichment/index.d.ts +2 -2
  85. package/dist/entity-retrieval.d.ts +2 -2
  86. package/dist/entity-retrieval.js +3 -3
  87. package/dist/entity-schema.d.ts +2 -2
  88. package/dist/explicit-capture.d.ts +4 -4
  89. package/dist/extraction-judge-telemetry.d.ts +2 -2
  90. package/dist/extraction-judge-training.d.ts +2 -2
  91. package/dist/extraction-judge.d.ts +2 -2
  92. package/dist/extraction.d.ts +2 -2
  93. package/dist/extraction.js +1 -1
  94. package/dist/fallback-llm.d.ts +2 -2
  95. package/dist/identity-continuity.d.ts +2 -2
  96. package/dist/importance.d.ts +2 -2
  97. package/dist/importers/index.d.ts +2 -2
  98. package/dist/importers/index.js +2 -2
  99. package/dist/index.d.ts +10 -10
  100. package/dist/index.js +28 -28
  101. package/dist/intent.d.ts +2 -2
  102. package/dist/lcm/engine.d.ts +2 -2
  103. package/dist/lcm/index.d.ts +2 -2
  104. package/dist/lcm/tools.d.ts +2 -2
  105. package/dist/lifecycle.d.ts +2 -2
  106. package/dist/live-connectors-runner.d.ts +2 -2
  107. package/dist/local-llm.d.ts +2 -2
  108. package/dist/maintenance/memory-governance.d.ts +2 -2
  109. package/dist/maintenance/memory-governance.js +3 -3
  110. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  111. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  112. package/dist/mcp-memory-inspector-app.d.ts +5 -5
  113. package/dist/memory-action-policy.d.ts +2 -2
  114. package/dist/memory-cache.d.ts +2 -2
  115. package/dist/memory-lifecycle-ledger-utils.d.ts +2 -2
  116. package/dist/memory-projection-store.d.ts +2 -2
  117. package/dist/memory-provenance.d.ts +2 -2
  118. package/dist/memory-worth-outcomes.d.ts +2 -2
  119. package/dist/models-json.d.ts +2 -2
  120. package/dist/namespaces/migrate.d.ts +2 -2
  121. package/dist/namespaces/migrate.js +4 -4
  122. package/dist/namespaces/principal.d.ts +2 -2
  123. package/dist/namespaces/search.d.ts +2 -2
  124. package/dist/namespaces/storage.d.ts +2 -2
  125. package/dist/namespaces/storage.js +3 -3
  126. package/dist/native-knowledge.d.ts +2 -2
  127. package/dist/operator-toolkit.d.ts +2 -2
  128. package/dist/operator-toolkit.js +6 -6
  129. package/dist/{orchestrator-BEDTiuEO.d.ts → orchestrator-DEQW9j0Z.d.ts} +22 -5
  130. package/dist/orchestrator.d.ts +4 -4
  131. package/dist/orchestrator.js +15 -13
  132. package/dist/patterns-cli.d.ts +2 -2
  133. package/dist/{pipeline-D18UAKlN.d.ts → pipeline-BSTuml0d.d.ts} +1 -1
  134. package/dist/policy-runtime.d.ts +2 -2
  135. package/dist/qmd-recall-cache.d.ts +2 -2
  136. package/dist/qmd.d.ts +2 -2
  137. package/dist/recall-disclosure-escalation.d.ts +2 -2
  138. package/dist/recall-explain-renderer.d.ts +2 -2
  139. package/dist/recall-explain-renderer.js +3 -3
  140. package/dist/recall-planner-llm.d.ts +2 -2
  141. package/dist/recall-state.d.ts +2 -2
  142. package/dist/recall-tag-filter.d.ts +2 -2
  143. package/dist/recall-xray-cli.d.ts +2 -2
  144. package/dist/recall-xray-cli.js +4 -4
  145. package/dist/recall-xray-renderer.d.ts +2 -2
  146. package/dist/recall-xray-renderer.js +3 -3
  147. package/dist/recall-xray.d.ts +2 -2
  148. package/dist/recall-xray.js +2 -2
  149. package/dist/resolve-auth-token.d.ts +2 -2
  150. package/dist/retrieval-agents.d.ts +2 -2
  151. package/dist/retrieval-tiers.d.ts +2 -2
  152. package/dist/routing/engine.d.ts +2 -2
  153. package/dist/routing/store.d.ts +2 -2
  154. package/dist/search/embed-helper.d.ts +2 -2
  155. package/dist/search/factory.d.ts +2 -2
  156. package/dist/search/index.d.ts +2 -2
  157. package/dist/search/lancedb-backend.d.ts +2 -2
  158. package/dist/search/meilisearch-backend.d.ts +2 -2
  159. package/dist/search/noop-backend.d.ts +2 -2
  160. package/dist/search/orama-backend.d.ts +2 -2
  161. package/dist/search/port.d.ts +2 -2
  162. package/dist/search/remote-backend.d.ts +2 -2
  163. package/dist/{semantic-consolidation-Byut1ufK.d.ts → semantic-consolidation-DKdYzQOg.d.ts} +1 -1
  164. package/dist/semantic-consolidation.d.ts +3 -3
  165. package/dist/semantic-consolidation.js +4 -4
  166. package/dist/semantic-rule-promotion.js +3 -3
  167. package/dist/semantic-rule-verifier.d.ts +2 -2
  168. package/dist/semantic-rule-verifier.js +3 -3
  169. package/dist/session-observer-bands.d.ts +2 -2
  170. package/dist/session-observer-state.d.ts +2 -2
  171. package/dist/shared-context/manager.d.ts +2 -2
  172. package/dist/signal.d.ts +2 -2
  173. package/dist/storage.d.ts +2 -2
  174. package/dist/storage.js +2 -2
  175. package/dist/summarizer.d.ts +2 -2
  176. package/dist/summary-snapshot.d.ts +2 -2
  177. package/dist/temporal-supersession.d.ts +2 -2
  178. package/dist/temporal-validity.d.ts +2 -2
  179. package/dist/threading.d.ts +2 -2
  180. package/dist/tier-migration.d.ts +2 -2
  181. package/dist/tier-routing.d.ts +2 -2
  182. package/dist/topics.d.ts +2 -2
  183. package/dist/transcript.d.ts +2 -2
  184. package/dist/{types-BliCnURB.d.ts → types-ByK7T3L6.d.ts} +2 -0
  185. package/dist/{types-BzA1FKbg.d.ts → types-D8yUmSik.d.ts} +2 -1
  186. package/dist/types.d.ts +2 -2
  187. package/dist/types.js +1 -1
  188. package/dist/utility-runtime.d.ts +2 -2
  189. package/dist/verified-recall.js +3 -3
  190. package/package.json +1 -1
  191. package/src/bulk-import/types.ts +2 -0
  192. package/src/extraction-timeout.test.ts +6 -1
  193. package/src/extraction.ts +34 -5
  194. package/src/importers/base.ts +1 -1
  195. package/src/orchestrator-flush.test.ts +640 -42
  196. package/src/orchestrator.ts +389 -70
  197. package/src/types.ts +1 -0
  198. package/dist/chunk-LUDUFZTV.js.map +0 -1
  199. package/dist/chunk-PU44GBL4.js.map +0 -1
  200. package/dist/chunk-SEOTBRO4.js.map +0 -1
  201. package/dist/chunk-WAKPKNEX.js.map +0 -1
  202. /package/dist/{chunk-RJGPH2Q3.js.map → chunk-4HYLRROV.js.map} +0 -0
  203. /package/dist/{chunk-3AZDLWDW.js.map → chunk-4HYWWO3W.js.map} +0 -0
  204. /package/dist/{chunk-ISMZIMD3.js.map → chunk-4MNDVDBS.js.map} +0 -0
  205. /package/dist/{chunk-JFHEEDRY.js.map → chunk-5IDVOWBE.js.map} +0 -0
  206. /package/dist/{chunk-4GYGEKUB.js.map → chunk-6DDBW2V4.js.map} +0 -0
  207. /package/dist/{chunk-UZBK2JR6.js.map → chunk-ACNU2UBF.js.map} +0 -0
  208. /package/dist/{chunk-UDZYZ5Q5.js.map → chunk-AK4DSORQ.js.map} +0 -0
  209. /package/dist/{chunk-CMZLMIZI.js.map → chunk-BEMWL2FZ.js.map} +0 -0
  210. /package/dist/{chunk-H2NCNXMS.js.map → chunk-CKITWVWJ.js.map} +0 -0
  211. /package/dist/{chunk-C4YAIWDG.js.map → chunk-DM4US77W.js.map} +0 -0
  212. /package/dist/{chunk-SBHQBC37.js.map → chunk-ECMMM2VE.js.map} +0 -0
  213. /package/dist/{chunk-YMI4NRV7.js.map → chunk-EGBH5IPJ.js.map} +0 -0
  214. /package/dist/{chunk-SHHT4JTC.js.map → chunk-GYITUZFT.js.map} +0 -0
  215. /package/dist/{chunk-EKIQBGAY.js.map → chunk-IENGGY2C.js.map} +0 -0
  216. /package/dist/{chunk-UU5LHD6J.js.map → chunk-NLPH23IQ.js.map} +0 -0
  217. /package/dist/{chunk-A3UPMOJZ.js.map → chunk-NV2MGIW7.js.map} +0 -0
  218. /package/dist/{chunk-J2KFC47S.js.map → chunk-RFSCIBCB.js.map} +0 -0
  219. /package/dist/{chunk-IXSTQGYV.js.map → chunk-RREI7CNM.js.map} +0 -0
  220. /package/dist/{chunk-C6XO6ONP.js.map → chunk-SZL2GGFY.js.map} +0 -0
  221. /package/dist/{chunk-7XRSLMUD.js.map → chunk-THWT7RJU.js.map} +0 -0
  222. /package/dist/{chunk-H4RWKZE2.js.map → chunk-TKNTAUA3.js.map} +0 -0
  223. /package/dist/{chunk-7OH7KPF5.js.map → chunk-TTOYT5RB.js.map} +0 -0
  224. /package/dist/{chunk-C3EJ3UIS.js.map → chunk-WLGE6KEO.js.map} +0 -0
@@ -376,6 +376,42 @@ import type {
376
376
  EntityTimelineEntry,
377
377
  } from "./types.js";
378
378
 
379
+ export interface BulkImportBatchIngestResult {
380
+ attemptedTurnCount: number;
381
+ extractionCount: number;
382
+ persistedCount: number;
383
+ durableOutputCount: number;
384
+ skippedCount: number;
385
+ failedCount: number;
386
+ postPersistMetadataFailureCount: number;
387
+ processedTurnCount: number;
388
+ }
389
+
390
+ export class BulkImportBatchPartialFailureError extends Error {
391
+ readonly partialResult: BulkImportBatchIngestResult;
392
+
393
+ readonly originalError: unknown;
394
+
395
+ constructor(
396
+ message: string,
397
+ partialResult: BulkImportBatchIngestResult,
398
+ originalError: unknown,
399
+ ) {
400
+ super(message);
401
+ this.name = "BulkImportBatchPartialFailureError";
402
+ this.partialResult = partialResult;
403
+ this.originalError = originalError;
404
+ }
405
+ }
406
+
407
+ interface ExtractionRunResult {
408
+ status: "completed" | "skipped";
409
+ reason?: string;
410
+ persistedCount: number;
411
+ durableOutputCount: number;
412
+ postPersistMetadataFailed?: boolean;
413
+ }
414
+
379
415
  export function dedupeEntitySynthesisEvidenceEntries(
380
416
  entries: EntityTimelineEntry[],
381
417
  ): EntityTimelineEntry[] {
@@ -1032,7 +1068,10 @@ function sortSourceValidAtSlicesChronologically(
1032
1068
  .map((slice) => slice.turns);
1033
1069
  }
1034
1070
 
1035
- function splitTurnsBySourceValidAt(turns: readonly BufferTurn[]): BufferTurn[][] {
1071
+ function splitTurnsBySourceValidAt(
1072
+ turns: readonly BufferTurn[],
1073
+ options: { includeContext?: boolean } = {},
1074
+ ): BufferTurn[][] {
1036
1075
  if (turns.length === 0) return [];
1037
1076
  if (!turns.some((turn) => sourceValidAtMs(turn) !== null)) {
1038
1077
  return [[...turns]];
@@ -1051,13 +1090,12 @@ function splitTurnsBySourceValidAt(turns: readonly BufferTurn[]): BufferTurn[][]
1051
1090
  end += 1;
1052
1091
  }
1053
1092
 
1093
+ const contextTurns =
1094
+ options.includeContext === false
1095
+ ? []
1096
+ : sourceValidAtContextTurns(turns, start, end, targetValidAtMs);
1054
1097
  slices.push([
1055
- ...sourceValidAtContextTurns(
1056
- turns,
1057
- start,
1058
- end,
1059
- targetValidAtMs,
1060
- ),
1098
+ ...contextTurns,
1061
1099
  ...turns.slice(start, end).map(asExtractionTargetTurn),
1062
1100
  ]);
1063
1101
  start = end;
@@ -11415,9 +11453,22 @@ export class Orchestrator {
11415
11453
  turns: ImportTurn[],
11416
11454
  options: {
11417
11455
  deadlineMs?: number;
11456
+ failOnExtractionFailure?: boolean;
11457
+ includeSourceValidAtContext?: boolean;
11418
11458
  } = {},
11419
- ): Promise<void> {
11420
- if (!Array.isArray(turns) || turns.length === 0) return;
11459
+ ): Promise<BulkImportBatchIngestResult> {
11460
+ if (!Array.isArray(turns) || turns.length === 0) {
11461
+ return {
11462
+ attemptedTurnCount: 0,
11463
+ extractionCount: 0,
11464
+ persistedCount: 0,
11465
+ durableOutputCount: 0,
11466
+ skippedCount: 0,
11467
+ failedCount: 0,
11468
+ postPersistMetadataFailureCount: 0,
11469
+ processedTurnCount: 0,
11470
+ };
11471
+ }
11421
11472
 
11422
11473
  // Per-batch unique sessionKey keeps threading honest without matching
11423
11474
  // typical prefix/map routing rules. Combined with writeNamespaceOverride
@@ -11426,9 +11477,33 @@ export class Orchestrator {
11426
11477
  // security-context insecure-randomness use even though this value never
11427
11478
  // leaves the process; the bytes just need to be collision-resistant
11428
11479
  // across concurrent bulk-import batches.
11429
- const sessionKey =
11430
- `bulk-import:batch:${Date.now().toString(36)}-` +
11431
- randomBytes(6).toString("hex");
11480
+ const shouldUseStableBatchKey = turns.some(
11481
+ (turn) =>
11482
+ turn.persistProcessedFingerprint === true ||
11483
+ (typeof turn.turnFingerprint === "string" &&
11484
+ turn.turnFingerprint.length > 0),
11485
+ );
11486
+ const stableBatchFingerprint = shouldUseStableBatchKey
11487
+ ? createHash("sha256")
11488
+ .update(
11489
+ turns
11490
+ .map((turn) =>
11491
+ [
11492
+ turn.role,
11493
+ typeof turn.turnFingerprint === "string" &&
11494
+ turn.turnFingerprint.length > 0
11495
+ ? turn.turnFingerprint
11496
+ : turn.content.replace(/\s+/g, " ").trim(),
11497
+ ].join(":"),
11498
+ )
11499
+ .join("\n"),
11500
+ )
11501
+ .digest("hex")
11502
+ .slice(0, 32)
11503
+ : undefined;
11504
+ const sessionKey = stableBatchFingerprint
11505
+ ? `bulk-import:batch:${stableBatchFingerprint}`
11506
+ : `bulk-import:batch:${Date.now().toString(36)}-${randomBytes(6).toString("hex")}`;
11432
11507
 
11433
11508
  const sessionTurns: BufferTurn[] = [];
11434
11509
  for (const turn of turns) {
@@ -11443,9 +11518,22 @@ export class Orchestrator {
11443
11518
  rawContent: turn.rawContent,
11444
11519
  sourceFormat: turn.sourceFormat,
11445
11520
  importProvenance: turn.importProvenance,
11521
+ turnFingerprint: turn.turnFingerprint,
11522
+ persistProcessedFingerprint: turn.persistProcessedFingerprint === true,
11446
11523
  });
11447
11524
  }
11448
- if (sessionTurns.length === 0) return;
11525
+ if (sessionTurns.length === 0) {
11526
+ return {
11527
+ attemptedTurnCount: 0,
11528
+ extractionCount: 0,
11529
+ persistedCount: 0,
11530
+ durableOutputCount: 0,
11531
+ skippedCount: 0,
11532
+ failedCount: 0,
11533
+ postPersistMetadataFailureCount: 0,
11534
+ processedTurnCount: 0,
11535
+ };
11536
+ }
11449
11537
 
11450
11538
  if (this.lcmEngine?.enabled) {
11451
11539
  await this.lcmEngine.observeMessages(
@@ -11460,29 +11548,79 @@ export class Orchestrator {
11460
11548
  );
11461
11549
  }
11462
11550
 
11463
- const importTasks = splitTurnsBySourceValidAt(sessionTurns).map(
11464
- (sessionSlice) =>
11465
- new Promise<void>((resolve, reject) => {
11466
- void this.queueBufferedExtraction(sessionSlice, "trigger_mode", {
11467
- skipDedupeCheck: true,
11468
- clearBufferAfterExtraction: false,
11469
- skipCharThreshold: true,
11470
- skipUserTurnThreshold: true,
11471
- bufferKey: sessionKey,
11472
- extractionDeadlineMs: options.deadlineMs,
11473
- writeNamespaceOverride: this.bulkImportWriteNamespace(),
11474
- onTaskSettled: (err) => (err ? reject(err) : resolve()),
11475
- }).catch(reject);
11476
- }),
11477
- );
11478
- const settled = await Promise.allSettled(importTasks);
11479
- const firstRejected = settled.find(
11480
- (result): result is PromiseRejectedResult =>
11481
- result.status === "rejected",
11482
- );
11551
+ const sessionSlices = splitTurnsBySourceValidAt(sessionTurns, {
11552
+ includeContext: options.includeSourceValidAtContext !== false,
11553
+ });
11554
+ const results: ExtractionRunResult[] = [];
11555
+ let processedTurnCount = 0;
11556
+ let firstRejected: unknown;
11557
+ for (const sessionSlice of sessionSlices) {
11558
+ try {
11559
+ const result = await new Promise<ExtractionRunResult>(
11560
+ (resolve, reject) => {
11561
+ void this.queueBufferedExtraction(sessionSlice, "trigger_mode", {
11562
+ skipDedupeCheck: true,
11563
+ clearBufferAfterExtraction: false,
11564
+ skipCharThreshold: true,
11565
+ skipUserTurnThreshold: true,
11566
+ bufferKey: sessionKey,
11567
+ extractionDeadlineMs: options.deadlineMs,
11568
+ failOnExtractionFailure: options.failOnExtractionFailure === true,
11569
+ writeNamespaceOverride: this.bulkImportWriteNamespace(),
11570
+ onTaskSettled: (err, result) =>
11571
+ err
11572
+ ? reject(err)
11573
+ : resolve(
11574
+ result ?? {
11575
+ status: "skipped",
11576
+ reason: "missing_extraction_result",
11577
+ persistedCount: 0,
11578
+ durableOutputCount: 0,
11579
+ },
11580
+ ),
11581
+ }).catch(reject);
11582
+ },
11583
+ );
11584
+ results.push(result);
11585
+ processedTurnCount += sessionSlice.filter(
11586
+ (turn) => turn.extractionContextOnly !== true,
11587
+ ).length;
11588
+ } catch (err) {
11589
+ firstRejected = err;
11590
+ break;
11591
+ }
11592
+ }
11593
+ const rejectedCount = firstRejected ? 1 : 0;
11594
+ const ingestResult: BulkImportBatchIngestResult = {
11595
+ attemptedTurnCount: sessionTurns.length,
11596
+ extractionCount: results.length,
11597
+ persistedCount: results.reduce(
11598
+ (sum, result) => sum + result.persistedCount,
11599
+ 0,
11600
+ ),
11601
+ durableOutputCount: results.reduce(
11602
+ (sum, result) => sum + result.durableOutputCount,
11603
+ 0,
11604
+ ),
11605
+ skippedCount: results.filter((result) => result.status === "skipped").length,
11606
+ failedCount: rejectedCount,
11607
+ postPersistMetadataFailureCount: results.filter(
11608
+ (result) => result.postPersistMetadataFailed === true,
11609
+ ).length,
11610
+ processedTurnCount:
11611
+ rejectedCount === 0 ? sessionTurns.length : processedTurnCount,
11612
+ };
11483
11613
  if (firstRejected) {
11484
- throw firstRejected.reason;
11614
+ if (processedTurnCount > 0) {
11615
+ throw new BulkImportBatchPartialFailureError(
11616
+ "bulk import failed after partial processing",
11617
+ ingestResult,
11618
+ firstRejected,
11619
+ );
11620
+ }
11621
+ throw firstRejected;
11485
11622
  }
11623
+ return ingestResult;
11486
11624
  }
11487
11625
 
11488
11626
  async observeSessionHeartbeat(
@@ -11564,7 +11702,11 @@ export class Orchestrator {
11564
11702
  skipCharThreshold?: boolean;
11565
11703
  skipUserTurnThreshold?: boolean;
11566
11704
  extractionDeadlineMs?: number;
11567
- onTaskSettled?: (error?: unknown) => void;
11705
+ failOnExtractionFailure?: boolean;
11706
+ onTaskSettled?: (
11707
+ error?: unknown,
11708
+ result?: ExtractionRunResult,
11709
+ ) => void;
11568
11710
  bufferKey?: string;
11569
11711
  abortSignal?: AbortSignal;
11570
11712
  /**
@@ -11583,26 +11725,77 @@ export class Orchestrator {
11583
11725
  !this.shouldQueueExtraction(turnsToExtract, { bufferKey })
11584
11726
  ) {
11585
11727
  log.debug(`extraction dedupe skip: preserving buffer (${reason})`);
11586
- options.onTaskSettled?.();
11728
+ options.onTaskSettled?.(undefined, {
11729
+ status: "skipped",
11730
+ reason: "dedupe",
11731
+ persistedCount: 0,
11732
+ durableOutputCount: 0,
11733
+ });
11587
11734
  return;
11588
11735
  }
11589
11736
 
11737
+ const extractionDeadlineMs =
11738
+ typeof options.extractionDeadlineMs === "number" &&
11739
+ Number.isFinite(options.extractionDeadlineMs)
11740
+ ? options.extractionDeadlineMs
11741
+ : undefined;
11742
+ let timeout: ReturnType<typeof setTimeout> | undefined;
11743
+ let settled = false;
11744
+ const clearQueueWaitTimer = (): void => {
11745
+ if (timeout) {
11746
+ clearTimeout(timeout);
11747
+ timeout = undefined;
11748
+ }
11749
+ };
11750
+ const settleTask = (
11751
+ error?: unknown,
11752
+ result?: ExtractionRunResult,
11753
+ ): boolean => {
11754
+ if (settled) return false;
11755
+ settled = true;
11756
+ clearQueueWaitTimer();
11757
+ options.onTaskSettled?.(error, result);
11758
+ return true;
11759
+ };
11760
+
11761
+ if (typeof extractionDeadlineMs === "number") {
11762
+ const remainingMs = extractionDeadlineMs - Date.now();
11763
+ if (remainingMs <= 0) {
11764
+ settleTask(new Error("replay extraction deadline exceeded (queue_wait)"));
11765
+ return;
11766
+ }
11767
+ timeout = setTimeout(() => {
11768
+ settleTask(new Error("replay extraction deadline exceeded (queue_wait)"));
11769
+ }, remainingMs);
11770
+ }
11771
+
11590
11772
  this.extractionQueue.push(async () => {
11773
+ if (settled) return;
11774
+ if (
11775
+ typeof extractionDeadlineMs === "number" &&
11776
+ extractionDeadlineMs <= Date.now()
11777
+ ) {
11778
+ settleTask(new Error("replay extraction deadline exceeded (queue_wait)"));
11779
+ return;
11780
+ }
11781
+ clearQueueWaitTimer();
11591
11782
  try {
11592
- await this.runExtraction(turnsToExtract, {
11783
+ const result = await this.runExtraction(turnsToExtract, {
11593
11784
  clearBufferAfterExtraction:
11594
11785
  options.clearBufferAfterExtraction ?? true,
11595
11786
  skipCharThreshold: options.skipCharThreshold ?? false,
11596
11787
  skipUserTurnThreshold: options.skipUserTurnThreshold ?? false,
11597
- deadlineMs: options.extractionDeadlineMs,
11788
+ deadlineMs: extractionDeadlineMs,
11598
11789
  bufferKey,
11599
11790
  abortSignal: options.abortSignal,
11791
+ failOnExtractionFailure: options.failOnExtractionFailure === true,
11600
11792
  writeNamespaceOverride: options.writeNamespaceOverride,
11601
11793
  });
11602
- options.onTaskSettled?.();
11794
+ settleTask(undefined, result);
11603
11795
  } catch (err) {
11604
- options.onTaskSettled?.(err);
11605
- throw err;
11796
+ if (settleTask(err)) {
11797
+ throw err;
11798
+ }
11606
11799
  }
11607
11800
  });
11608
11801
 
@@ -11744,6 +11937,7 @@ export class Orchestrator {
11744
11937
  deadlineMs?: number;
11745
11938
  bufferKey?: string;
11746
11939
  abortSignal?: AbortSignal;
11940
+ failOnExtractionFailure?: boolean;
11747
11941
  /**
11748
11942
  * Explicit namespace override for the write path (#460). When set,
11749
11943
  * extraction writes go to this namespace instead of the one derived
@@ -11753,7 +11947,7 @@ export class Orchestrator {
11753
11947
  */
11754
11948
  writeNamespaceOverride?: string;
11755
11949
  } = {},
11756
- ): Promise<void> {
11950
+ ): Promise<ExtractionRunResult> {
11757
11951
  log.debug(`running extraction on ${turns.length} turns`);
11758
11952
  const clearBufferAfterExtraction =
11759
11953
  options.clearBufferAfterExtraction ?? true;
@@ -11787,7 +11981,12 @@ export class Orchestrator {
11787
11981
  if (sessionKey.includes(":cron:")) {
11788
11982
  log.debug(`skipping extraction for cron session: ${sessionKey}`);
11789
11983
  await clearBuffer();
11790
- return;
11984
+ return {
11985
+ status: "skipped",
11986
+ reason: "cron_session",
11987
+ persistedCount: 0,
11988
+ durableOutputCount: 0,
11989
+ };
11791
11990
  }
11792
11991
 
11793
11992
  const normalizedTurns = turns
@@ -11807,7 +12006,12 @@ export class Orchestrator {
11807
12006
  if (targetTurns.length === 0) {
11808
12007
  log.debug("skipping extraction: no non-context turns after normalization");
11809
12008
  await clearBuffer();
11810
- return;
12009
+ return {
12010
+ status: "skipped",
12011
+ reason: "empty_normalized_turns",
12012
+ persistedCount: 0,
12013
+ durableOutputCount: 0,
12014
+ };
11811
12015
  }
11812
12016
  const sourceValidAt = latestSourceValidAtFromTurns(targetTurns);
11813
12017
  throwIfDeadlineExceeded("before_extract");
@@ -11827,7 +12031,12 @@ export class Orchestrator {
11827
12031
  `skipping extraction: below threshold (totalChars=${totalChars}, userTurns=${userTurns.length})`,
11828
12032
  );
11829
12033
  await clearBuffer();
11830
- return;
12034
+ return {
12035
+ status: "skipped",
12036
+ reason: "below_threshold",
12037
+ persistedCount: 0,
12038
+ durableOutputCount: 0,
12039
+ };
11831
12040
  }
11832
12041
 
11833
12042
  const principal = resolvePrincipal(sessionKey, this.config);
@@ -11866,7 +12075,12 @@ export class Orchestrator {
11866
12075
  `runExtraction: skipping already-processed extraction fingerprint for ${bufferKey}`,
11867
12076
  );
11868
12077
  await clearBuffer();
11869
- return;
12078
+ return {
12079
+ status: "skipped",
12080
+ reason: "processed_fingerprint",
12081
+ persistedCount: 0,
12082
+ durableOutputCount: 0,
12083
+ };
11870
12084
  }
11871
12085
 
11872
12086
  // Pass existing entity names so the LLM can reuse them instead of inventing variants
@@ -11882,19 +12096,59 @@ export class Orchestrator {
11882
12096
  throwIfDeadlineExceeded("before_persist");
11883
12097
  throwIfAborted("before_persist");
11884
12098
 
11885
- // Defensive: validate extraction result before processing
12099
+ // Defensive: validate extraction result before processing. Explicit
12100
+ // fail-closed callers, such as flush-plan import, must not observe
12101
+ // malformed extractor output as a successful skip.
11886
12102
  if (!result) {
11887
12103
  log.warn("runExtraction: extraction returned null/undefined");
12104
+ if (options.failOnExtractionFailure) {
12105
+ throw new Error("extraction failed: invalid_extraction_result");
12106
+ }
11888
12107
  await clearBuffer();
11889
- return;
12108
+ return {
12109
+ status: "skipped",
12110
+ reason: "invalid_extraction_result",
12111
+ persistedCount: 0,
12112
+ durableOutputCount: 0,
12113
+ };
11890
12114
  }
11891
- if (!Array.isArray(result.facts)) {
12115
+ const invalidExtractionResultFields = [
12116
+ ["facts", result.facts],
12117
+ ["entities", result.entities],
12118
+ ["questions", result.questions],
12119
+ ["profileUpdates", result.profileUpdates],
12120
+ ]
12121
+ .filter(([, value]) => !Array.isArray(value))
12122
+ .map(([field]) => field);
12123
+ if (invalidExtractionResultFields.length > 0) {
11892
12124
  log.warn(
11893
- "runExtraction: extraction returned invalid facts (not an array)",
11894
- { factsType: typeof result.facts, resultKeys: Object.keys(result) },
12125
+ "runExtraction: extraction returned invalid collection fields",
12126
+ {
12127
+ invalidFields: invalidExtractionResultFields,
12128
+ resultKeys:
12129
+ typeof result === "object" && result !== null
12130
+ ? Object.keys(result)
12131
+ : [],
12132
+ },
11895
12133
  );
12134
+ if (options.failOnExtractionFailure) {
12135
+ throw new Error("extraction failed: invalid_extraction_result");
12136
+ }
11896
12137
  await clearBuffer();
11897
- return;
12138
+ return {
12139
+ status: "skipped",
12140
+ reason: "invalid_extraction_result",
12141
+ persistedCount: 0,
12142
+ durableOutputCount: 0,
12143
+ };
12144
+ }
12145
+ const extractionFailure =
12146
+ typeof result.extractionFailure === "string" &&
12147
+ result.extractionFailure.trim().length > 0
12148
+ ? result.extractionFailure
12149
+ : undefined;
12150
+ if (options.failOnExtractionFailure && extractionFailure) {
12151
+ throw new Error(`extraction failed: ${extractionFailure}`);
11898
12152
  }
11899
12153
  if (
11900
12154
  result.facts.length === 0 &&
@@ -11905,8 +12159,34 @@ export class Orchestrator {
11905
12159
  log.debug(
11906
12160
  "runExtraction: extraction produced no durable outputs; skipping persistence",
11907
12161
  );
12162
+ if (extractionFailure) {
12163
+ log.warn(
12164
+ "runExtraction: extraction reported failure with no durable outputs; not marking fingerprint processed",
12165
+ { extractionFailure },
12166
+ );
12167
+ }
12168
+ if (
12169
+ extractionFingerprint &&
12170
+ shouldPersistProcessedFingerprint &&
12171
+ !extractionFailure
12172
+ ) {
12173
+ meta ??= await storage.loadMeta();
12174
+ await this.recordProcessedExtractionFingerprint(
12175
+ storage,
12176
+ extractionFingerprint,
12177
+ meta,
12178
+ );
12179
+ meta.extractionCount += 1;
12180
+ meta.lastExtractionAt = new Date().toISOString();
12181
+ await storage.saveMeta(meta);
12182
+ }
11908
12183
  await clearBuffer();
11909
- return;
12184
+ return {
12185
+ status: "skipped",
12186
+ reason: "empty_extraction_result",
12187
+ persistedCount: 0,
12188
+ durableOutputCount: 0,
12189
+ };
11910
12190
  }
11911
12191
 
11912
12192
  let threadIdForExtraction: string | null = null;
@@ -11929,6 +12209,7 @@ export class Orchestrator {
11929
12209
  threadIdForExtraction,
11930
12210
  { sessionKey, principal, validAt: sourceValidAt },
11931
12211
  );
12212
+ let postPersistMetadataFailed = false;
11932
12213
  meta ??= await storage.loadMeta();
11933
12214
  if (extractionFingerprint && shouldPersistProcessedFingerprint) {
11934
12215
  try {
@@ -11942,6 +12223,7 @@ export class Orchestrator {
11942
12223
  "runExtraction: failed to persist processed extraction fingerprint; continuing with buffer clear",
11943
12224
  error,
11944
12225
  );
12226
+ postPersistMetadataFailed = true;
11945
12227
  }
11946
12228
  }
11947
12229
  // Persist extraction counters and processed fingerprints before running
@@ -11957,13 +12239,22 @@ export class Orchestrator {
11957
12239
  meta.totalEntities += Array.isArray(result?.entities)
11958
12240
  ? result.entities.length
11959
12241
  : 0;
11960
- let postPersistMetaError: unknown;
11961
12242
  try {
11962
12243
  await storage.saveMeta(meta);
11963
12244
  } catch (error) {
11964
- postPersistMetaError = error;
12245
+ log.warn(
12246
+ "runExtraction: failed to save extraction metadata after durable persistence; continuing with buffer clear",
12247
+ error,
12248
+ );
12249
+ postPersistMetadataFailed = true;
11965
12250
  }
11966
12251
 
12252
+ const durableOutputCount =
12253
+ result.facts.length +
12254
+ result.entities.length +
12255
+ result.questions.length +
12256
+ result.profileUpdates.length;
12257
+
11967
12258
  // Buffer retention for defer verdicts (issue #562, PR 2). When the judge
11968
12259
  // deferred at least one candidate, retain the tail of the current turn
11969
12260
  // window so the next extraction pass has the surrounding context that
@@ -12050,27 +12341,55 @@ export class Orchestrator {
12050
12341
  // Thread title update for the already-established thread context.
12051
12342
  if (this.config.threadingEnabled && threadIdForExtraction) {
12052
12343
  const conversationContent = turns.map((t) => t.content).join(" ");
12053
- await this.threading.updateThreadTitle(
12054
- threadIdForExtraction,
12055
- conversationContent,
12056
- );
12344
+ try {
12345
+ await this.threading.updateThreadTitle(
12346
+ threadIdForExtraction,
12347
+ conversationContent,
12348
+ );
12349
+ } catch (err) {
12350
+ log.warn(
12351
+ "[threading] updateThreadTitle failed after persistence (non-fatal)",
12352
+ err,
12353
+ );
12354
+ }
12057
12355
  }
12058
12356
 
12059
12357
  // Check if consolidation is needed (debounced + non-zero gated).
12060
- const nonZeroExtraction =
12061
- result.facts.length > 0 ||
12062
- result.entities.length > 0 ||
12063
- result.questions.length > 0 ||
12064
- result.profileUpdates.length > 0;
12065
- if (nonZeroExtraction) this.nonZeroExtractionsSinceConsolidation += 1;
12066
- this.maybeScheduleConsolidation(nonZeroExtraction);
12358
+ const nonZeroExtraction = durableOutputCount > 0;
12359
+ try {
12360
+ if (nonZeroExtraction) this.nonZeroExtractionsSinceConsolidation += 1;
12361
+ this.maybeScheduleConsolidation(nonZeroExtraction);
12362
+ } catch (err) {
12363
+ log.warn(
12364
+ "runExtraction: consolidation scheduling failed after persistence (non-fatal)",
12365
+ err,
12366
+ );
12367
+ }
12067
12368
 
12068
- this.requestQmdMaintenance();
12069
- await this.runTierMigrationCycle(storage, "extraction");
12369
+ try {
12370
+ this.requestQmdMaintenance();
12371
+ } catch (err) {
12372
+ log.warn(
12373
+ "runExtraction: QMD maintenance scheduling failed after persistence (non-fatal)",
12374
+ err,
12375
+ );
12376
+ }
12070
12377
 
12071
- if (postPersistMetaError) {
12072
- throw postPersistMetaError;
12378
+ try {
12379
+ await this.runTierMigrationCycle(storage, "extraction");
12380
+ } catch (err) {
12381
+ log.warn(
12382
+ "runExtraction: tier migration failed after persistence (non-fatal)",
12383
+ err,
12384
+ );
12073
12385
  }
12386
+
12387
+ return {
12388
+ status: "completed",
12389
+ persistedCount: persistedIds.length,
12390
+ durableOutputCount,
12391
+ postPersistMetadataFailed,
12392
+ };
12074
12393
  }
12075
12394
 
12076
12395
  private async recordProcessedExtractionFingerprint(
package/src/types.ts CHANGED
@@ -2594,6 +2594,7 @@ export interface ExtractionResult {
2594
2594
  questions: ExtractedQuestion[];
2595
2595
  identityReflection?: string;
2596
2596
  relationships?: ExtractedRelationship[];
2597
+ extractionFailure?: string;
2597
2598
  }
2598
2599
 
2599
2600
  export interface EntityMention {