@remnic/core 9.25.3 → 9.25.5

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 (161) hide show
  1. package/dist/access-admin-ops-surface.d.ts +3 -2
  2. package/dist/access-admin-ops-surface.js +9 -8
  3. package/dist/access-authorization-probe.d.ts +10 -3
  4. package/dist/access-authorization-probe.js +12 -9
  5. package/dist/access-boundary.d.ts +6 -3
  6. package/dist/access-boundary.js +13 -10
  7. package/dist/access-cli.js +42 -41
  8. package/dist/access-cli.js.map +1 -1
  9. package/dist/access-http.d.ts +3 -2
  10. package/dist/access-http.js +16 -15
  11. package/dist/access-identity-continuity-surface.d.ts +2 -1
  12. package/dist/access-identity-continuity-surface.js +9 -8
  13. package/dist/access-lcm-surface.d.ts +3 -2
  14. package/dist/access-lcm-surface.js +9 -8
  15. package/dist/access-mcp-output-schemas.js +1 -1
  16. package/dist/access-mcp.d.ts +3 -2
  17. package/dist/access-mcp.js +13 -12
  18. package/dist/access-namespace-preflight.js +9 -8
  19. package/dist/access-observe-write-surface.d.ts +3 -2
  20. package/dist/access-observe-write-surface.js +9 -8
  21. package/dist/access-operations-batch.js +10 -9
  22. package/dist/access-operations.d.ts +6 -5
  23. package/dist/access-operations.js +11 -10
  24. package/dist/access-recall-concurrency.d.ts +3 -2
  25. package/dist/access-recall-concurrency.js +9 -8
  26. package/dist/access-recall-response.d.ts +4 -2
  27. package/dist/access-recall-response.js +9 -8
  28. package/dist/access-recall-surface.d.ts +3 -2
  29. package/dist/access-recall-surface.js +9 -8
  30. package/dist/access-schema.d.ts +76 -76
  31. package/dist/{access-service-BlNKWBC9.d.ts → access-service-BJQ22URi.d.ts} +4 -1
  32. package/dist/access-service.d.ts +3 -2
  33. package/dist/access-service.js +9 -8
  34. package/dist/access-surface-catalog.d.ts +3 -2
  35. package/dist/bootstrap.d.ts +2 -1
  36. package/dist/briefing.js +3 -3
  37. package/dist/causal-consolidation.js +4 -4
  38. package/dist/{chunk-GF22HWS2.js → chunk-2PHX3I5C.js} +7 -7
  39. package/dist/{chunk-3I35RLJW.js → chunk-32XTXKIA.js} +14 -13
  40. package/dist/chunk-32XTXKIA.js.map +1 -0
  41. package/dist/{chunk-LXKJNUND.js → chunk-37VPMR4E.js} +2 -2
  42. package/dist/{chunk-3CDMIR3C.js → chunk-3CHQZVAL.js} +2 -2
  43. package/dist/{chunk-U37RGPJT.js → chunk-5J7HJ7RT.js} +2 -2
  44. package/dist/{chunk-AGR7MGUX.js → chunk-7IGWPXVC.js} +2 -2
  45. package/dist/{chunk-FIGDB2HR.js → chunk-7KXQ44PA.js} +2 -2
  46. package/dist/chunk-AHVEHR6K.js +82 -0
  47. package/dist/chunk-AHVEHR6K.js.map +1 -0
  48. package/dist/{chunk-MNLZGCM2.js → chunk-AQ2ZQYDI.js} +3 -3
  49. package/dist/{chunk-TWCOYET2.js → chunk-B6JDYXL7.js} +2 -1
  50. package/dist/chunk-B6JDYXL7.js.map +1 -0
  51. package/dist/{chunk-RROMWDDC.js → chunk-BL4JHZSO.js} +49 -12
  52. package/dist/chunk-BL4JHZSO.js.map +1 -0
  53. package/dist/{chunk-PXJ4A6IK.js → chunk-BUK2FXOL.js} +133 -105
  54. package/dist/chunk-BUK2FXOL.js.map +1 -0
  55. package/dist/{chunk-JMDZDT24.js → chunk-D7MAWMMK.js} +18 -4
  56. package/dist/chunk-D7MAWMMK.js.map +1 -0
  57. package/dist/{chunk-TG2DJFH7.js → chunk-E5A6C4A5.js} +20 -20
  58. package/dist/{chunk-NBT7O25Z.js → chunk-FR75B4UU.js} +82 -56
  59. package/dist/chunk-FR75B4UU.js.map +1 -0
  60. package/dist/{chunk-DPYYSG6F.js → chunk-G6MGAE36.js} +7 -7
  61. package/dist/{chunk-GNFZFJBJ.js → chunk-IR4LGIH6.js} +2 -2
  62. package/dist/{chunk-MPDX5QMH.js → chunk-JA4HEXV7.js} +2 -2
  63. package/dist/{chunk-XTAAG4SQ.js → chunk-K4DP5KZA.js} +6 -6
  64. package/dist/{chunk-AT6DNUCM.js → chunk-NY6W6CJW.js} +2 -2
  65. package/dist/{chunk-NMGO2HAZ.js → chunk-OIE242WM.js} +2 -2
  66. package/dist/{chunk-SZN6XVFC.js → chunk-PS5OG75R.js} +2 -2
  67. package/dist/{chunk-VY6SDKWZ.js → chunk-SSYBI43M.js} +2 -2
  68. package/dist/{chunk-IE4HB6O5.js → chunk-TPYZ727G.js} +3 -3
  69. package/dist/{chunk-5ZGWA4AG.js → chunk-UMFLAACL.js} +6 -6
  70. package/dist/{chunk-THHPDPMS.js → chunk-UOURUMBN.js} +2 -2
  71. package/dist/{chunk-6LRTGCRV.js → chunk-VPSAVXHM.js} +3 -3
  72. package/dist/{chunk-AL4TZWI3.js → chunk-W33DEUKR.js} +2 -2
  73. package/dist/{chunk-W4ZDTHWZ.js → chunk-WIGBTWXA.js} +2 -2
  74. package/dist/{chunk-NVYCTASB.js → chunk-WSEA52RI.js} +2 -2
  75. package/dist/{chunk-LARPWF3S.js → chunk-ZPUFQGUF.js} +4 -4
  76. package/dist/{cli-Zc6UclD1.d.ts → cli-1W7cNjJJ.d.ts} +2 -2
  77. package/dist/cli.d.ts +4 -3
  78. package/dist/cli.js +28 -27
  79. package/dist/compounding/engine.js +3 -3
  80. package/dist/connectors/codex-materialize-runner.js +3 -3
  81. package/dist/connectors/index.js +3 -3
  82. package/dist/corpus-watermark.js +5 -5
  83. package/dist/entity-retrieval.js +3 -3
  84. package/dist/explicit-capture.d.ts +2 -1
  85. package/dist/extraction.js +5 -1
  86. package/dist/{first-start-migration-LEXL3XYL.js → first-start-migration-BKNGOFED.js} +4 -4
  87. package/dist/index.d.ts +391 -390
  88. package/dist/index.js +74 -54
  89. package/dist/lcm/engine.js +3 -3
  90. package/dist/lcm/index.js +8 -8
  91. package/dist/maintenance/memory-governance.js +3 -3
  92. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  93. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  94. package/dist/mcp-memory-inspector-app.d.ts +3 -2
  95. package/dist/namespaces/migrate.js +4 -4
  96. package/dist/namespaces/storage.js +3 -3
  97. package/dist/operator-doctor-corpus.js +6 -6
  98. package/dist/operator-toolkit.js +9 -9
  99. package/dist/orchestration/maintenance.js +8 -8
  100. package/dist/{orchestrator-BTGgoKli.d.ts → orchestrator-BtA1wC5K.d.ts} +2 -0
  101. package/dist/orchestrator.d.ts +2 -1
  102. package/dist/orchestrator.js +42 -41
  103. package/dist/recall-context-composition.d.ts +30 -0
  104. package/dist/recall-context-composition.js +24 -0
  105. package/dist/recall-context-composition.js.map +1 -0
  106. package/dist/schemas.d.ts +54 -54
  107. package/dist/semantic-consolidation.js +4 -4
  108. package/dist/semantic-rule-promotion.js +3 -3
  109. package/dist/semantic-rule-verifier.js +3 -3
  110. package/dist/shared-context/manager.d.ts +8 -8
  111. package/dist/storage.js +2 -2
  112. package/dist/transfer/types.d.ts +66 -66
  113. package/dist/verified-recall.js +3 -3
  114. package/package.json +2 -2
  115. package/src/access-authorization-probe.ts +30 -1
  116. package/src/access-boundary.ts +21 -0
  117. package/src/access-http.test.ts +86 -1
  118. package/src/access-http.ts +6 -6
  119. package/src/access-mcp-output-schemas.ts +1 -0
  120. package/src/access-recall-response.ts +18 -13
  121. package/src/access-recall-surface.ts +11 -1
  122. package/src/access-service.ts +3 -0
  123. package/src/extraction-prompt-safety.test.ts +179 -0
  124. package/src/extraction.ts +186 -205
  125. package/src/index.ts +1 -1
  126. package/src/orchestration/orchestrator-helpers.ts +2 -0
  127. package/src/orchestration/recall-internal.ts +49 -49
  128. package/src/recall-context-composition.test.ts +90 -0
  129. package/src/recall-context-composition.ts +104 -0
  130. package/src/recall-curiosity-composition.test.ts +301 -0
  131. package/dist/chunk-3I35RLJW.js.map +0 -1
  132. package/dist/chunk-JMDZDT24.js.map +0 -1
  133. package/dist/chunk-NBT7O25Z.js.map +0 -1
  134. package/dist/chunk-PXJ4A6IK.js.map +0 -1
  135. package/dist/chunk-RROMWDDC.js.map +0 -1
  136. package/dist/chunk-TWCOYET2.js.map +0 -1
  137. /package/dist/{chunk-GF22HWS2.js.map → chunk-2PHX3I5C.js.map} +0 -0
  138. /package/dist/{chunk-LXKJNUND.js.map → chunk-37VPMR4E.js.map} +0 -0
  139. /package/dist/{chunk-3CDMIR3C.js.map → chunk-3CHQZVAL.js.map} +0 -0
  140. /package/dist/{chunk-U37RGPJT.js.map → chunk-5J7HJ7RT.js.map} +0 -0
  141. /package/dist/{chunk-AGR7MGUX.js.map → chunk-7IGWPXVC.js.map} +0 -0
  142. /package/dist/{chunk-FIGDB2HR.js.map → chunk-7KXQ44PA.js.map} +0 -0
  143. /package/dist/{chunk-MNLZGCM2.js.map → chunk-AQ2ZQYDI.js.map} +0 -0
  144. /package/dist/{chunk-TG2DJFH7.js.map → chunk-E5A6C4A5.js.map} +0 -0
  145. /package/dist/{chunk-DPYYSG6F.js.map → chunk-G6MGAE36.js.map} +0 -0
  146. /package/dist/{chunk-GNFZFJBJ.js.map → chunk-IR4LGIH6.js.map} +0 -0
  147. /package/dist/{chunk-MPDX5QMH.js.map → chunk-JA4HEXV7.js.map} +0 -0
  148. /package/dist/{chunk-XTAAG4SQ.js.map → chunk-K4DP5KZA.js.map} +0 -0
  149. /package/dist/{chunk-AT6DNUCM.js.map → chunk-NY6W6CJW.js.map} +0 -0
  150. /package/dist/{chunk-NMGO2HAZ.js.map → chunk-OIE242WM.js.map} +0 -0
  151. /package/dist/{chunk-SZN6XVFC.js.map → chunk-PS5OG75R.js.map} +0 -0
  152. /package/dist/{chunk-VY6SDKWZ.js.map → chunk-SSYBI43M.js.map} +0 -0
  153. /package/dist/{chunk-IE4HB6O5.js.map → chunk-TPYZ727G.js.map} +0 -0
  154. /package/dist/{chunk-5ZGWA4AG.js.map → chunk-UMFLAACL.js.map} +0 -0
  155. /package/dist/{chunk-THHPDPMS.js.map → chunk-UOURUMBN.js.map} +0 -0
  156. /package/dist/{chunk-6LRTGCRV.js.map → chunk-VPSAVXHM.js.map} +0 -0
  157. /package/dist/{chunk-AL4TZWI3.js.map → chunk-W33DEUKR.js.map} +0 -0
  158. /package/dist/{chunk-W4ZDTHWZ.js.map → chunk-WIGBTWXA.js.map} +0 -0
  159. /package/dist/{chunk-NVYCTASB.js.map → chunk-WSEA52RI.js.map} +0 -0
  160. /package/dist/{chunk-LARPWF3S.js.map → chunk-ZPUFQGUF.js.map} +0 -0
  161. /package/dist/{first-start-migration-LEXL3XYL.js.map → first-start-migration-BKNGOFED.js.map} +0 -0
@@ -48,6 +48,10 @@ import { buildProcedureRecallSection } from "../procedural/procedure-recall.js";
48
48
  import { ProfilingCollector } from "../profiling.js";
49
49
  import { buildQmdRecallCacheKey, getCachedQmdRecall, setCachedQmdRecall } from "../qmd-recall-cache.js";
50
50
  import { MEMORY_ID_PATTERN } from "../recall-handles.js";
51
+ import {
52
+ boundRecallContextComposition, composeRecallContext, contextBudgetForFooter,
53
+ formatCuriosityFooter, selectCuriosityQuestion,
54
+ } from "../recall-context-composition.js";
51
55
  import { createRecallSectionMetricRecorder } from "../recall-qos.js";
52
56
  import { buildRecallQueryPolicy } from "../recall-query-policy.js";
53
57
  import { type GraphRecallExpandedEntry, type LastRecallBudgetSummary, type LastRecallSnapshot, LastRecallStore, RecallHandleHistoryStore } from "../recall-state.js";
@@ -4405,7 +4409,6 @@ export class RecallInternalCoordinator {
4405
4409
  success: !trustFellBack,
4406
4410
  });
4407
4411
  }
4408
-
4409
4412
  // Synapse-inspired confidence gate: check scores BEFORE slicing so
4410
4413
  // reranking doesn't affect which score the gate evaluates.
4411
4414
  //
@@ -4445,7 +4448,6 @@ export class RecallInternalCoordinator {
4445
4448
  confidenceGateRejected = true;
4446
4449
  }
4447
4450
  }
4448
-
4449
4451
  // Diversify via MMR over the full candidate pool *before* truncating to
4450
4452
  // the final recall limit. Running MMR after the slice would be unable
4451
4453
  // to promote diverse candidates sitting just below the cutoff.
@@ -4456,7 +4458,6 @@ export class RecallInternalCoordinator {
4456
4458
  retrievalQuery,
4457
4459
  caps,
4458
4460
  );
4459
-
4460
4461
  // E-Mem-inspired memory reconstruction: fill gaps for referenced entities
4461
4462
  if (resolveRecallEnhancementCapabilities(this.deps.config).memoryReconstruction && memoryResults.length > 0) {
4462
4463
  try {
@@ -4499,7 +4500,6 @@ export class RecallInternalCoordinator {
4499
4500
  log.warn("recall: memory reconstruction failed (non-fatal)", err);
4500
4501
  }
4501
4502
  }
4502
-
4503
4503
  if (memoryResults.length > 0) {
4504
4504
  if (shouldPersistGraphSnapshot) {
4505
4505
  graphSnapshotFinalResults = this.deps.buildGraphRecallRankedResults(
@@ -4689,7 +4689,6 @@ export class RecallInternalCoordinator {
4689
4689
  }
4690
4690
  }
4691
4691
  }
4692
-
4693
4692
  if (globalResults.length > 0) {
4694
4693
  this.deps.appendRecallSection(
4695
4694
  sectionBuckets,
@@ -4697,7 +4696,6 @@ export class RecallInternalCoordinator {
4697
4696
  this.deps.formatQmdResults("Workspace Context", globalResults, sessionKey, recallTrustByPath),
4698
4697
  );
4699
4698
  }
4700
-
4701
4699
  recordRecallSectionMetric({
4702
4700
  section: "qmdPost",
4703
4701
  priority: "enrichment",
@@ -4706,7 +4704,6 @@ export class RecallInternalCoordinator {
4706
4704
  source: "fresh",
4707
4705
  success: true,
4708
4706
  });
4709
-
4710
4707
  // If the user is pushing back ("that's not right", "why did you say that"),
4711
4708
  // gently suggest an explicit workflow to inspect what was recalled and record feedback.
4712
4709
  // IMPORTANT: this is suggestion-only; never auto-mark negatives.
@@ -5026,7 +5023,6 @@ export class RecallInternalCoordinator {
5026
5023
  success: !trustFellBack,
5027
5024
  });
5028
5025
  }
5029
-
5030
5026
  if (recent.length > 0) {
5031
5027
  if (shouldPersistGraphSnapshot) {
5032
5028
  graphSnapshotFinalResults = this.deps.buildGraphRecallRankedResults(
@@ -5155,7 +5151,6 @@ export class RecallInternalCoordinator {
5155
5151
  }
5156
5152
  }
5157
5153
  }
5158
-
5159
5154
  if (isDisagreementPrompt(prompt)) {
5160
5155
  this.deps.appendRecallSection(
5161
5156
  sectionBuckets,
@@ -5174,7 +5169,6 @@ export class RecallInternalCoordinator {
5174
5169
  );
5175
5170
  }
5176
5171
  }
5177
-
5178
5172
  const phase2AfterQmdMs = Date.now() - recallStart;
5179
5173
  if (shouldPersistGraphSnapshot) {
5180
5174
  if (!graphSnapshotStatus) {
@@ -5211,7 +5205,6 @@ export class RecallInternalCoordinator {
5211
5205
  storage: profileStorage,
5212
5206
  snapshot: buildIntentDebugSnapshot(),
5213
5207
  });
5214
-
5215
5208
  // 2.5. Compression guideline recall section (v8.11 Task 5)
5216
5209
  if (
5217
5210
  this.deps.isRecallSectionEnabled(
@@ -5229,7 +5222,6 @@ export class RecallInternalCoordinator {
5229
5222
  );
5230
5223
  }
5231
5224
  }
5232
-
5233
5225
  // 3. Transcript/summaries/conversation/compounding are fetched in parallel above,
5234
5226
  // then assembled here according to recallPipeline order.
5235
5227
  if (transcriptSection) {
@@ -5264,28 +5256,22 @@ export class RecallInternalCoordinator {
5264
5256
  compoundingSection,
5265
5257
  );
5266
5258
  }
5267
-
5268
- // 5. Inject most relevant question (if enabled) (existing)
5269
- if (
5270
- this.deps.config.injectQuestions &&
5259
+ let curiosityFooter: string | undefined, curiosityQuestionSelected = false, curiosityFooterTruncated = false;
5260
+ const topQuestion = this.deps.config.injectQuestions &&
5271
5261
  this.deps.isRecallSectionEnabled("questions", true)
5272
- ) {
5273
- const questions = await profileStorage.readQuestions({
5274
- unresolvedOnly: true,
5275
- });
5276
- if (questions.length > 0) {
5277
- // Find the most relevant question to the current prompt
5278
- // Simple approach: use the highest-priority unresolved question
5279
- // TODO: Could use QMD search to find the most contextually relevant one
5280
- const topQuestion = questions[0]; // Already sorted by priority desc
5281
- this.deps.appendRecallSection(
5282
- sectionBuckets,
5283
- "questions",
5284
- `## Open Question\n\nSomething I've been curious about: ${topQuestion.question}\n\n_Context: ${topQuestion.context}_`,
5285
- );
5262
+ ? selectCuriosityQuestion(await profileStorage.readQuestions({ unresolvedOnly: true }))
5263
+ : undefined;
5264
+ if (topQuestion) {
5265
+ curiosityQuestionSelected = true;
5266
+ const formattedFooter = formatCuriosityFooter(topQuestion);
5267
+ const questionMaxChars = this.deps.getRecallSectionMaxChars("questions");
5268
+ if (questionMaxChars !== 0) {
5269
+ curiosityFooter = typeof questionMaxChars === "number"
5270
+ ? this.deps.truncateRecallSectionToBudget(formattedFooter, questionMaxChars)
5271
+ : formattedFooter;
5272
+ curiosityFooterTruncated = curiosityFooter.length < formattedFooter.length;
5286
5273
  }
5287
5274
  }
5288
-
5289
5275
  const phase2QuestionsDoneMs = Date.now() - recallStart;
5290
5276
  const finalizedQueryAwarePrefilter = await queryAwarePrefilterPromise;
5291
5277
  const phase2QapDoneMs = Date.now() - recallStart;
@@ -5299,7 +5285,6 @@ export class RecallInternalCoordinator {
5299
5285
  ).length;
5300
5286
  timings.queryAware = `${timings.queryAware};helped=${helpedCount}`;
5301
5287
  }
5302
-
5303
5288
  // --- Timing summary ---
5304
5289
  timings.total = `${Date.now() - recallStart}ms`;
5305
5290
  this.deps.profiler.endSpan("assembly", profileTraceId);
@@ -5318,10 +5303,9 @@ export class RecallInternalCoordinator {
5318
5303
  recallPlan: timings.recallPlan,
5319
5304
  queryPolicy: timings.queryPolicy,
5320
5305
  });
5321
-
5306
+ const recallBudgetChars = this.deps.getRecallBudgetChars(options.budgetCharsOverride);
5322
5307
  const assembledRecall = this.deps.assembleRecallSections(
5323
- sectionBuckets,
5324
- options.budgetCharsOverride,
5308
+ sectionBuckets, contextBudgetForFooter(recallBudgetChars, curiosityFooter),
5325
5309
  );
5326
5310
  recalledMemoryIds = assembledRecall.includedMemoryIds;
5327
5311
  recalledMemoryPaths = assembledRecall.includedMemoryPaths;
@@ -5332,29 +5316,45 @@ export class RecallInternalCoordinator {
5332
5316
  assembledRecall.includedMemoryPaths,
5333
5317
  assembledRecall.includedMemoryNamespaces,
5334
5318
  );
5335
- const context =
5336
- assembledRecall.sections.length === 0
5337
- ? ""
5338
- : assembledRecall.sections.join("\n\n---\n\n");
5339
- const sourcesUsed = this.deps.collectLastRecallSources(
5340
- sectionBuckets,
5341
- recallSource,
5342
- );
5319
+ const recalledContext = assembledRecall.sections.join("\n\n---\n\n");
5320
+ const composition = boundRecallContextComposition({
5321
+ context: recalledContext, footer: curiosityFooter, maxChars: recallBudgetChars,
5322
+ });
5323
+ const context = composeRecallContext(composition);
5324
+ try {
5325
+ const observerResult = options.onContextComposition?.(composition);
5326
+ if (observerResult && typeof observerResult.then === "function") {
5327
+ void Promise.resolve(observerResult).catch((err) => log.warn(
5328
+ "recall: context composition observer rejected", err,
5329
+ ));
5330
+ }
5331
+ } catch (err) {
5332
+ log.warn("recall: context composition observer failed open", err);
5333
+ }
5334
+ const compositionTruncated = context.length < composeRecallContext({
5335
+ context: recalledContext, footer: curiosityFooter,
5336
+ }).length;
5337
+ const includedSections = [...assembledRecall.includedIds];
5338
+ const omittedSections = [...assembledRecall.omittedIds];
5339
+ if (composition.footer && !includedSections.includes("questions")) includedSections.push("questions");
5340
+ else if (!composition.footer && curiosityQuestionSelected && !omittedSections.includes("questions"))
5341
+ omittedSections.push("questions");
5342
+ const sourcesUsed = this.deps.collectLastRecallSources(sectionBuckets, recallSource);
5343
+ if (composition.footer && !sourcesUsed.includes("questions")) sourcesUsed.push("questions");
5343
5344
  const budgetsApplied = this.deps.buildLastRecallBudgetSummary({
5344
5345
  requestedTopK,
5345
5346
  recallResultLimit,
5346
5347
  qmdFetchLimit,
5347
5348
  qmdHybridFetchLimit,
5348
- finalContextChars: assembledRecall.finalChars,
5349
- truncated: assembledRecall.truncated,
5350
- includedSections: assembledRecall.includedIds,
5351
- omittedSections: assembledRecall.omittedIds,
5349
+ finalContextChars: context.length,
5350
+ truncated: assembledRecall.truncated || compositionTruncated || curiosityFooterTruncated,
5351
+ includedSections,
5352
+ omittedSections,
5352
5353
  includedMemoryIds: assembledRecall.includedMemoryIds,
5353
5354
  includedMemoryPaths: assembledRecall.includedMemoryPaths,
5354
5355
  includedMemoryNamespaces: assembledRecall.includedMemoryNamespaces,
5355
5356
  omittedMemoryIds: assembledRecall.omittedMemoryIds,
5356
5357
  });
5357
-
5358
5358
  // X-ray capture (issue #570 PR 1). Only fires when the caller
5359
5359
  // explicitly opts in via `xrayCapture: true`. No behavior change
5360
5360
  // when the flag is absent — this branch and the setter both
@@ -5535,7 +5535,7 @@ export class RecallInternalCoordinator {
5535
5535
  filters,
5536
5536
  budget: {
5537
5537
  chars: this.deps.getRecallBudgetChars(options.budgetCharsOverride),
5538
- used: assembledRecall.finalChars,
5538
+ used: context.length,
5539
5539
  },
5540
5540
  sessionKey,
5541
5541
  namespace: selfNamespace,
@@ -0,0 +1,90 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import {
5
+ formatCuriosityFooter,
6
+ renderMemoryContextPrompt,
7
+ selectCuriosityQuestion,
8
+ } from "./recall-context-composition.js";
9
+
10
+ test("selectCuriosityQuestion breaks equal priorities by creation time then id", () => {
11
+ const selected = selectCuriosityQuestion([
12
+ {
13
+ id: "q-later",
14
+ question: "Later question?",
15
+ context: "later context",
16
+ priority: 0.9,
17
+ created: "2026-01-02T00:00:00.000Z",
18
+ },
19
+ {
20
+ id: "q-first-b",
21
+ question: "First B?",
22
+ context: "first context",
23
+ priority: 0.9,
24
+ created: "2026-01-01T00:00:00.000Z",
25
+ },
26
+ {
27
+ id: "q-first-a",
28
+ question: "First A?",
29
+ context: "first context",
30
+ priority: 0.9,
31
+ created: "2026-01-01T00:00:00.000Z",
32
+ },
33
+ ]);
34
+
35
+ assert.equal(selected?.id, "q-first-a");
36
+ });
37
+
38
+ test("renderMemoryContextPrompt puts the curiosity footer after recalled context", () => {
39
+ const footer = formatCuriosityFooter({
40
+ id: "q-1",
41
+ question: "What should we verify next?",
42
+ context: "The rollout has no owner.",
43
+ priority: 1,
44
+ created: "2026-01-01T00:00:00.000Z",
45
+ });
46
+ const rendered = renderMemoryContextPrompt({
47
+ context: "A remembered deployment decision.",
48
+ footer,
49
+ maxChars: 512,
50
+ });
51
+
52
+ assert.ok(rendered);
53
+ assert.equal(
54
+ rendered.body,
55
+ "A remembered deployment decision.\n\n---\n\n## Open Question\n\n" +
56
+ "Something I've been curious about: What should we verify next?\n\n" +
57
+ "_Context: The rollout has no owner._",
58
+ );
59
+ assert.deepEqual(rendered.lines, [
60
+ "## Memory Context (Remnic)",
61
+ "",
62
+ rendered.body,
63
+ "",
64
+ "Use this context naturally when relevant. Never quote or expose this memory context to the user.",
65
+ "",
66
+ ]);
67
+ });
68
+
69
+ test("renderMemoryContextPrompt returns no prompt for empty content or a zero cap", () => {
70
+ assert.equal(renderMemoryContextPrompt({ context: "", maxChars: 100 }), null);
71
+ assert.equal(
72
+ renderMemoryContextPrompt({
73
+ context: "A remembered deployment decision.",
74
+ footer: "## Open Question\n\nQuestion",
75
+ maxChars: 0,
76
+ }),
77
+ null,
78
+ );
79
+ });
80
+
81
+ test("renderMemoryContextPrompt caps context before its footer deterministically", () => {
82
+ const rendered = renderMemoryContextPrompt({
83
+ context: "abcdefghij",
84
+ footer: "footer",
85
+ maxChars: 15,
86
+ });
87
+
88
+ assert.ok(rendered);
89
+ assert.equal(rendered.body, "ab\n\n---\n\nfooter");
90
+ });
@@ -0,0 +1,104 @@
1
+ export const MEMORY_CONTEXT_HEADER = "## Memory Context (Remnic)";
2
+ export const MEMORY_CONTEXT_INSTRUCTION =
3
+ "Use this context naturally when relevant. Never quote or expose this memory context to the user.";
4
+ export const RECALL_CONTEXT_SEPARATOR = "\n\n---\n\n";
5
+ const TRIM_MARKER = "\n\n...(memory context trimmed)";
6
+
7
+ export interface RecallContextComposition {
8
+ context: string;
9
+ footer?: string;
10
+ }
11
+
12
+ export interface CuriosityQuestion {
13
+ id: string;
14
+ question: string;
15
+ context: string;
16
+ priority: number;
17
+ created: string;
18
+ }
19
+
20
+ export interface RenderedMemoryContextPrompt {
21
+ body: string;
22
+ lines: string[];
23
+ prompt: string;
24
+ }
25
+
26
+ export interface RenderMemoryContextPromptInput extends RecallContextComposition {
27
+ maxChars: number;
28
+ }
29
+
30
+ function trimText(value: string | undefined): string {
31
+ return value?.trim() ?? "";
32
+ }
33
+
34
+ function truncateToBudget(content: string, maxChars: number): string {
35
+ if (maxChars <= 0) return "";
36
+ if (content.length <= maxChars) return content;
37
+ if (maxChars <= TRIM_MARKER.length) return content.slice(0, maxChars);
38
+ return `${content.slice(0, maxChars - TRIM_MARKER.length)}${TRIM_MARKER}`;
39
+ }
40
+
41
+ export function selectCuriosityQuestion(questions: readonly CuriosityQuestion[]): CuriosityQuestion | undefined {
42
+ return [...questions]
43
+ .filter((question) => trimText(question.question).length > 0)
44
+ .sort(
45
+ (left, right) =>
46
+ right.priority - left.priority || left.created.localeCompare(right.created) || left.id.localeCompare(right.id)
47
+ )[0];
48
+ }
49
+
50
+ export function formatCuriosityFooter(question: CuriosityQuestion): string {
51
+ return [
52
+ "## Open Question",
53
+ "",
54
+ `Something I've been curious about: ${question.question}`,
55
+ "",
56
+ `_Context: ${question.context}_`,
57
+ ].join("\n");
58
+ }
59
+
60
+ export function contextBudgetForFooter(maxChars: number, footer?: string): number {
61
+ const normalizedFooter = trimText(footer);
62
+ if (maxChars <= 0 || normalizedFooter.length === 0) return Math.max(0, maxChars);
63
+ if (normalizedFooter.length + RECALL_CONTEXT_SEPARATOR.length >= maxChars) return 0;
64
+ return maxChars - normalizedFooter.length - RECALL_CONTEXT_SEPARATOR.length;
65
+ }
66
+
67
+ export function boundRecallContextComposition({
68
+ context: rawContext,
69
+ footer: rawFooter,
70
+ maxChars,
71
+ }: RenderMemoryContextPromptInput): RecallContextComposition {
72
+ if (!Number.isFinite(maxChars) || maxChars <= 0) return { context: "" };
73
+
74
+ const limit = Math.floor(maxChars);
75
+ const footer = trimText(rawFooter);
76
+ const context = truncateToBudget(trimText(rawContext), contextBudgetForFooter(limit, footer));
77
+ const boundedFooter = footer.length > limit ? truncateToBudget(footer, limit) : footer;
78
+ return { context, ...(boundedFooter ? { footer: boundedFooter } : {}) };
79
+ }
80
+ export function composeRecallContext(composition: RecallContextComposition): string {
81
+ const context = trimText(composition.context);
82
+ const footer = trimText(composition.footer);
83
+ if (context.length === 0) return footer;
84
+ if (footer.length === 0) return context;
85
+ return `${context}${RECALL_CONTEXT_SEPARATOR}${footer}`;
86
+ }
87
+
88
+ export function renderMemoryContextPrompt({
89
+ context: rawContext,
90
+ footer: rawFooter,
91
+ maxChars,
92
+ }: RenderMemoryContextPromptInput): RenderedMemoryContextPrompt | null {
93
+ const bounded = boundRecallContextComposition({
94
+ context: rawContext,
95
+ footer: rawFooter,
96
+ maxChars,
97
+ });
98
+ const body = composeRecallContext(bounded);
99
+ if (body.length === 0) return null;
100
+
101
+ const boundedBody = body.length <= maxChars ? body : truncateToBudget(bounded.footer || body, Math.floor(maxChars));
102
+ const lines = [MEMORY_CONTEXT_HEADER, "", boundedBody, "", MEMORY_CONTEXT_INSTRUCTION, ""];
103
+ return { body: boundedBody, lines, prompt: lines.join("\n").replace(/\n$/, "") };
104
+ }