@remnic/core 9.3.640 → 9.3.642

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 (231) 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 +138 -11
  100. package/dist/index.js +940 -92
  101. package/dist/index.js.map +1 -1
  102. package/dist/intent.d.ts +2 -2
  103. package/dist/lcm/engine.d.ts +2 -2
  104. package/dist/lcm/index.d.ts +2 -2
  105. package/dist/lcm/tools.d.ts +2 -2
  106. package/dist/lifecycle.d.ts +2 -2
  107. package/dist/live-connectors-runner.d.ts +2 -2
  108. package/dist/local-llm.d.ts +2 -2
  109. package/dist/maintenance/memory-governance.d.ts +2 -2
  110. package/dist/maintenance/memory-governance.js +3 -3
  111. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  112. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  113. package/dist/mcp-memory-inspector-app.d.ts +5 -5
  114. package/dist/memory-action-policy.d.ts +2 -2
  115. package/dist/memory-cache.d.ts +2 -2
  116. package/dist/memory-lifecycle-ledger-utils.d.ts +2 -2
  117. package/dist/memory-projection-store.d.ts +2 -2
  118. package/dist/memory-provenance.d.ts +2 -2
  119. package/dist/memory-worth-outcomes.d.ts +2 -2
  120. package/dist/models-json.d.ts +2 -2
  121. package/dist/namespaces/migrate.d.ts +2 -2
  122. package/dist/namespaces/migrate.js +4 -4
  123. package/dist/namespaces/principal.d.ts +2 -2
  124. package/dist/namespaces/search.d.ts +2 -2
  125. package/dist/namespaces/storage.d.ts +2 -2
  126. package/dist/namespaces/storage.js +3 -3
  127. package/dist/native-knowledge.d.ts +2 -2
  128. package/dist/operator-toolkit.d.ts +2 -2
  129. package/dist/operator-toolkit.js +6 -6
  130. package/dist/{orchestrator-BEDTiuEO.d.ts → orchestrator-DEQW9j0Z.d.ts} +22 -5
  131. package/dist/orchestrator.d.ts +4 -4
  132. package/dist/orchestrator.js +15 -13
  133. package/dist/patterns-cli.d.ts +2 -2
  134. package/dist/{pipeline-D18UAKlN.d.ts → pipeline-BSTuml0d.d.ts} +1 -1
  135. package/dist/policy-runtime.d.ts +2 -2
  136. package/dist/qmd-recall-cache.d.ts +2 -2
  137. package/dist/qmd.d.ts +2 -2
  138. package/dist/recall-disclosure-escalation.d.ts +2 -2
  139. package/dist/recall-explain-renderer.d.ts +2 -2
  140. package/dist/recall-explain-renderer.js +3 -3
  141. package/dist/recall-planner-llm.d.ts +2 -2
  142. package/dist/recall-state.d.ts +2 -2
  143. package/dist/recall-tag-filter.d.ts +2 -2
  144. package/dist/recall-xray-cli.d.ts +2 -2
  145. package/dist/recall-xray-cli.js +4 -4
  146. package/dist/recall-xray-renderer.d.ts +2 -2
  147. package/dist/recall-xray-renderer.js +3 -3
  148. package/dist/recall-xray.d.ts +2 -2
  149. package/dist/recall-xray.js +2 -2
  150. package/dist/resolve-auth-token.d.ts +2 -2
  151. package/dist/retrieval-agents.d.ts +2 -2
  152. package/dist/retrieval-tiers.d.ts +2 -2
  153. package/dist/routing/engine.d.ts +2 -2
  154. package/dist/routing/store.d.ts +2 -2
  155. package/dist/search/embed-helper.d.ts +2 -2
  156. package/dist/search/factory.d.ts +2 -2
  157. package/dist/search/index.d.ts +2 -2
  158. package/dist/search/lancedb-backend.d.ts +2 -2
  159. package/dist/search/meilisearch-backend.d.ts +2 -2
  160. package/dist/search/noop-backend.d.ts +2 -2
  161. package/dist/search/orama-backend.d.ts +2 -2
  162. package/dist/search/port.d.ts +2 -2
  163. package/dist/search/remote-backend.d.ts +2 -2
  164. package/dist/{semantic-consolidation-Byut1ufK.d.ts → semantic-consolidation-DKdYzQOg.d.ts} +1 -1
  165. package/dist/semantic-consolidation.d.ts +3 -3
  166. package/dist/semantic-consolidation.js +4 -4
  167. package/dist/semantic-rule-promotion.js +3 -3
  168. package/dist/semantic-rule-verifier.d.ts +2 -2
  169. package/dist/semantic-rule-verifier.js +3 -3
  170. package/dist/session-observer-bands.d.ts +2 -2
  171. package/dist/session-observer-state.d.ts +2 -2
  172. package/dist/shared-context/manager.d.ts +2 -2
  173. package/dist/signal.d.ts +2 -2
  174. package/dist/storage.d.ts +2 -2
  175. package/dist/storage.js +2 -2
  176. package/dist/summarizer.d.ts +2 -2
  177. package/dist/summary-snapshot.d.ts +2 -2
  178. package/dist/temporal-supersession.d.ts +2 -2
  179. package/dist/temporal-validity.d.ts +2 -2
  180. package/dist/threading.d.ts +2 -2
  181. package/dist/tier-migration.d.ts +2 -2
  182. package/dist/tier-routing.d.ts +2 -2
  183. package/dist/topics.d.ts +2 -2
  184. package/dist/transcript.d.ts +2 -2
  185. package/dist/{types-BliCnURB.d.ts → types-ByK7T3L6.d.ts} +2 -0
  186. package/dist/{types-BzA1FKbg.d.ts → types-D8yUmSik.d.ts} +2 -1
  187. package/dist/types.d.ts +2 -2
  188. package/dist/types.js +1 -1
  189. package/dist/utility-runtime.d.ts +2 -2
  190. package/dist/verified-recall.js +3 -3
  191. package/package.json +1 -1
  192. package/src/bulk-import/types.ts +2 -0
  193. package/src/extraction-timeout.test.ts +6 -1
  194. package/src/extraction.ts +34 -5
  195. package/src/importers/base.ts +1 -1
  196. package/src/index.ts +7 -4
  197. package/src/orchestrator-flush.test.ts +640 -42
  198. package/src/orchestrator.ts +389 -70
  199. package/src/session-summaries/adapters.ts +438 -0
  200. package/src/session-summaries/index.ts +401 -0
  201. package/src/session-summaries/redaction.ts +160 -0
  202. package/src/session-summaries/session-summaries.test.ts +1748 -0
  203. package/src/session-summaries/types.ts +130 -0
  204. package/src/types.ts +1 -0
  205. package/dist/chunk-LUDUFZTV.js.map +0 -1
  206. package/dist/chunk-PU44GBL4.js.map +0 -1
  207. package/dist/chunk-SEOTBRO4.js.map +0 -1
  208. package/dist/chunk-WAKPKNEX.js.map +0 -1
  209. /package/dist/{chunk-RJGPH2Q3.js.map → chunk-4HYLRROV.js.map} +0 -0
  210. /package/dist/{chunk-3AZDLWDW.js.map → chunk-4HYWWO3W.js.map} +0 -0
  211. /package/dist/{chunk-ISMZIMD3.js.map → chunk-4MNDVDBS.js.map} +0 -0
  212. /package/dist/{chunk-JFHEEDRY.js.map → chunk-5IDVOWBE.js.map} +0 -0
  213. /package/dist/{chunk-4GYGEKUB.js.map → chunk-6DDBW2V4.js.map} +0 -0
  214. /package/dist/{chunk-UZBK2JR6.js.map → chunk-ACNU2UBF.js.map} +0 -0
  215. /package/dist/{chunk-UDZYZ5Q5.js.map → chunk-AK4DSORQ.js.map} +0 -0
  216. /package/dist/{chunk-CMZLMIZI.js.map → chunk-BEMWL2FZ.js.map} +0 -0
  217. /package/dist/{chunk-H2NCNXMS.js.map → chunk-CKITWVWJ.js.map} +0 -0
  218. /package/dist/{chunk-C4YAIWDG.js.map → chunk-DM4US77W.js.map} +0 -0
  219. /package/dist/{chunk-SBHQBC37.js.map → chunk-ECMMM2VE.js.map} +0 -0
  220. /package/dist/{chunk-YMI4NRV7.js.map → chunk-EGBH5IPJ.js.map} +0 -0
  221. /package/dist/{chunk-SHHT4JTC.js.map → chunk-GYITUZFT.js.map} +0 -0
  222. /package/dist/{chunk-EKIQBGAY.js.map → chunk-IENGGY2C.js.map} +0 -0
  223. /package/dist/{chunk-UU5LHD6J.js.map → chunk-NLPH23IQ.js.map} +0 -0
  224. /package/dist/{chunk-A3UPMOJZ.js.map → chunk-NV2MGIW7.js.map} +0 -0
  225. /package/dist/{chunk-J2KFC47S.js.map → chunk-RFSCIBCB.js.map} +0 -0
  226. /package/dist/{chunk-IXSTQGYV.js.map → chunk-RREI7CNM.js.map} +0 -0
  227. /package/dist/{chunk-C6XO6ONP.js.map → chunk-SZL2GGFY.js.map} +0 -0
  228. /package/dist/{chunk-7XRSLMUD.js.map → chunk-THWT7RJU.js.map} +0 -0
  229. /package/dist/{chunk-H4RWKZE2.js.map → chunk-TKNTAUA3.js.map} +0 -0
  230. /package/dist/{chunk-7OH7KPF5.js.map → chunk-TTOYT5RB.js.map} +0 -0
  231. /package/dist/{chunk-C3EJ3UIS.js.map → chunk-WLGE6KEO.js.map} +0 -0
@@ -1,8 +1,12 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { Orchestrator } from "./orchestrator.js";
3
+ import {
4
+ BulkImportBatchPartialFailureError,
5
+ Orchestrator,
6
+ } from "./orchestrator.js";
4
7
  import { parseConfig } from "./config.js";
5
8
  import type { BufferTurn } from "./types.js";
9
+ import type { ImportTurn } from "./bulk-import/types.js";
6
10
 
7
11
  function makeTurn(sessionKey: string, content: string): BufferTurn {
8
12
  return {
@@ -134,6 +138,304 @@ test("flushSession waits for queued extraction task completion", async () => {
134
138
  assert.equal(flushSettled, true);
135
139
  });
136
140
 
141
+ test("ingestBulkImportBatch rejects when the extraction deadline expires in the queue", async () => {
142
+ const orchestrator = Object.create(Orchestrator.prototype) as any;
143
+ orchestrator.config = parseConfig({});
144
+ orchestrator.extractionQueue = [];
145
+ orchestrator.queueProcessing = true;
146
+ let runExtractionCalls = 0;
147
+ orchestrator.runExtraction = async () => {
148
+ runExtractionCalls += 1;
149
+ };
150
+
151
+ const turns: ImportTurn[] = [
152
+ {
153
+ role: "user",
154
+ timestamp: "2026-06-24T12:00:00.000Z",
155
+ content: "Remember this queued bulk import.",
156
+ },
157
+ ];
158
+ const startedAt = Date.now();
159
+ const outcome = await Promise.race([
160
+ orchestrator
161
+ .ingestBulkImportBatch(turns, {
162
+ deadlineMs: Date.now() + 25,
163
+ failOnExtractionFailure: true,
164
+ })
165
+ .then(
166
+ () => new Error("bulk import unexpectedly resolved"),
167
+ (error: unknown) => error,
168
+ ),
169
+ new Promise<Error>((resolve) =>
170
+ setTimeout(() => resolve(new Error("bulk import did not time out")), 300),
171
+ ),
172
+ ]);
173
+
174
+ assert.ok(outcome instanceof Error);
175
+ assert.match(outcome.message, /deadline exceeded \(queue_wait\)/);
176
+ assert.ok(
177
+ Date.now() - startedAt < 250,
178
+ "bulk import should not wait behind the extraction queue past its deadline",
179
+ );
180
+ assert.equal(runExtractionCalls, 0);
181
+
182
+ const queuedTask = orchestrator.extractionQueue.shift();
183
+ assert.ok(queuedTask);
184
+ await queuedTask();
185
+ assert.equal(
186
+ runExtractionCalls,
187
+ 0,
188
+ "the expired bulk-import task should be a no-op when the queue later drains",
189
+ );
190
+ });
191
+
192
+ test("ingestBulkImportBatch does not report queue wait timeout after extraction starts", async () => {
193
+ const orchestrator = Object.create(Orchestrator.prototype) as any;
194
+ orchestrator.config = parseConfig({});
195
+ orchestrator.extractionQueue = [];
196
+ orchestrator.queueProcessing = false;
197
+ let runExtractionCalls = 0;
198
+ orchestrator.runExtraction = async () => {
199
+ runExtractionCalls += 1;
200
+ await new Promise((resolve) => setTimeout(resolve, 60));
201
+ return {
202
+ status: "completed",
203
+ persistedCount: 1,
204
+ durableOutputCount: 1,
205
+ };
206
+ };
207
+
208
+ const result = await orchestrator.ingestBulkImportBatch(
209
+ [
210
+ {
211
+ role: "user",
212
+ timestamp: "2026-06-24T12:00:00.000Z",
213
+ content: "Remember this active bulk import.",
214
+ },
215
+ ],
216
+ {
217
+ deadlineMs: Date.now() + 25,
218
+ failOnExtractionFailure: true,
219
+ },
220
+ );
221
+
222
+ assert.equal(runExtractionCalls, 1);
223
+ assert.equal(result.extractionCount, 1);
224
+ assert.equal(result.persistedCount, 1);
225
+ assert.equal(result.failedCount, 0);
226
+ });
227
+
228
+ test("ingestBulkImportBatch reports post-persist metadata failures separately", async () => {
229
+ const orchestrator = Object.create(Orchestrator.prototype) as any;
230
+ orchestrator.config = parseConfig({});
231
+ orchestrator.extractionQueue = [];
232
+ orchestrator.queueProcessing = false;
233
+ orchestrator.runExtraction = async () => ({
234
+ status: "completed",
235
+ persistedCount: 1,
236
+ durableOutputCount: 1,
237
+ postPersistMetadataFailed: true,
238
+ });
239
+
240
+ const result = await orchestrator.ingestBulkImportBatch(
241
+ [
242
+ {
243
+ role: "user",
244
+ timestamp: "2026-06-24T12:00:00.000Z",
245
+ content: "Remember this import, but fail the replay metadata marker.",
246
+ turnFingerprint: "flush-plan-fp-1",
247
+ persistProcessedFingerprint: true,
248
+ },
249
+ ],
250
+ {
251
+ failOnExtractionFailure: true,
252
+ },
253
+ );
254
+
255
+ assert.equal(result.extractionCount, 1);
256
+ assert.equal(result.persistedCount, 1);
257
+ assert.equal(result.failedCount, 0);
258
+ assert.equal(result.postPersistMetadataFailureCount, 1);
259
+ });
260
+
261
+ test("ingestBulkImportBatch can disable source-valid-at replay context", async () => {
262
+ const orchestrator = Object.create(Orchestrator.prototype) as any;
263
+ orchestrator.config = parseConfig({});
264
+ orchestrator.extractionQueue = [];
265
+ orchestrator.queueProcessing = false;
266
+ const capturedSlices: BufferTurn[][] = [];
267
+ orchestrator.runExtraction = async (turns: BufferTurn[]) => {
268
+ capturedSlices.push(turns);
269
+ return {
270
+ status: "completed",
271
+ persistedCount: 1,
272
+ durableOutputCount: 1,
273
+ };
274
+ };
275
+
276
+ await orchestrator.ingestBulkImportBatch(
277
+ [
278
+ {
279
+ role: "user",
280
+ timestamp: "2026-06-24T12:00:00.000Z",
281
+ content: "Remember the first chunk.",
282
+ },
283
+ {
284
+ role: "user",
285
+ timestamp: "2026-06-24T12:00:00.001Z",
286
+ content: "Remember the second chunk.",
287
+ },
288
+ {
289
+ role: "user",
290
+ timestamp: "2026-06-24T12:00:00.002Z",
291
+ content: "Remember the third chunk.",
292
+ },
293
+ ],
294
+ {
295
+ includeSourceValidAtContext: false,
296
+ },
297
+ );
298
+
299
+ assert.equal(capturedSlices.length, 3);
300
+ assert.deepEqual(
301
+ capturedSlices.map((slice) =>
302
+ slice.map((turn) => ({
303
+ content: turn.content,
304
+ contextOnly: turn.extractionContextOnly === true,
305
+ })),
306
+ ),
307
+ [
308
+ [{ content: "Remember the first chunk.", contextOnly: false }],
309
+ [{ content: "Remember the second chunk.", contextOnly: false }],
310
+ [{ content: "Remember the third chunk.", contextOnly: false }],
311
+ ],
312
+ );
313
+ });
314
+
315
+ test("ingestBulkImportBatch preserves partial metadata failure before a later slice rejects", async () => {
316
+ const orchestrator = Object.create(Orchestrator.prototype) as any;
317
+ orchestrator.config = parseConfig({});
318
+ orchestrator.extractionQueue = [];
319
+ orchestrator.queueProcessing = false;
320
+ let runExtractionCalls = 0;
321
+ orchestrator.runExtraction = async () => {
322
+ runExtractionCalls += 1;
323
+ if (runExtractionCalls <= 2) {
324
+ return {
325
+ status: "completed",
326
+ persistedCount: 1,
327
+ durableOutputCount: 1,
328
+ postPersistMetadataFailed: runExtractionCalls === 1,
329
+ };
330
+ }
331
+ throw new Error("backend unavailable");
332
+ };
333
+
334
+ const error = await orchestrator
335
+ .ingestBulkImportBatch(
336
+ [
337
+ {
338
+ role: "user",
339
+ timestamp: "2026-06-24T12:00:00.000Z",
340
+ content: "Remember the first chunk.",
341
+ turnFingerprint: "flush-plan-fp-1",
342
+ persistProcessedFingerprint: true,
343
+ },
344
+ {
345
+ role: "user",
346
+ timestamp: "2026-06-24T12:00:00.001Z",
347
+ content: "Remember the second chunk.",
348
+ turnFingerprint: "flush-plan-fp-2",
349
+ persistProcessedFingerprint: true,
350
+ },
351
+ {
352
+ role: "user",
353
+ timestamp: "2026-06-24T12:00:00.002Z",
354
+ content: "Remember the third chunk.",
355
+ turnFingerprint: "flush-plan-fp-3",
356
+ persistProcessedFingerprint: true,
357
+ },
358
+ ],
359
+ {
360
+ failOnExtractionFailure: true,
361
+ },
362
+ )
363
+ .then(
364
+ () => undefined,
365
+ (rejection: unknown) => rejection,
366
+ );
367
+
368
+ assert.ok(error instanceof BulkImportBatchPartialFailureError);
369
+ assert.equal(error.partialResult.attemptedTurnCount, 3);
370
+ assert.equal(error.partialResult.extractionCount, 2);
371
+ assert.equal(error.partialResult.persistedCount, 2);
372
+ assert.equal(error.partialResult.failedCount, 1);
373
+ assert.equal(error.partialResult.postPersistMetadataFailureCount, 1);
374
+ assert.equal(error.partialResult.processedTurnCount, 2);
375
+ assert.equal(runExtractionCalls, 3);
376
+ });
377
+
378
+ test("ingestBulkImportBatch stops after the first failed source-valid-at slice", async () => {
379
+ const orchestrator = Object.create(Orchestrator.prototype) as any;
380
+ orchestrator.config = parseConfig({});
381
+ orchestrator.extractionQueue = [];
382
+ orchestrator.queueProcessing = false;
383
+ let runExtractionCalls = 0;
384
+ orchestrator.runExtraction = async () => {
385
+ runExtractionCalls += 1;
386
+ if (runExtractionCalls === 1) {
387
+ return {
388
+ status: "completed",
389
+ persistedCount: 1,
390
+ durableOutputCount: 1,
391
+ };
392
+ }
393
+ throw new Error("backend unavailable");
394
+ };
395
+
396
+ const error = await orchestrator
397
+ .ingestBulkImportBatch(
398
+ [
399
+ {
400
+ role: "user",
401
+ timestamp: "2026-06-24T12:00:00.000Z",
402
+ content: "Remember the first chunk.",
403
+ turnFingerprint: "flush-plan-fp-1",
404
+ persistProcessedFingerprint: true,
405
+ },
406
+ {
407
+ role: "user",
408
+ timestamp: "2026-06-24T12:00:00.001Z",
409
+ content: "Remember the second chunk.",
410
+ turnFingerprint: "flush-plan-fp-2",
411
+ persistProcessedFingerprint: true,
412
+ },
413
+ {
414
+ role: "user",
415
+ timestamp: "2026-06-24T12:00:00.002Z",
416
+ content: "Remember the third chunk.",
417
+ turnFingerprint: "flush-plan-fp-3",
418
+ persistProcessedFingerprint: true,
419
+ },
420
+ ],
421
+ {
422
+ failOnExtractionFailure: true,
423
+ },
424
+ )
425
+ .then(
426
+ () => undefined,
427
+ (rejection: unknown) => rejection,
428
+ );
429
+
430
+ assert.ok(error instanceof BulkImportBatchPartialFailureError);
431
+ assert.equal(error.partialResult.attemptedTurnCount, 3);
432
+ assert.equal(error.partialResult.extractionCount, 1);
433
+ assert.equal(error.partialResult.persistedCount, 1);
434
+ assert.equal(error.partialResult.failedCount, 1);
435
+ assert.equal(error.partialResult.processedTurnCount, 1);
436
+ assert.equal(runExtractionCalls, 2);
437
+ });
438
+
137
439
  test("processTurn preserves the original sessionKey on buffered turns", async () => {
138
440
  const orchestrator = Object.create(Orchestrator.prototype) as any;
139
441
  let capturedTurn: BufferTurn | undefined;
@@ -396,6 +698,298 @@ test("runExtraction skips batches whose persisted fingerprint already exists in
396
698
  assert.equal(clearCalls, 1);
397
699
  });
398
700
 
701
+ test("runExtraction fails closed on invalid extraction results when required", async () => {
702
+ const cases: Array<[string, unknown]> = [
703
+ ["null", null],
704
+ [
705
+ "malformed collections",
706
+ {
707
+ facts: "not an array",
708
+ entities: [],
709
+ questions: [],
710
+ profileUpdates: [],
711
+ },
712
+ ],
713
+ ];
714
+
715
+ for (const [label, extractionResult] of cases) {
716
+ const config = parseConfig({});
717
+ config.extractionMinChars = 0;
718
+ config.extractionMinUserTurns = 1;
719
+
720
+ let clearCalls = 0;
721
+ let persistCalls = 0;
722
+ const orchestrator = Object.create(Orchestrator.prototype) as any;
723
+ orchestrator.config = config;
724
+ orchestrator.buffer = {
725
+ clearAfterExtraction: async () => {
726
+ clearCalls += 1;
727
+ },
728
+ };
729
+ orchestrator.storageRouter = {
730
+ storageFor: async () => ({
731
+ listEntityNames: async () => [],
732
+ loadMeta: async () => ({
733
+ extractionCount: 0,
734
+ lastExtractionAt: null,
735
+ lastConsolidationAt: null,
736
+ totalMemories: 0,
737
+ totalEntities: 0,
738
+ processedExtractionFingerprints: [],
739
+ }),
740
+ saveMeta: async () => undefined,
741
+ }),
742
+ };
743
+ orchestrator.extraction = {
744
+ extract: async () => extractionResult,
745
+ };
746
+ orchestrator.persistExtraction = async () => {
747
+ persistCalls += 1;
748
+ return ["fact-1"];
749
+ };
750
+
751
+ await assert.rejects(
752
+ orchestrator.runExtraction([makeTurn("session-invalid", "remember bad output")], {
753
+ bufferKey: `bulk-import:batch:${label}`,
754
+ failOnExtractionFailure: true,
755
+ }),
756
+ /extraction failed: invalid_extraction_result/,
757
+ );
758
+
759
+ assert.equal(clearCalls, 0);
760
+ assert.equal(persistCalls, 0);
761
+ }
762
+ });
763
+
764
+ test("runExtraction persists processed fingerprints for empty extraction results", async () => {
765
+ const config = parseConfig({});
766
+ config.extractionMinChars = 0;
767
+ config.extractionMinUserTurns = 1;
768
+
769
+ let clearCalls = 0;
770
+ let persistCalls = 0;
771
+ let saveMetaCalls = 0;
772
+ let savedMeta:
773
+ | {
774
+ extractionCount: number;
775
+ lastExtractionAt: string | null;
776
+ totalMemories: number;
777
+ totalEntities: number;
778
+ processedExtractionFingerprints: Array<{
779
+ fingerprint: string;
780
+ observedAt: string;
781
+ }>;
782
+ }
783
+ | undefined;
784
+
785
+ const meta = {
786
+ extractionCount: 0,
787
+ lastExtractionAt: null,
788
+ lastConsolidationAt: null,
789
+ totalMemories: 0,
790
+ totalEntities: 0,
791
+ processedExtractionFingerprints: [] as Array<{
792
+ fingerprint: string;
793
+ observedAt: string;
794
+ }>,
795
+ };
796
+
797
+ const orchestrator = Object.create(Orchestrator.prototype) as any;
798
+ orchestrator.config = config;
799
+ orchestrator.buffer = {
800
+ clearAfterExtraction: async () => {
801
+ clearCalls += 1;
802
+ },
803
+ };
804
+ orchestrator.storageRouter = {
805
+ storageFor: async () => ({
806
+ listEntityNames: async () => [],
807
+ loadMeta: async () => meta,
808
+ saveMeta: async (nextMeta: typeof meta) => {
809
+ saveMetaCalls += 1;
810
+ savedMeta = structuredClone(nextMeta);
811
+ },
812
+ }),
813
+ };
814
+ orchestrator.extraction = {
815
+ extract: async () => ({
816
+ facts: [],
817
+ entities: [],
818
+ questions: [],
819
+ profileUpdates: [],
820
+ }),
821
+ };
822
+ orchestrator.persistExtraction = async () => {
823
+ persistCalls += 1;
824
+ return [];
825
+ };
826
+
827
+ const turns = [
828
+ {
829
+ ...makeTurn("session-empty", "transient note not worth remembering"),
830
+ logicalSessionKey: "logical-thread:empty",
831
+ turnFingerprint: "fp-empty",
832
+ persistProcessedFingerprint: true,
833
+ },
834
+ ];
835
+ const result = await orchestrator.runExtraction(turns, {
836
+ bufferKey: "logical-thread:empty",
837
+ failOnExtractionFailure: true,
838
+ });
839
+
840
+ assert.equal(result.status, "skipped");
841
+ assert.equal(result.reason, "empty_extraction_result");
842
+ assert.equal(persistCalls, 0);
843
+ assert.equal(saveMetaCalls, 1);
844
+ assert.equal(clearCalls, 1);
845
+ assert.equal(savedMeta?.extractionCount, 1);
846
+ assert.equal(savedMeta?.totalMemories, 0);
847
+ assert.equal(savedMeta?.totalEntities, 0);
848
+ assert.equal(savedMeta?.processedExtractionFingerprints.length, 1);
849
+ assert.equal(
850
+ savedMeta?.processedExtractionFingerprints[0]?.fingerprint,
851
+ orchestrator.buildExtractionFingerprint(turns, "logical-thread:empty"),
852
+ );
853
+ });
854
+
855
+ test("runExtraction does not persist processed fingerprints for failed empty extraction results", async () => {
856
+ const config = parseConfig({});
857
+ config.extractionMinChars = 0;
858
+ config.extractionMinUserTurns = 1;
859
+
860
+ let clearCalls = 0;
861
+ let persistCalls = 0;
862
+ let saveMetaCalls = 0;
863
+
864
+ const meta = {
865
+ extractionCount: 0,
866
+ lastExtractionAt: null,
867
+ lastConsolidationAt: null,
868
+ totalMemories: 0,
869
+ totalEntities: 0,
870
+ processedExtractionFingerprints: [] as Array<{
871
+ fingerprint: string;
872
+ observedAt: string;
873
+ }>,
874
+ };
875
+
876
+ const orchestrator = Object.create(Orchestrator.prototype) as any;
877
+ orchestrator.config = config;
878
+ orchestrator.buffer = {
879
+ clearAfterExtraction: async () => {
880
+ clearCalls += 1;
881
+ },
882
+ };
883
+ orchestrator.storageRouter = {
884
+ storageFor: async () => ({
885
+ listEntityNames: async () => [],
886
+ loadMeta: async () => meta,
887
+ saveMeta: async () => {
888
+ saveMetaCalls += 1;
889
+ },
890
+ }),
891
+ };
892
+ orchestrator.extraction = {
893
+ extract: async () => ({
894
+ facts: [],
895
+ entities: [],
896
+ questions: [],
897
+ profileUpdates: [],
898
+ extractionFailure: "gateway_unavailable",
899
+ }),
900
+ };
901
+ orchestrator.persistExtraction = async () => {
902
+ persistCalls += 1;
903
+ return [];
904
+ };
905
+
906
+ const turns = [
907
+ {
908
+ ...makeTurn("session-empty-failed", "remember failed gateway output"),
909
+ logicalSessionKey: "logical-thread:empty-failed",
910
+ turnFingerprint: "fp-empty-failed",
911
+ persistProcessedFingerprint: true,
912
+ },
913
+ ];
914
+ const result = await orchestrator.runExtraction(turns, {
915
+ bufferKey: "logical-thread:empty-failed",
916
+ });
917
+
918
+ assert.equal(result.status, "skipped");
919
+ assert.equal(result.reason, "empty_extraction_result");
920
+ assert.equal(persistCalls, 0);
921
+ assert.equal(saveMetaCalls, 0);
922
+ assert.equal(clearCalls, 1);
923
+ assert.deepEqual(meta.processedExtractionFingerprints, []);
924
+ });
925
+
926
+ test("runExtraction preserves empty-result buffers when fingerprint persistence fails", async () => {
927
+ const config = parseConfig({});
928
+ config.extractionMinChars = 0;
929
+ config.extractionMinUserTurns = 1;
930
+
931
+ let clearCalls = 0;
932
+ let saveMetaCalls = 0;
933
+ const meta = {
934
+ extractionCount: 0,
935
+ lastExtractionAt: null,
936
+ lastConsolidationAt: null,
937
+ totalMemories: 0,
938
+ totalEntities: 0,
939
+ processedExtractionFingerprints: [] as Array<{
940
+ fingerprint: string;
941
+ observedAt: string;
942
+ }>,
943
+ };
944
+
945
+ const orchestrator = Object.create(Orchestrator.prototype) as any;
946
+ orchestrator.config = config;
947
+ orchestrator.buffer = {
948
+ clearAfterExtraction: async () => {
949
+ clearCalls += 1;
950
+ },
951
+ };
952
+ orchestrator.storageRouter = {
953
+ storageFor: async () => ({
954
+ listEntityNames: async () => [],
955
+ loadMeta: async () => meta,
956
+ saveMeta: async () => {
957
+ saveMetaCalls += 1;
958
+ throw new Error("meta save failed");
959
+ },
960
+ }),
961
+ };
962
+ orchestrator.extraction = {
963
+ extract: async () => ({
964
+ facts: [],
965
+ entities: [],
966
+ questions: [],
967
+ profileUpdates: [],
968
+ }),
969
+ };
970
+
971
+ await assert.rejects(
972
+ orchestrator.runExtraction(
973
+ [
974
+ {
975
+ ...makeTurn("session-empty-fail", "transient note not worth remembering"),
976
+ logicalSessionKey: "logical-thread:empty-fail",
977
+ turnFingerprint: "fp-empty-fail",
978
+ persistProcessedFingerprint: true,
979
+ },
980
+ ],
981
+ {
982
+ bufferKey: "logical-thread:empty-fail",
983
+ failOnExtractionFailure: true,
984
+ },
985
+ ),
986
+ /meta save failed/,
987
+ );
988
+
989
+ assert.equal(saveMetaCalls, 1);
990
+ assert.equal(clearCalls, 0);
991
+ });
992
+
399
993
  test("runExtraction preserves deduped buffers when the caller aborts before clearing", async () => {
400
994
  const config = parseConfig({});
401
995
  config.extractionMinChars = 0;
@@ -734,7 +1328,7 @@ test("runExtraction loads meta before updating extraction counters when fingerpr
734
1328
  assert.equal(savedMeta?.processedExtractionFingerprints.length, 0);
735
1329
  });
736
1330
 
737
- test("runExtraction saves the processed fingerprint before late threading failures", async () => {
1331
+ test("runExtraction completes after late threading failures and saves the processed fingerprint", async () => {
738
1332
  const config = parseConfig({});
739
1333
  config.extractionMinChars = 0;
740
1334
  config.extractionMinUserTurns = 1;
@@ -807,26 +1401,29 @@ test("runExtraction saves the processed fingerprint before late threading failur
807
1401
  throw new Error("thread title failed");
808
1402
  },
809
1403
  };
1404
+ orchestrator.maybeScheduleConsolidation = () => undefined;
810
1405
  orchestrator.requestQmdMaintenance = () => undefined;
811
1406
  orchestrator.runTierMigrationCycle = async () => undefined;
1407
+ orchestrator.nonZeroExtractionsSinceConsolidation = 0;
812
1408
 
813
- await assert.rejects(
814
- orchestrator.runExtraction(
815
- [
816
- {
817
- ...makeTurn("session-f", "remember eta"),
818
- logicalSessionKey: "logical-thread:thread-15",
819
- turnFingerprint: "fp-thread-15",
820
- persistProcessedFingerprint: true,
821
- },
822
- ],
1409
+ const result = await orchestrator.runExtraction(
1410
+ [
823
1411
  {
824
- bufferKey: "logical-thread:thread-15",
1412
+ ...makeTurn("session-f", "remember eta"),
1413
+ logicalSessionKey: "logical-thread:thread-15",
1414
+ turnFingerprint: "fp-thread-15",
1415
+ persistProcessedFingerprint: true,
825
1416
  },
826
- ),
827
- /thread title failed/,
1417
+ ],
1418
+ {
1419
+ bufferKey: "logical-thread:thread-15",
1420
+ },
828
1421
  );
829
1422
 
1423
+ assert.equal(result.status, "completed");
1424
+ assert.equal(result.persistedCount, 1);
1425
+ assert.equal(result.durableOutputCount, 1);
1426
+ assert.equal(result.postPersistMetadataFailed, false);
830
1427
  assert.equal(saveMetaCalls, 1);
831
1428
  assert.equal(clearCalls, 1);
832
1429
  assert.equal(savedMeta?.processedExtractionFingerprints.length, 1);
@@ -846,7 +1443,7 @@ test("runExtraction saves the processed fingerprint before late threading failur
846
1443
  );
847
1444
  });
848
1445
 
849
- test("runExtraction clears the buffer even when the post-persist meta save fails", async () => {
1446
+ test("runExtraction completes and clears the buffer when the post-persist meta save fails", async () => {
850
1447
  const config = parseConfig({});
851
1448
  config.extractionMinChars = 0;
852
1449
  config.extractionMinUserTurns = 1;
@@ -902,23 +1499,23 @@ test("runExtraction clears the buffer even when the post-persist meta save fails
902
1499
  orchestrator.requestQmdMaintenance = () => undefined;
903
1500
  orchestrator.runTierMigrationCycle = async () => undefined;
904
1501
 
905
- await assert.rejects(
906
- orchestrator.runExtraction(
907
- [
908
- {
909
- ...makeTurn("session-g", "remember theta"),
910
- logicalSessionKey: "logical-thread:thread-16",
911
- turnFingerprint: "fp-thread-16",
912
- persistProcessedFingerprint: true,
913
- },
914
- ],
1502
+ const result = await orchestrator.runExtraction(
1503
+ [
915
1504
  {
916
- bufferKey: "logical-thread:thread-16",
1505
+ ...makeTurn("session-g", "remember theta"),
1506
+ logicalSessionKey: "logical-thread:thread-16",
1507
+ turnFingerprint: "fp-thread-16",
1508
+ persistProcessedFingerprint: true,
917
1509
  },
918
- ),
919
- /meta save failed/,
1510
+ ],
1511
+ {
1512
+ bufferKey: "logical-thread:thread-16",
1513
+ },
920
1514
  );
921
1515
 
1516
+ assert.equal(result.status, "completed");
1517
+ assert.equal(result.persistedCount, 1);
1518
+ assert.equal(result.postPersistMetadataFailed, true);
922
1519
  assert.equal(saveMetaCalls, 1);
923
1520
  assert.equal(clearCalls, 1);
924
1521
  });
@@ -1004,26 +1601,27 @@ test("runExtraction still runs follow-on extraction helpers when the post-persis
1004
1601
  };
1005
1602
  orchestrator.runTierMigrationCycle = async () => {
1006
1603
  tierCalls += 1;
1604
+ throw new Error("tier migration failed");
1007
1605
  };
1008
1606
  orchestrator.nonZeroExtractionsSinceConsolidation = 0;
1009
1607
 
1010
- await assert.rejects(
1011
- orchestrator.runExtraction(
1012
- [
1013
- {
1014
- ...makeTurn("session-h", "remember iota"),
1015
- logicalSessionKey: "logical-thread:thread-17",
1016
- turnFingerprint: "fp-thread-17",
1017
- persistProcessedFingerprint: true,
1018
- },
1019
- ],
1608
+ const result = await orchestrator.runExtraction(
1609
+ [
1020
1610
  {
1021
- bufferKey: "logical-thread:thread-17",
1611
+ ...makeTurn("session-h", "remember iota"),
1612
+ logicalSessionKey: "logical-thread:thread-17",
1613
+ turnFingerprint: "fp-thread-17",
1614
+ persistProcessedFingerprint: true,
1022
1615
  },
1023
- ),
1024
- /meta save failed/,
1616
+ ],
1617
+ {
1618
+ bufferKey: "logical-thread:thread-17",
1619
+ },
1025
1620
  );
1026
1621
 
1622
+ assert.equal(result.status, "completed");
1623
+ assert.equal(result.persistedCount, 1);
1624
+ assert.equal(result.postPersistMetadataFailed, true);
1027
1625
  assert.equal(clearCalls, 1);
1028
1626
  assert.equal(boxCalls, 1);
1029
1627
  assert.equal(appendCalls, 1);