@remnic/core 9.47.1 → 9.48.1

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 (233) hide show
  1. package/dist/access-admin-ops-surface.d.ts +6 -5
  2. package/dist/access-admin-ops-surface.js +27 -25
  3. package/dist/access-audit.js +2 -2
  4. package/dist/access-authorization-probe.d.ts +6 -5
  5. package/dist/access-authorization-probe.js +28 -26
  6. package/dist/access-boundary.d.ts +6 -5
  7. package/dist/access-boundary.js +27 -25
  8. package/dist/access-cli.js +87 -85
  9. package/dist/access-cli.js.map +1 -1
  10. package/dist/access-extraction-force-flush.d.ts +6 -5
  11. package/dist/access-extraction-force-flush.js +27 -25
  12. package/dist/access-http-lcm-compaction.d.ts +6 -5
  13. package/dist/access-http-lifecycle-flush.d.ts +6 -5
  14. package/dist/access-http-offline-stream.d.ts +6 -5
  15. package/dist/access-http-query.js +27 -25
  16. package/dist/access-http.d.ts +6 -5
  17. package/dist/access-http.js +36 -34
  18. package/dist/access-identity-continuity-surface.d.ts +5 -4
  19. package/dist/access-identity-continuity-surface.js +27 -25
  20. package/dist/access-lcm-surface.d.ts +6 -5
  21. package/dist/access-lcm-surface.js +27 -25
  22. package/dist/access-mcp.d.ts +6 -5
  23. package/dist/access-mcp.js +32 -30
  24. package/dist/access-namespace-preflight.js +27 -25
  25. package/dist/access-observe-write-surface.d.ts +6 -5
  26. package/dist/access-observe-write-surface.js +27 -25
  27. package/dist/access-offline-manifest.d.ts +5 -4
  28. package/dist/access-offline-manifest.js +4 -4
  29. package/dist/access-operations-batch.js +29 -27
  30. package/dist/access-operations.d.ts +6 -5
  31. package/dist/access-operations.js +31 -29
  32. package/dist/access-recall-concurrency.d.ts +6 -5
  33. package/dist/access-recall-concurrency.js +27 -25
  34. package/dist/access-recall-response.d.ts +6 -5
  35. package/dist/access-recall-response.js +27 -25
  36. package/dist/access-recall-surface.d.ts +11 -10
  37. package/dist/access-recall-surface.js +27 -25
  38. package/dist/access-schema.d.ts +2 -2
  39. package/dist/{access-service-DNBYHK33.d.ts → access-service-andNjwfD.d.ts} +3 -3
  40. package/dist/access-service-helpers.d.ts +6 -5
  41. package/dist/access-service.d.ts +10 -9
  42. package/dist/access-service.js +27 -25
  43. package/dist/access-surface-catalog.d.ts +6 -5
  44. package/dist/access-wearables-meetings-surface.d.ts +3 -2
  45. package/dist/adapters/index.js +4 -4
  46. package/dist/adapters/registry.js +2 -2
  47. package/dist/artifact-search.d.ts +37 -0
  48. package/dist/artifact-search.js +13 -0
  49. package/dist/artifact-search.js.map +1 -0
  50. package/dist/bootstrap.d.ts +5 -4
  51. package/dist/briefing.d.ts +2 -1
  52. package/dist/briefing.js +10 -7
  53. package/dist/buffer.d.ts +2 -1
  54. package/dist/buffer.js +4 -4
  55. package/dist/causal-consolidation.js +11 -8
  56. package/dist/causal-consolidation.js.map +1 -1
  57. package/dist/{chunk-T6JRMVOV.js → chunk-2EJNXE53.js} +2 -2
  58. package/dist/{chunk-SIP5DBDN.js → chunk-2P3GQVM2.js} +152 -130
  59. package/dist/chunk-2P3GQVM2.js.map +1 -0
  60. package/dist/{chunk-64BHFFKZ.js → chunk-2RPJ4NEE.js} +2 -2
  61. package/dist/{chunk-JFO5QZ7N.js → chunk-3JUYWPFJ.js} +2 -2
  62. package/dist/{chunk-L7G72WY4.js → chunk-3NDN5P55.js} +2 -2
  63. package/dist/{chunk-IXH2U2QT.js → chunk-45OTV7FZ.js} +19 -19
  64. package/dist/chunk-4O4TKIXQ.js +171 -0
  65. package/dist/chunk-4O4TKIXQ.js.map +1 -0
  66. package/dist/{chunk-BDNL34CT.js → chunk-4WBGZYZZ.js} +33 -8
  67. package/dist/chunk-4WBGZYZZ.js.map +1 -0
  68. package/dist/{chunk-U6JGQ37L.js → chunk-52I3YZZ6.js} +3 -3
  69. package/dist/{chunk-IZQ5TDFT.js → chunk-53AJILXF.js} +3 -3
  70. package/dist/chunk-5AUY5Z5E.js +34 -0
  71. package/dist/chunk-5AUY5Z5E.js.map +1 -0
  72. package/dist/{chunk-YWCOJOUI.js → chunk-5L7PTX2Q.js} +2 -2
  73. package/dist/{chunk-ELIL2A37.js → chunk-7FGL6CGA.js} +5 -5
  74. package/dist/{chunk-TVVEYCNW.js → chunk-7K5Q6COX.js} +4 -4
  75. package/dist/{chunk-HXXTXLKA.js → chunk-A2TFK5PG.js} +2 -2
  76. package/dist/{chunk-LI7OE7G6.js → chunk-ANCC3JJH.js} +2 -2
  77. package/dist/{chunk-ILBLZ5Y5.js → chunk-D5NGH2EW.js} +2 -2
  78. package/dist/{chunk-ST5IEK3H.js → chunk-DUEEQCTT.js} +2 -2
  79. package/dist/{chunk-QISYTKG4.js → chunk-EZNXW27Q.js} +6 -6
  80. package/dist/{chunk-47J4EDGR.js → chunk-FJ2ND6CZ.js} +2 -2
  81. package/dist/{chunk-FQU3HTQM.js → chunk-FMCHFSZF.js} +6 -6
  82. package/dist/{chunk-FULKGK7P.js → chunk-G2RBWVFQ.js} +2 -2
  83. package/dist/{chunk-W2ZSNIBP.js → chunk-GD7UPIBG.js} +2 -2
  84. package/dist/{chunk-Z54EM5YZ.js → chunk-GF22HWS2.js} +4 -4
  85. package/dist/{chunk-ZDAYWJV6.js → chunk-GVX3NAUU.js} +2 -2
  86. package/dist/{chunk-M2INOSTA.js → chunk-GWHPQOUC.js} +2 -2
  87. package/dist/{chunk-N5XULCU3.js → chunk-HIDEBEAN.js} +9 -9
  88. package/dist/{chunk-74HN5QUH.js → chunk-HRMEYQRO.js} +2 -2
  89. package/dist/{chunk-VFZS2HAA.js → chunk-IP3DHDU2.js} +2 -2
  90. package/dist/{chunk-RKNJBZ55.js → chunk-JBPKEARU.js} +4 -4
  91. package/dist/{chunk-PFJXPOD4.js → chunk-JYPO7L3G.js} +2 -2
  92. package/dist/{chunk-G2L5IZV4.js → chunk-KSGEGHYA.js} +44 -44
  93. package/dist/{chunk-PKCLQ7KM.js → chunk-LZGCJSQR.js} +5 -5
  94. package/dist/{chunk-WOC6DXP5.js → chunk-M4DMO67I.js} +4 -4
  95. package/dist/{chunk-JCNJIXZN.js → chunk-NXUJRXSF.js} +2 -2
  96. package/dist/{chunk-5VQPZ4GV.js → chunk-PELT6ADC.js} +3 -3
  97. package/dist/{chunk-IDE5AR5W.js → chunk-PLCZD3LG.js} +28 -66
  98. package/dist/chunk-PLCZD3LG.js.map +1 -0
  99. package/dist/{chunk-KMWYZTIE.js → chunk-PRVJH7CY.js} +2 -2
  100. package/dist/{chunk-RVO6TCTU.js → chunk-QCECHYZL.js} +6 -6
  101. package/dist/{chunk-KA47C7BF.js → chunk-RSPSHRZA.js} +2 -2
  102. package/dist/{chunk-E4FOAENZ.js → chunk-S55OAS5P.js} +5 -5
  103. package/dist/{chunk-BIPBQQFI.js → chunk-UMOO7IY7.js} +2 -2
  104. package/dist/{chunk-5U7ID3EZ.js → chunk-UTVZKHJK.js} +2 -2
  105. package/dist/{chunk-V5FE3UAI.js → chunk-WOEVJKKQ.js} +2 -2
  106. package/dist/{chunk-N3LL5NEU.js → chunk-WQIHRA5F.js} +2 -2
  107. package/dist/chunk-Z7L72ZHL.js +75 -0
  108. package/dist/chunk-Z7L72ZHL.js.map +1 -0
  109. package/dist/{cli-BWXSPxB9.d.ts → cli-p_WmVo2h.d.ts} +3 -3
  110. package/dist/cli.d.ts +7 -6
  111. package/dist/cli.js +55 -53
  112. package/dist/compounding/engine.d.ts +2 -1
  113. package/dist/compounding/engine.js +10 -7
  114. package/dist/connectors/codex-materialize-runner.js +10 -7
  115. package/dist/connectors/index.js +10 -7
  116. package/dist/consolidation-provenance-check.d.ts +2 -1
  117. package/dist/consolidation-undo.d.ts +2 -1
  118. package/dist/contradiction/index.d.ts +7 -6
  119. package/dist/convergence-refresh.d.ts +2 -1
  120. package/dist/convergence-refresh.js +12 -9
  121. package/dist/corpus-watermark.js +15 -13
  122. package/dist/entity-recall-cancellation.d.ts +36 -0
  123. package/dist/entity-recall-cancellation.js +19 -0
  124. package/dist/entity-recall-cancellation.js.map +1 -0
  125. package/dist/entity-retrieval.d.ts +14 -6
  126. package/dist/entity-retrieval.js +11 -7
  127. package/dist/explicit-capture.d.ts +5 -4
  128. package/dist/external-wiki-access.d.ts +6 -5
  129. package/dist/external-wiki-access.js +28 -26
  130. package/dist/external-wiki-mcp-tools.d.ts +6 -5
  131. package/dist/extraction.js +15 -13
  132. package/dist/index.d.ts +11 -10
  133. package/dist/index.js +114 -112
  134. package/dist/lcm/engine.js +2 -2
  135. package/dist/lcm/index.js +7 -7
  136. package/dist/maintenance/memory-governance.js +10 -7
  137. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +7 -6
  138. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +10 -7
  139. package/dist/maintenance/rebuild-memory-projection.d.ts +2 -1
  140. package/dist/maintenance/rebuild-memory-projection.js +11 -8
  141. package/dist/{maintenance-DQrL93Uu.d.ts → maintenance-BfcuwfJS.d.ts} +1 -1
  142. package/dist/mcp-memory-inspector-app.d.ts +6 -5
  143. package/dist/memory-worth-outcomes.d.ts +2 -1
  144. package/dist/namespaces/migrate.d.ts +2 -1
  145. package/dist/namespaces/migrate.js +14 -12
  146. package/dist/namespaces/search.js +4 -4
  147. package/dist/namespaces/storage.d.ts +7 -6
  148. package/dist/namespaces/storage.js +10 -7
  149. package/dist/offline-sync-impression-drain.js +3 -3
  150. package/dist/operator-doctor-corpus.js +16 -14
  151. package/dist/operator-toolkit.d.ts +2 -1
  152. package/dist/operator-toolkit.js +20 -18
  153. package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -1
  154. package/dist/orchestration/maintenance.d.ts +3 -2
  155. package/dist/orchestration/maintenance.js +14 -12
  156. package/dist/{orchestrator-CRR-6sVB.d.ts → orchestrator-B7nLo4z1.d.ts} +3 -3
  157. package/dist/orchestrator.d.ts +5 -4
  158. package/dist/orchestrator.js +89 -87
  159. package/dist/qmd.js +4 -4
  160. package/dist/recall-qos.d.ts +109 -2
  161. package/dist/recall-qos.js +12 -3
  162. package/dist/search/factory.js +3 -3
  163. package/dist/search/index.js +3 -3
  164. package/dist/semantic-consolidation.js +11 -8
  165. package/dist/semantic-rule-promotion.js +10 -7
  166. package/dist/semantic-rule-verifier.js +10 -7
  167. package/dist/{service-DJ0LDyuN.d.ts → service-B5W5IYCx.d.ts} +1 -1
  168. package/dist/source-agent-qualifier.js +13 -11
  169. package/dist/{storage-y340tYKb.d.ts → storage-COL907QF.d.ts} +2 -1
  170. package/dist/storage.d.ts +2 -1
  171. package/dist/storage.js +9 -6
  172. package/dist/temporal-supersession.d.ts +2 -1
  173. package/dist/tier-migration.d.ts +2 -1
  174. package/dist/verified-recall.js +10 -7
  175. package/package.json +3 -3
  176. package/src/artifact-search.test.ts +110 -0
  177. package/src/artifact-search.ts +115 -0
  178. package/src/entity-recall-cancellation.ts +58 -0
  179. package/src/entity-retrieval.ts +58 -4
  180. package/src/orchestration/namespace-read-fanout.ts +4 -1
  181. package/src/orchestration/recall-internal-deps.ts +2 -0
  182. package/src/orchestration/recall-internal.ts +76 -55
  183. package/src/orchestration/recall-search-pipeline.ts +9 -1
  184. package/src/orchestration/recall-search-prefilter.ts +18 -1
  185. package/src/orchestrator.ts +5 -6
  186. package/src/recall-qos.test.ts +289 -0
  187. package/src/recall-qos.ts +238 -1
  188. package/src/storage.ts +12 -56
  189. package/dist/chunk-BDNL34CT.js.map +0 -1
  190. package/dist/chunk-C3AKM55W.js +0 -59
  191. package/dist/chunk-C3AKM55W.js.map +0 -1
  192. package/dist/chunk-IDE5AR5W.js.map +0 -1
  193. package/dist/chunk-SIP5DBDN.js.map +0 -1
  194. /package/dist/{chunk-T6JRMVOV.js.map → chunk-2EJNXE53.js.map} +0 -0
  195. /package/dist/{chunk-64BHFFKZ.js.map → chunk-2RPJ4NEE.js.map} +0 -0
  196. /package/dist/{chunk-JFO5QZ7N.js.map → chunk-3JUYWPFJ.js.map} +0 -0
  197. /package/dist/{chunk-L7G72WY4.js.map → chunk-3NDN5P55.js.map} +0 -0
  198. /package/dist/{chunk-IXH2U2QT.js.map → chunk-45OTV7FZ.js.map} +0 -0
  199. /package/dist/{chunk-U6JGQ37L.js.map → chunk-52I3YZZ6.js.map} +0 -0
  200. /package/dist/{chunk-IZQ5TDFT.js.map → chunk-53AJILXF.js.map} +0 -0
  201. /package/dist/{chunk-YWCOJOUI.js.map → chunk-5L7PTX2Q.js.map} +0 -0
  202. /package/dist/{chunk-ELIL2A37.js.map → chunk-7FGL6CGA.js.map} +0 -0
  203. /package/dist/{chunk-TVVEYCNW.js.map → chunk-7K5Q6COX.js.map} +0 -0
  204. /package/dist/{chunk-HXXTXLKA.js.map → chunk-A2TFK5PG.js.map} +0 -0
  205. /package/dist/{chunk-LI7OE7G6.js.map → chunk-ANCC3JJH.js.map} +0 -0
  206. /package/dist/{chunk-ILBLZ5Y5.js.map → chunk-D5NGH2EW.js.map} +0 -0
  207. /package/dist/{chunk-ST5IEK3H.js.map → chunk-DUEEQCTT.js.map} +0 -0
  208. /package/dist/{chunk-QISYTKG4.js.map → chunk-EZNXW27Q.js.map} +0 -0
  209. /package/dist/{chunk-47J4EDGR.js.map → chunk-FJ2ND6CZ.js.map} +0 -0
  210. /package/dist/{chunk-FQU3HTQM.js.map → chunk-FMCHFSZF.js.map} +0 -0
  211. /package/dist/{chunk-FULKGK7P.js.map → chunk-G2RBWVFQ.js.map} +0 -0
  212. /package/dist/{chunk-W2ZSNIBP.js.map → chunk-GD7UPIBG.js.map} +0 -0
  213. /package/dist/{chunk-Z54EM5YZ.js.map → chunk-GF22HWS2.js.map} +0 -0
  214. /package/dist/{chunk-ZDAYWJV6.js.map → chunk-GVX3NAUU.js.map} +0 -0
  215. /package/dist/{chunk-M2INOSTA.js.map → chunk-GWHPQOUC.js.map} +0 -0
  216. /package/dist/{chunk-N5XULCU3.js.map → chunk-HIDEBEAN.js.map} +0 -0
  217. /package/dist/{chunk-74HN5QUH.js.map → chunk-HRMEYQRO.js.map} +0 -0
  218. /package/dist/{chunk-VFZS2HAA.js.map → chunk-IP3DHDU2.js.map} +0 -0
  219. /package/dist/{chunk-RKNJBZ55.js.map → chunk-JBPKEARU.js.map} +0 -0
  220. /package/dist/{chunk-PFJXPOD4.js.map → chunk-JYPO7L3G.js.map} +0 -0
  221. /package/dist/{chunk-G2L5IZV4.js.map → chunk-KSGEGHYA.js.map} +0 -0
  222. /package/dist/{chunk-PKCLQ7KM.js.map → chunk-LZGCJSQR.js.map} +0 -0
  223. /package/dist/{chunk-WOC6DXP5.js.map → chunk-M4DMO67I.js.map} +0 -0
  224. /package/dist/{chunk-JCNJIXZN.js.map → chunk-NXUJRXSF.js.map} +0 -0
  225. /package/dist/{chunk-5VQPZ4GV.js.map → chunk-PELT6ADC.js.map} +0 -0
  226. /package/dist/{chunk-KMWYZTIE.js.map → chunk-PRVJH7CY.js.map} +0 -0
  227. /package/dist/{chunk-RVO6TCTU.js.map → chunk-QCECHYZL.js.map} +0 -0
  228. /package/dist/{chunk-KA47C7BF.js.map → chunk-RSPSHRZA.js.map} +0 -0
  229. /package/dist/{chunk-E4FOAENZ.js.map → chunk-S55OAS5P.js.map} +0 -0
  230. /package/dist/{chunk-BIPBQQFI.js.map → chunk-UMOO7IY7.js.map} +0 -0
  231. /package/dist/{chunk-5U7ID3EZ.js.map → chunk-UTVZKHJK.js.map} +0 -0
  232. /package/dist/{chunk-V5FE3UAI.js.map → chunk-WOEVJKKQ.js.map} +0 -0
  233. /package/dist/{chunk-N3LL5NEU.js.map → chunk-WQIHRA5F.js.map} +0 -0
@@ -46,7 +46,10 @@ import {
46
46
  boundRecallContextComposition, composeRecallContext, contextBudgetForFooter,
47
47
  formatCuriosityFooter, selectCuriosityQuestion,
48
48
  } from "../recall-context-composition.js";
49
- import { createRecallSectionMetricRecorder } from "../recall-qos.js";
49
+ import {
50
+ createBoundedCoreSectionRunner,
51
+ createRecallSectionMetricRecorder,
52
+ } from "../recall-qos.js";
50
53
  import { buildRecallQueryPolicy } from "../recall-query-policy.js";
51
54
  import { type GraphRecallExpandedEntry, type LastRecallSnapshot } from "../recall-state.js";
52
55
  import {
@@ -74,7 +77,7 @@ import { searchTrustZoneRecords } from "../trust-zones.js";
74
77
  import type { IdentityInjectionMode, MemoryFile, QmdSearchResult, RecallPlanMode } from "../types.js";
75
78
  import { type VerifiedEpisodeResult, compareVerifiedEpisodeResults, searchVerifiedEpisodes } from "../verified-recall.js";
76
79
  import { type WorkProductLedgerSearchResult, searchWorkProductLedgerEntries } from "../work-product-ledger.js";
77
- import { abortError } from "../abort-error.js";
80
+ import { abortError, isAbortError, raceAbort } from "../abort-error.js";
78
81
  import {
79
82
  applyQueryAwareCandidateFilter,
80
83
  filterRecallCandidates,
@@ -147,7 +150,13 @@ export class RecallInternalCoordinator {
147
150
  profileTraceClosed = true;
148
151
  this.deps.profiler.endTrace(profileTraceId); // persists to JSONL file
149
152
  };
153
+ // The CONFIGURED core budget, which is what the sections that run outside the
154
+ // bounded runner report in their metrics: nothing enforces a deadline for them,
155
+ // so reporting a derived figure would claim a bound that does not exist. The
156
+ // bounded runner below derives and reports its own effective budget per
157
+ // section, against the request budget still left when that section starts.
150
158
  const recallSectionDeadlineMs = this.deps.config.recallCoreDeadlineMs ?? 75_000;
159
+ const recallOuterTimeoutMs = this.deps.config.recallOuterTimeoutMs ?? 75_000;
151
160
  const enrichmentSectionDeadlineMs =
152
161
  this.deps.config.recallEnrichmentDeadlineMs ?? 25_000;
153
162
  // Wrap entire recall body in try/finally so profiling trace is always closed,
@@ -206,6 +215,14 @@ export class RecallInternalCoordinator {
206
215
  timings,
207
216
  logger: log,
208
217
  });
218
+ const runBoundedCoreSection = createBoundedCoreSectionRunner({
219
+ configuredDeadlineMs: recallSectionDeadlineMs,
220
+ outerDeadlineAtMs:
221
+ recallOuterTimeoutMs > 0 ? recallStart + recallOuterTimeoutMs : null,
222
+ parentSignal: options.abortSignal,
223
+ record: recordRecallSectionMetric,
224
+ logger: log,
225
+ });
209
226
  const promptHash = createHash("sha256").update(prompt).digest("hex");
210
227
  const traceId = createHash("sha256")
211
228
  .update(`${sessionKey ?? "default"}:${recallStart}:${promptHash}`)
@@ -985,41 +1002,48 @@ export class RecallInternalCoordinator {
985
1002
  });
986
1003
  return null;
987
1004
  }
988
- const t0 = Date.now();
989
- const transcriptEntries = sessionKey
990
- ? await readRecentEntityTranscriptEntries(
991
- this.deps.transcript.readRecent(
992
- entityRecentTranscriptLookbackHours,
993
- sessionKey,
994
- ),
1005
+ return await runBoundedCoreSection(
1006
+ "entityRetrieval",
1007
+ null as string | null,
1008
+ async (sectionSignal) => {
1009
+ // The transcript store is shared and takes no signal of its own, so the
1010
+ // section stops WAITING on a stalled read rather than pretending to
1011
+ // interrupt it — otherwise a cancelled section sits in this await.
1012
+ const transcriptEntries = sessionKey
1013
+ ? await raceAbort(
1014
+ readRecentEntityTranscriptEntries(
1015
+ this.deps.transcript.readRecent(
1016
+ entityRecentTranscriptLookbackHours,
1017
+ sessionKey,
1018
+ ),
1019
+ recentTurns,
1020
+ ),
1021
+ sectionSignal,
1022
+ "entity recall aborted",
1023
+ )
1024
+ : [];
1025
+ return await buildEntityRecallSection({
1026
+ config: this.deps.config,
1027
+ storage: profileStorage,
1028
+ namespaceStorage: (namespace) => this.deps.getStorage(namespace),
1029
+ query: retrievalQuery,
1030
+ recallNamespaces,
995
1031
  recentTurns,
996
- )
997
- : [];
998
- const section = await buildEntityRecallSection({
999
- config: this.deps.config,
1000
- storage: profileStorage,
1001
- namespaceStorage: (namespace) => this.deps.getStorage(namespace),
1002
- query: retrievalQuery,
1003
- recallNamespaces,
1004
- recentTurns,
1005
- maxHints,
1006
- maxSupportingFacts,
1007
- maxRelatedEntities,
1008
- maxChars,
1009
- transcriptEntries,
1010
- }).catch((err) => {
1011
- log.warn(`entity retrieval build failed: ${err}`);
1012
- return null;
1013
- });
1014
- recordRecallSectionMetric({
1015
- section: "entityRetrieval",
1016
- priority: "core",
1017
- durationMs: Date.now() - t0,
1018
- deadlineMs: recallSectionDeadlineMs,
1019
- source: "fresh",
1020
- success: true,
1021
- });
1022
- return section;
1032
+ maxHints,
1033
+ maxSupportingFacts,
1034
+ maxRelatedEntities,
1035
+ maxChars,
1036
+ transcriptEntries,
1037
+ abortSignal: sectionSignal,
1038
+ }).catch((err) => {
1039
+ // Cancellation is the deadline/abort contract working; let the bounded
1040
+ // runner record it as cancelled rather than as an empty section.
1041
+ if (isAbortError(err)) throw err;
1042
+ log.warn(`entity retrieval build failed: ${err}`);
1043
+ return null;
1044
+ });
1045
+ },
1046
+ );
1023
1047
  })();
1024
1048
 
1025
1049
  // 1b. Knowledge Index (v7.0)
@@ -1130,7 +1154,6 @@ export class RecallInternalCoordinator {
1130
1154
  )
1131
1155
  return [];
1132
1156
  if (!resolvePresentationCapabilities(this.deps.config).verbatimArtifacts) return [];
1133
- const t0 = Date.now();
1134
1157
  const targetCount = computeArtifactRecallLimit(
1135
1158
  recallMode,
1136
1159
  recallResultLimit,
@@ -1148,25 +1171,23 @@ export class RecallInternalCoordinator {
1148
1171
  });
1149
1172
  return [];
1150
1173
  }
1151
- const results = await this.deps.recallArtifactsAcrossNamespaces(
1152
- retrievalQuery,
1153
- recallNamespaces,
1154
- targetCount,
1174
+ return await runBoundedCoreSection(
1175
+ "artifacts",
1176
+ [] as MemoryFile[],
1177
+ async (sectionSignal) => {
1178
+ const results = await this.deps.recallArtifactsAcrossNamespaces(
1179
+ retrievalQuery,
1180
+ recallNamespaces,
1181
+ targetCount,
1182
+ { abortSignal: sectionSignal },
1183
+ );
1184
+ return lifecycleCaps.extractionScopeClassification
1185
+ ? results.filter((artifact) =>
1186
+ canRecallToolScopedMemory(artifact.frontmatter, options.sourceConnector),
1187
+ )
1188
+ : results;
1189
+ },
1155
1190
  );
1156
-
1157
- recordRecallSectionMetric({
1158
- section: "artifacts",
1159
- priority: "core",
1160
- durationMs: Date.now() - t0,
1161
- deadlineMs: recallSectionDeadlineMs,
1162
- source: "fresh",
1163
- success: true,
1164
- });
1165
- return lifecycleCaps.extractionScopeClassification
1166
- ? results.filter((artifact) =>
1167
- canRecallToolScopedMemory(artifact.frontmatter, options.sourceConnector),
1168
- )
1169
- : results;
1170
1191
  })();
1171
1192
 
1172
1193
  const objectiveStatePromise = (async (): Promise<string | null> => {
@@ -40,6 +40,7 @@ import {
40
40
  fetchActiveArtifactsForNamespace as fetchActiveArtifactsForNamespaceHelper,
41
41
  searchEmbeddingFallback as searchEmbeddingFallbackHelper,
42
42
  searchQueryAwareFallback as searchQueryAwareFallbackHelper,
43
+ type ArtifactRecallOptions,
43
44
  } from "./recall-search-prefilter.js";
44
45
  import { shouldFilterSupersededFromRecall } from "../temporal-supersession.js";
45
46
  import { isValidAsOf, isValidityExpiredNow } from "../temporal-validity.js";
@@ -251,8 +252,15 @@ export class RecallSearchPipelineCoordinator {
251
252
  namespace: string,
252
253
  prompt: string,
253
254
  targetCount: number,
255
+ options: ArtifactRecallOptions = {},
254
256
  ): Promise<MemoryFile[]> {
255
- return fetchActiveArtifactsForNamespaceHelper(this.deps, namespace, prompt, targetCount);
257
+ return fetchActiveArtifactsForNamespaceHelper(
258
+ this.deps,
259
+ namespace,
260
+ prompt,
261
+ targetCount,
262
+ options,
263
+ );
256
264
  }
257
265
 
258
266
  async buildQueryAwarePrefilter(
@@ -42,11 +42,22 @@ export interface PrefilterAndArtifactDeps {
42
42
  ): Promise<QmdSearchResult[]>;
43
43
  }
44
44
 
45
+ /**
46
+ * Cancellation for the artifact recall path (issue #2291). Artifact recall
47
+ * re-scans and re-tokenizes every stored artifact, up to `MAX_ATTEMPTS` times
48
+ * with a growing fetch limit; on a large or slow memory tree that is minutes of
49
+ * work that used to keep running after the caller had already given up.
50
+ */
51
+ export interface ArtifactRecallOptions {
52
+ abortSignal?: AbortSignal;
53
+ }
54
+
45
55
  export async function fetchActiveArtifactsForNamespace(
46
56
  deps: Pick<PrefilterAndArtifactDeps, "storageRouter" | "resolveArtifactSourceStatuses">,
47
57
  namespace: string,
48
58
  prompt: string,
49
59
  targetCount: number,
60
+ options: ArtifactRecallOptions = {},
50
61
  ): Promise<MemoryFile[]> {
51
62
  const storage = await deps.storageRouter.storageFor(namespace);
52
63
  let fetchLimit = computeArtifactCandidateFetchLimit(targetCount);
@@ -55,7 +66,10 @@ export async function fetchActiveArtifactsForNamespace(
55
66
  let bestFiltered: MemoryFile[] = [];
56
67
 
57
68
  for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {
58
- const rawResults = await storage.searchArtifacts(prompt, fetchLimit);
69
+ throwIfRecallAborted(options.abortSignal);
70
+ const rawResults = await storage.searchArtifacts(prompt, fetchLimit, {
71
+ abortSignal: options.abortSignal,
72
+ });
59
73
  const sourceIds = Array.from(
60
74
  new Set(
61
75
  rawResults
@@ -65,6 +79,9 @@ export async function fetchActiveArtifactsForNamespace(
65
79
  ),
66
80
  ),
67
81
  );
82
+ // The status snapshot rebuild reads every memory in the namespace; do not
83
+ // start one for a recall that has already been abandoned.
84
+ throwIfRecallAborted(options.abortSignal);
68
85
  const sourceStatus =
69
86
  sourceIds.length > 0
70
87
  ? await deps.resolveArtifactSourceStatuses(storage, sourceIds)
@@ -120,6 +120,7 @@ import { ConsolidationRunCoordinator } from "./orchestration/consolidation-run.j
120
120
  import { ExtractionPersistCoordinator } from "./orchestration/extraction-persist.js";
121
121
  import { RecallInternalCoordinator } from "./orchestration/recall-internal.js";
122
122
  import { RecallSearchPipelineCoordinator } from "./orchestration/recall-search-pipeline.js";
123
+ import type { ArtifactRecallOptions } from "./orchestration/recall-search-prefilter.js";
123
124
  import { TurnIngestionCoordinator, type TurnIngestionOptions } from "./orchestration/turn-ingestion.js";
124
125
  import { RecallIntrospectionCoordinator } from "./orchestration/recall-introspection.js";
125
126
  import { OrchestratorInitCoordinator } from "./orchestration/orchestrator-init.js";
@@ -2460,11 +2461,10 @@ export class Orchestrator {
2460
2461
  namespace: string,
2461
2462
  prompt: string,
2462
2463
  targetCount: number,
2464
+ options: ArtifactRecallOptions = {},
2463
2465
  ): Promise<MemoryFile[]> {
2464
2466
  return this.recallSearchPipelineCoordinator.fetchActiveArtifactsForNamespace(
2465
- namespace,
2466
- prompt,
2467
- targetCount,
2467
+ namespace, prompt, targetCount, options,
2468
2468
  );
2469
2469
  }
2470
2470
 
@@ -2472,11 +2472,10 @@ export class Orchestrator {
2472
2472
  prompt: string,
2473
2473
  recallNamespaces: string[],
2474
2474
  targetCount: number,
2475
+ options: ArtifactRecallOptions = {},
2475
2476
  ): Promise<MemoryFile[]> {
2476
2477
  return this.namespaceReadFanoutCoordinator.recallArtifactsAcrossNamespaces(
2477
- prompt,
2478
- recallNamespaces,
2479
- targetCount,
2478
+ prompt, recallNamespaces, targetCount, options,
2480
2479
  );
2481
2480
  }
2482
2481
 
@@ -1,7 +1,12 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
+ import { throwIfAborted } from "./abort-error.js";
3
4
  import {
5
+ createBoundedCoreSectionRunner,
4
6
  createRecallSectionMetricRecorder,
7
+ resolveRecallCoreSectionDeadlineMs,
8
+ runRecallSectionWithinDeadline,
9
+ yieldToEventLoop,
5
10
  type RecallSectionMetric,
6
11
  } from "./recall-qos.js";
7
12
 
@@ -83,3 +88,287 @@ test("recall section metric recorder respects timing overrides and logs enrichme
83
88
  success: true,
84
89
  });
85
90
  });
91
+
92
+ test("timed-out section metric reports the breach instead of a plain duration", () => {
93
+ const timings: Record<string, string> = {};
94
+ const calls: Array<{ level: "info" | "debug"; payload: unknown[] }> = [];
95
+ const recorder = createRecallSectionMetricRecorder({
96
+ timings,
97
+ logger: {
98
+ info: (_message: string, ...payload: unknown[]) => calls.push({ level: "info", payload }),
99
+ debug: (_message: string, ...payload: unknown[]) => calls.push({ level: "debug", payload }),
100
+ },
101
+ });
102
+
103
+ recorder({
104
+ section: "entityRetrieval",
105
+ priority: "core",
106
+ durationMs: 75_004,
107
+ deadlineMs: 75_000,
108
+ source: "timeout",
109
+ success: false,
110
+ });
111
+
112
+ assert.equal(timings.entityRetrieval, "timeout(75004ms)");
113
+ // A breach is not a success, so it must not be logged as one.
114
+ assert.equal(calls[0]?.level, "debug");
115
+ });
116
+
117
+ test("recall section deadline degrades to the fallback and cancels the section", async () => {
118
+ let observedSignal: AbortSignal | undefined;
119
+ let cancelledDuringWork = false;
120
+
121
+ // A real timer is the subject here, not a guessed wait: the section below
122
+ // never settles on its own, so the deadline always wins regardless of load.
123
+ const outcome = await runRecallSectionWithinDeadline({
124
+ deadlineMs: 15,
125
+ fallback: "degraded",
126
+ run: async (sectionSignal) => {
127
+ observedSignal = sectionSignal;
128
+ const cancelled = Promise.withResolvers<void>();
129
+ sectionSignal.addEventListener("abort", () => {
130
+ cancelledDuringWork = true;
131
+ cancelled.resolve();
132
+ });
133
+ await cancelled.promise;
134
+ return "late value";
135
+ },
136
+ });
137
+
138
+ assert.equal(outcome.timedOut, true);
139
+ assert.equal(outcome.value, "degraded");
140
+ assert.equal(observedSignal?.aborted, true);
141
+ assert.equal(cancelledDuringWork, true);
142
+ });
143
+
144
+ test("recall section deadline returns the real value when the section finishes in time", async () => {
145
+ const outcome = await runRecallSectionWithinDeadline({
146
+ deadlineMs: 5_000,
147
+ fallback: null as string | null,
148
+ run: async (sectionSignal) => {
149
+ assert.equal(sectionSignal.aborted, false);
150
+ return "section body";
151
+ },
152
+ });
153
+
154
+ assert.equal(outcome.timedOut, false);
155
+ assert.equal(outcome.value, "section body");
156
+ });
157
+
158
+ test("a zero deadline disables the bound rather than failing every section", async () => {
159
+ const released = Promise.withResolvers<void>();
160
+ const pending = runRecallSectionWithinDeadline({
161
+ deadlineMs: 0,
162
+ fallback: "degraded",
163
+ run: async () => {
164
+ await released.promise;
165
+ return "unbounded value";
166
+ },
167
+ });
168
+
169
+ // Two full event-loop turns: any 0ms timer would already have fired, so a
170
+ // still-pending section proves the bound was disabled rather than instant.
171
+ for (let turn = 0; turn < 2; turn += 1) {
172
+ const turned = Promise.withResolvers<void>();
173
+ setImmediate(turned.resolve);
174
+ await turned.promise;
175
+ }
176
+ released.resolve();
177
+
178
+ const outcome = await pending;
179
+ assert.equal(outcome.timedOut, false);
180
+ assert.equal(outcome.value, "unbounded value");
181
+ });
182
+
183
+ test("a caller abort propagates into the section signal", async () => {
184
+ const caller = new AbortController();
185
+ let cancelledDuringWork = false;
186
+
187
+ const pending = runRecallSectionWithinDeadline({
188
+ deadlineMs: 5_000,
189
+ fallback: "degraded",
190
+ parentSignal: caller.signal,
191
+ run: async (sectionSignal) => {
192
+ const cancelled = Promise.withResolvers<void>();
193
+ sectionSignal.addEventListener("abort", () => {
194
+ cancelledDuringWork = true;
195
+ cancelled.resolve();
196
+ });
197
+ await cancelled.promise;
198
+ return "cancelled";
199
+ },
200
+ });
201
+
202
+ caller.abort();
203
+ const outcome = await pending;
204
+
205
+ assert.equal(cancelledDuringWork, true);
206
+ assert.equal(outcome.timedOut, false);
207
+ assert.equal(outcome.value, "cancelled");
208
+ });
209
+
210
+ test("an already-aborted caller signal cancels the section before it does work", async () => {
211
+ const caller = new AbortController();
212
+ caller.abort();
213
+
214
+ const outcome = await runRecallSectionWithinDeadline({
215
+ deadlineMs: 5_000,
216
+ fallback: "degraded",
217
+ parentSignal: caller.signal,
218
+ run: async (sectionSignal) => {
219
+ assert.equal(sectionSignal.aborted, true);
220
+ return "observed";
221
+ },
222
+ });
223
+
224
+ assert.equal(outcome.value, "observed");
225
+ });
226
+
227
+ test("a section rejection surfaces to the caller instead of being swallowed", async () => {
228
+ await assert.rejects(
229
+ runRecallSectionWithinDeadline({
230
+ deadlineMs: 5_000,
231
+ fallback: "degraded",
232
+ run: async () => {
233
+ throw new Error("section exploded");
234
+ },
235
+ }),
236
+ /section exploded/,
237
+ );
238
+ });
239
+
240
+ test("the default core section budget is capped below the remaining request budget", () => {
241
+ // Both settings default to 75s. Taken at face value the section deadline could
242
+ // never fire first, so degradation would be unreachable on default config.
243
+ assert.equal(
244
+ resolveRecallCoreSectionDeadlineMs({
245
+ configuredCoreDeadlineMs: 75_000,
246
+ remainingOuterMs: 75_000,
247
+ }),
248
+ 60_000,
249
+ );
250
+ });
251
+
252
+ test("the core section budget shrinks with the request budget already spent", () => {
253
+ // A section that starts 20s into a 75s request must finish inside the 55s left,
254
+ // not inside the original ceiling — otherwise the request is aborted wholesale.
255
+ assert.equal(
256
+ resolveRecallCoreSectionDeadlineMs({
257
+ configuredCoreDeadlineMs: 75_000,
258
+ remainingOuterMs: 55_000,
259
+ }),
260
+ 44_000,
261
+ );
262
+ });
263
+
264
+ test("a request already over budget degrades its sections immediately", () => {
265
+ assert.equal(
266
+ resolveRecallCoreSectionDeadlineMs({
267
+ configuredCoreDeadlineMs: 75_000,
268
+ remainingOuterMs: 0,
269
+ }),
270
+ 1,
271
+ );
272
+ assert.equal(
273
+ resolveRecallCoreSectionDeadlineMs({
274
+ configuredCoreDeadlineMs: 75_000,
275
+ remainingOuterMs: -5_000,
276
+ }),
277
+ 1,
278
+ );
279
+ });
280
+
281
+ test("an explicitly lowered core section budget is honored exactly", () => {
282
+ assert.equal(
283
+ resolveRecallCoreSectionDeadlineMs({
284
+ configuredCoreDeadlineMs: 5_000,
285
+ remainingOuterMs: 75_000,
286
+ }),
287
+ 5_000,
288
+ );
289
+ });
290
+
291
+ test("a zero budget on either side keeps the configured value", () => {
292
+ assert.equal(
293
+ resolveRecallCoreSectionDeadlineMs({ configuredCoreDeadlineMs: 0, remainingOuterMs: 75_000 }),
294
+ 0,
295
+ );
296
+ // No request ceiling means nothing to reserve headroom against.
297
+ assert.equal(
298
+ resolveRecallCoreSectionDeadlineMs({ configuredCoreDeadlineMs: 75_000, remainingOuterMs: null }),
299
+ 75_000,
300
+ );
301
+ });
302
+
303
+ test("a bounded core section resolves its budget from the budget left when it starts", async () => {
304
+ const metrics: RecallSectionMetric[] = [];
305
+ const clockMs = 1_000_000;
306
+ const runSection = createBoundedCoreSectionRunner({
307
+ configuredDeadlineMs: 75_000,
308
+ // 75s request that started 20s ago: 55s left, so the section gets 80% of 55s.
309
+ outerDeadlineAtMs: clockMs + 55_000,
310
+ record: (metric) => metrics.push(metric),
311
+ logger: { warn: () => {} },
312
+ now: () => clockMs,
313
+ });
314
+
315
+ await runSection("entityRetrieval", null as string | null, async () => "section body");
316
+
317
+ assert.equal(metrics[0]?.deadlineMs, 44_000);
318
+ });
319
+
320
+ test("a bounded core section degrades on cancellation rather than rejecting", async () => {
321
+ // The phase this section belongs to is awaited through a race the caller's
322
+ // abort also wins, so a rejection here would be an unhandled rejection.
323
+ const metrics: RecallSectionMetric[] = [];
324
+ const caller = new AbortController();
325
+ const runSection = createBoundedCoreSectionRunner({
326
+ configuredDeadlineMs: 5_000,
327
+ outerDeadlineAtMs: null,
328
+ parentSignal: caller.signal,
329
+ record: (metric) => metrics.push(metric),
330
+ logger: { warn: () => {} },
331
+ });
332
+
333
+ caller.abort();
334
+ const value = await runSection("artifacts", [] as string[], async (sectionSignal) => {
335
+ throwIfAborted(sectionSignal, "artifact search aborted");
336
+ return ["should not be reached"];
337
+ });
338
+
339
+ assert.deepEqual(value, []);
340
+ assert.equal(metrics[0]?.section, "artifacts");
341
+ assert.equal(metrics[0]?.success, false);
342
+ assert.match(metrics[0]?.timing ?? "", /^cancelled\(\d+ms\)$/);
343
+ });
344
+
345
+ test("a bounded core section still surfaces a non-abort failure", async () => {
346
+ const runSection = createBoundedCoreSectionRunner({
347
+ configuredDeadlineMs: 5_000,
348
+ outerDeadlineAtMs: null,
349
+ record: () => {},
350
+ logger: { warn: () => {} },
351
+ });
352
+
353
+ await assert.rejects(
354
+ runSection("artifacts", [] as string[], async () => {
355
+ throw new Error("artifact tier unreadable");
356
+ }),
357
+ /artifact tier unreadable/,
358
+ );
359
+ });
360
+
361
+ test("yieldToEventLoop lets a pending timer run", async () => {
362
+ // The guarantee corpus scans depend on: without a yield the timer that carries
363
+ // a section deadline cannot fire while the scan holds the loop.
364
+ let timerFired = false;
365
+ const timer = setTimeout(() => {
366
+ timerFired = true;
367
+ }, 0);
368
+
369
+ await yieldToEventLoop();
370
+ await yieldToEventLoop();
371
+ clearTimeout(timer);
372
+
373
+ assert.equal(timerFired, true);
374
+ });