@remnic/core 9.25.4 → 9.25.6

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 (155) 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 +41 -40
  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-JMDZDT24.js → chunk-D7MAWMMK.js} +18 -4
  54. package/dist/chunk-D7MAWMMK.js.map +1 -0
  55. package/dist/{chunk-TG2DJFH7.js → chunk-E5A6C4A5.js} +20 -20
  56. package/dist/{chunk-FDAPXLRC.js → chunk-FR75B4UU.js} +81 -55
  57. package/dist/chunk-FR75B4UU.js.map +1 -0
  58. package/dist/{chunk-DPYYSG6F.js → chunk-G6MGAE36.js} +7 -7
  59. package/dist/{chunk-GNFZFJBJ.js → chunk-IR4LGIH6.js} +2 -2
  60. package/dist/{chunk-MPDX5QMH.js → chunk-JA4HEXV7.js} +2 -2
  61. package/dist/{chunk-XTAAG4SQ.js → chunk-K4DP5KZA.js} +6 -6
  62. package/dist/{chunk-AT6DNUCM.js → chunk-NY6W6CJW.js} +2 -2
  63. package/dist/{chunk-NMGO2HAZ.js → chunk-OIE242WM.js} +2 -2
  64. package/dist/{chunk-SZN6XVFC.js → chunk-PS5OG75R.js} +2 -2
  65. package/dist/{chunk-VY6SDKWZ.js → chunk-SSYBI43M.js} +2 -2
  66. package/dist/{chunk-IE4HB6O5.js → chunk-TPYZ727G.js} +3 -3
  67. package/dist/{chunk-5ZGWA4AG.js → chunk-UMFLAACL.js} +6 -6
  68. package/dist/{chunk-THHPDPMS.js → chunk-UOURUMBN.js} +2 -2
  69. package/dist/{chunk-6LRTGCRV.js → chunk-VPSAVXHM.js} +3 -3
  70. package/dist/{chunk-AL4TZWI3.js → chunk-W33DEUKR.js} +2 -2
  71. package/dist/{chunk-W4ZDTHWZ.js → chunk-WIGBTWXA.js} +2 -2
  72. package/dist/{chunk-NVYCTASB.js → chunk-WSEA52RI.js} +2 -2
  73. package/dist/{chunk-LARPWF3S.js → chunk-ZPUFQGUF.js} +4 -4
  74. package/dist/{cli-Zc6UclD1.d.ts → cli-1W7cNjJJ.d.ts} +2 -2
  75. package/dist/cli.d.ts +4 -3
  76. package/dist/cli.js +28 -27
  77. package/dist/compounding/engine.js +3 -3
  78. package/dist/connectors/codex-materialize-runner.js +3 -3
  79. package/dist/connectors/index.js +3 -3
  80. package/dist/corpus-watermark.js +5 -5
  81. package/dist/entity-retrieval.js +3 -3
  82. package/dist/explicit-capture.d.ts +2 -1
  83. package/dist/{first-start-migration-LEXL3XYL.js → first-start-migration-BKNGOFED.js} +4 -4
  84. package/dist/index.d.ts +393 -392
  85. package/dist/index.js +73 -53
  86. package/dist/lcm/engine.js +3 -3
  87. package/dist/lcm/index.js +8 -8
  88. package/dist/maintenance/memory-governance.js +3 -3
  89. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  90. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  91. package/dist/mcp-memory-inspector-app.d.ts +3 -2
  92. package/dist/namespaces/migrate.js +4 -4
  93. package/dist/namespaces/storage.js +3 -3
  94. package/dist/operator-doctor-corpus.js +6 -6
  95. package/dist/operator-toolkit.js +9 -9
  96. package/dist/orchestration/maintenance.js +8 -8
  97. package/dist/{orchestrator-BTGgoKli.d.ts → orchestrator-BtA1wC5K.d.ts} +2 -0
  98. package/dist/orchestrator.d.ts +2 -1
  99. package/dist/orchestrator.js +41 -40
  100. package/dist/recall-context-composition.d.ts +30 -0
  101. package/dist/recall-context-composition.js +24 -0
  102. package/dist/recall-context-composition.js.map +1 -0
  103. package/dist/schemas.d.ts +76 -76
  104. package/dist/semantic-consolidation.js +4 -4
  105. package/dist/semantic-rule-promotion.js +3 -3
  106. package/dist/semantic-rule-verifier.js +3 -3
  107. package/dist/shared-context/manager.d.ts +8 -8
  108. package/dist/storage.js +2 -2
  109. package/dist/transfer/types.d.ts +66 -66
  110. package/dist/verified-recall.js +3 -3
  111. package/package.json +2 -2
  112. package/src/access-authorization-probe.ts +30 -1
  113. package/src/access-boundary.ts +21 -0
  114. package/src/access-http.test.ts +86 -1
  115. package/src/access-http.ts +6 -6
  116. package/src/access-mcp-output-schemas.ts +1 -0
  117. package/src/access-recall-response.ts +18 -13
  118. package/src/access-recall-surface.ts +11 -1
  119. package/src/access-service.ts +3 -0
  120. package/src/index.ts +1 -1
  121. package/src/orchestration/orchestrator-helpers.ts +2 -0
  122. package/src/orchestration/recall-internal.ts +49 -49
  123. package/src/recall-context-composition.test.ts +90 -0
  124. package/src/recall-context-composition.ts +104 -0
  125. package/src/recall-curiosity-composition.test.ts +301 -0
  126. package/dist/chunk-3I35RLJW.js.map +0 -1
  127. package/dist/chunk-FDAPXLRC.js.map +0 -1
  128. package/dist/chunk-JMDZDT24.js.map +0 -1
  129. package/dist/chunk-RROMWDDC.js.map +0 -1
  130. package/dist/chunk-TWCOYET2.js.map +0 -1
  131. /package/dist/{chunk-GF22HWS2.js.map → chunk-2PHX3I5C.js.map} +0 -0
  132. /package/dist/{chunk-LXKJNUND.js.map → chunk-37VPMR4E.js.map} +0 -0
  133. /package/dist/{chunk-3CDMIR3C.js.map → chunk-3CHQZVAL.js.map} +0 -0
  134. /package/dist/{chunk-U37RGPJT.js.map → chunk-5J7HJ7RT.js.map} +0 -0
  135. /package/dist/{chunk-AGR7MGUX.js.map → chunk-7IGWPXVC.js.map} +0 -0
  136. /package/dist/{chunk-FIGDB2HR.js.map → chunk-7KXQ44PA.js.map} +0 -0
  137. /package/dist/{chunk-MNLZGCM2.js.map → chunk-AQ2ZQYDI.js.map} +0 -0
  138. /package/dist/{chunk-TG2DJFH7.js.map → chunk-E5A6C4A5.js.map} +0 -0
  139. /package/dist/{chunk-DPYYSG6F.js.map → chunk-G6MGAE36.js.map} +0 -0
  140. /package/dist/{chunk-GNFZFJBJ.js.map → chunk-IR4LGIH6.js.map} +0 -0
  141. /package/dist/{chunk-MPDX5QMH.js.map → chunk-JA4HEXV7.js.map} +0 -0
  142. /package/dist/{chunk-XTAAG4SQ.js.map → chunk-K4DP5KZA.js.map} +0 -0
  143. /package/dist/{chunk-AT6DNUCM.js.map → chunk-NY6W6CJW.js.map} +0 -0
  144. /package/dist/{chunk-NMGO2HAZ.js.map → chunk-OIE242WM.js.map} +0 -0
  145. /package/dist/{chunk-SZN6XVFC.js.map → chunk-PS5OG75R.js.map} +0 -0
  146. /package/dist/{chunk-VY6SDKWZ.js.map → chunk-SSYBI43M.js.map} +0 -0
  147. /package/dist/{chunk-IE4HB6O5.js.map → chunk-TPYZ727G.js.map} +0 -0
  148. /package/dist/{chunk-5ZGWA4AG.js.map → chunk-UMFLAACL.js.map} +0 -0
  149. /package/dist/{chunk-THHPDPMS.js.map → chunk-UOURUMBN.js.map} +0 -0
  150. /package/dist/{chunk-6LRTGCRV.js.map → chunk-VPSAVXHM.js.map} +0 -0
  151. /package/dist/{chunk-AL4TZWI3.js.map → chunk-W33DEUKR.js.map} +0 -0
  152. /package/dist/{chunk-W4ZDTHWZ.js.map → chunk-WIGBTWXA.js.map} +0 -0
  153. /package/dist/{chunk-NVYCTASB.js.map → chunk-WSEA52RI.js.map} +0 -0
  154. /package/dist/{chunk-LARPWF3S.js.map → chunk-ZPUFQGUF.js.map} +0 -0
  155. /package/dist/{first-start-migration-LEXL3XYL.js.map → first-start-migration-BKNGOFED.js.map} +0 -0
@@ -38,7 +38,7 @@ import {
38
38
  import { expandTildePath } from "./utils/path.js";
39
39
  import { projectTagProjectId } from "./coding/coding-namespace.js";
40
40
  import { getOperation, type OperationName } from "./access-boundary.js";
41
- import { probeOperationAuthorization } from "./access-authorization-probe.js";
41
+ import { authorizationProbeNamespaces, probeOperationAuthorization } from "./access-authorization-probe.js";
42
42
  import { resolveQueryNamespaceWritablePreflight } from "./access-namespace-preflight.js";
43
43
  import {
44
44
  assertOperationAllowed,
@@ -892,11 +892,11 @@ export class EngramAccessHttpServer {
892
892
 
893
893
  if (req.method === "GET" && pathname === "/engram/v1/authorization") {
894
894
  res.setHeader("cache-control", "no-store");
895
- this.respondJson(
896
- res,
897
- 200,
898
- probeOperationAuthorization(tokenCapabilityStore.getStore(), parsed.searchParams.getAll("op")),
899
- );
895
+ const probe = probeOperationAuthorization(tokenCapabilityStore.getStore(), parsed.searchParams.getAll("op"));
896
+ const namespaceParam = parsed.searchParams.get("namespace") ?? undefined;
897
+ for (const namespace of authorizationProbeNamespaces(probe.operations, namespaceParam))
898
+ this.resolveNamespace(req, namespace);
899
+ this.respondJson(res, 200, probe);
900
900
  return;
901
901
  }
902
902
 
@@ -31,6 +31,7 @@ const TOOL_OUTPUT_SCHEMAS: Readonly<Record<string, Record<string, unknown>>> = {
31
31
  query: T_STRING,
32
32
  namespace: T_STRING,
33
33
  context: T_STRING,
34
+ contextComposition: objectSchema({ context: T_STRING, footer: T_STRING }),
34
35
  count: T_NUMBER,
35
36
  memoryIds: T_ARRAY,
36
37
  results: T_ARRAY,
@@ -16,6 +16,11 @@ import { decideDisclosureEscalation } from "./recall-disclosure-escalation.js";
16
16
  import { type TagMatchMode, applyTagFilter, normalizeTags, parseTagMatch } from "./recall-tag-filter.js";
17
17
  import type { RecallDisclosure, RecallPlanMode } from "./types.js";
18
18
  import { displaySafeBudgetsApplied, displaySafeRecallSnapshot } from "./orchestration/recall-result-formatter.js";
19
+ import {
20
+ boundRecallContextComposition,
21
+ composeRecallContext,
22
+ type RecallContextComposition,
23
+ } from "./recall-context-composition.js";
19
24
 
20
25
  /**
21
26
  * Build the recall response from the completed pipeline (issue #1906 review
@@ -29,6 +34,7 @@ export async function assembleRecallResponse(
29
34
  params: {
30
35
  request: EngramAccessRecallRequest;
31
36
  context: string;
37
+ contextComposition?: RecallContextComposition;
32
38
  query: string;
33
39
  mode: RecallPlanMode | undefined;
34
40
  namespace: string;
@@ -47,6 +53,7 @@ export async function assembleRecallResponse(
47
53
  const {
48
54
  request,
49
55
  context,
56
+ contextComposition,
50
57
  query,
51
58
  mode,
52
59
  namespace,
@@ -191,7 +198,8 @@ export async function assembleRecallResponse(
191
198
  err instanceof Error ? err.message : String(err),
192
199
  );
193
200
  }
194
- let effectiveContext = context;
201
+ let effectiveComposition: RecallContextComposition = contextComposition ?? { context };
202
+ let effectiveContext = composeRecallContext(effectiveComposition);
195
203
  if (filterTags && filterTags.length > 0) {
196
204
  const beforeIds = results.map((r) => r.id);
197
205
  const { results: admitted } = applyTagFilter(results, {
@@ -213,20 +221,16 @@ export async function assembleRecallResponse(
213
221
  const admittedIds = new Set(results.map((r) => r.id));
214
222
  const droppedAny = beforeIds.some((id) => !admittedIds.has(id));
215
223
  if (droppedAny) {
216
- effectiveContext = results
217
- .map((r) => {
218
- const content =
219
- typeof (r as { content?: unknown }).content === "string"
220
- ? ((r as { content?: string }).content ?? "")
221
- : "";
222
- const preview =
223
- typeof (r as { preview?: unknown }).preview === "string"
224
- ? ((r as { preview?: string }).preview ?? "")
225
- : "";
226
- return content || preview;
227
- })
224
+ const filteredContext = results
225
+ .map((result) => result.content || result.preview)
228
226
  .filter((s) => s.length > 0)
229
227
  .join("\n\n");
228
+ effectiveComposition = boundRecallContextComposition({
229
+ context: filteredContext,
230
+ footer: effectiveComposition.footer,
231
+ maxChars: deps.orchestrator.config.recallBudgetChars,
232
+ });
233
+ effectiveContext = composeRecallContext(effectiveComposition);
230
234
  }
231
235
  }
232
236
  const filteredMemoryIds = filterTags && filterTags.length > 0
@@ -289,6 +293,7 @@ export async function assembleRecallResponse(
289
293
  sessionKey: request.sessionKey,
290
294
  namespace: effectiveNamespace,
291
295
  context: effectiveContext,
296
+ contextComposition: effectiveComposition,
292
297
  count: filterTags && filterTags.length > 0
293
298
  ? results.length
294
299
  : (snapshot?.memoryIds.length ?? results.length),
@@ -27,6 +27,7 @@ import type { Orchestrator, RecallInvocationOptions } from "./orchestrator.js";
27
27
  import { decideDisclosureEscalation } from "./recall-disclosure-escalation.js";
28
28
  import { assembleRecallResponse } from "./access-recall-response.js";
29
29
  import type { LastRecallSnapshot } from "./recall-state.js";
30
+ import type { RecallContextComposition } from "./recall-context-composition.js";
30
31
  import { type TagMatchMode, applyTagFilter, normalizeTags, parseTagMatch } from "./recall-tag-filter.js";
31
32
  import { type RecallXraySnapshot, estimateRecallTokens } from "./recall-xray.js";
32
33
  import { SecureStoreLockedError } from "./secure-store/index.js";
@@ -852,10 +853,14 @@ export class AccessRecallSurface {
852
853
  }
853
854
  asOf = request.asOf;
854
855
  }
856
+ let contextComposition: RecallContextComposition | undefined;
855
857
  const recallOptions: RecallInvocationOptions = {
856
858
  namespace: namespaceOverride,
857
859
  topK,
858
860
  mode,
861
+ onContextComposition: (composition) => {
862
+ contextComposition = composition;
863
+ },
859
864
  ...(authenticatedPrincipal ? { principalOverride: authenticatedPrincipal } : {}),
860
865
  ...(asOf !== undefined ? { asOf } : {}),
861
866
  ...(request.includeLowConfidence === true ? { includeLowConfidence: true } : {}),
@@ -892,10 +897,15 @@ export class AccessRecallSurface {
892
897
  // construction — so ANY failure after the reserve releases the exact
893
898
  // budget entry (by token, review #4) instead of leaking it.
894
899
  try {
895
- const context = await this.deps.orchestrator.recall(query, request.sessionKey, recallOptions);
900
+ const context = await this.deps.orchestrator.recall(
901
+ query,
902
+ request.sessionKey,
903
+ recallOptions,
904
+ );
896
905
  const assembled = await assembleRecallResponse(this.deps, {
897
906
  request,
898
907
  context,
908
+ contextComposition,
899
909
  query,
900
910
  mode,
901
911
  namespace,
@@ -170,6 +170,7 @@ import {
170
170
  type ScopeInspection,
171
171
  } from "./admin/admin-surfaces.js";
172
172
  import type { LastRecallSnapshot } from "./recall-state.js";
173
+ import type { RecallContextComposition } from "./recall-context-composition.js";
173
174
  import type {
174
175
  GraphRecallSnapshot,
175
176
  IntentDebugSnapshot,
@@ -453,6 +454,8 @@ export interface EngramAccessRecallResponse {
453
454
  sessionKey?: string;
454
455
  namespace: string;
455
456
  context: string;
457
+ /** Request-local split used by adapters that must re-render a tighter prompt budget. */
458
+ contextComposition?: RecallContextComposition;
456
459
  count: number;
457
460
  memoryIds: string[];
458
461
  results: EngramAccessMemorySummary[];
package/src/index.ts CHANGED
@@ -471,7 +471,7 @@ export {
471
471
  } from "./resolve-auth-token.js";
472
472
  export type { ResolveSecretRefFn } from "./resolve-auth-token.js";
473
473
  export type { SecretRef, AgentAccessAuthToken } from "./types.js";
474
-
474
+ export * from "./recall-context-composition.js";
475
475
  // Recall X-ray CLI helpers (issue #570). Exported so the standalone
476
476
  // `@remnic/cli` binary can wire the `remnic xray` command without
477
477
  // reimporting core-internal modules by relative path (CLAUDE.md rule 26).
@@ -27,6 +27,7 @@ import type { GraphRecallRankedResult, GraphRecallShadowComparison } from "./gra
27
27
  import { parseQmdExplain } from "../qmd.js";
28
28
  import type { GraphRecallExpandedEntry } from "../recall-state.js";
29
29
  import type { RecallXrayServedBy } from "../recall-xray.js";
30
+ import type { RecallContextComposition } from "../recall-context-composition.js";
30
31
  import { type BufferTurn, type MemoryActionEvent, type MemoryFile, type MemoryFrontmatter, type MemoryIntent, type PluginConfig, type QmdSearchResult, type RecallPlanMode, confidenceTier } from "../types.js";
31
32
  import { categoryDirName } from "../utils/category-dir.js";
32
33
  import { parseFlexibleIsoTimestamp } from "../utils/iso-timestamp.js";
@@ -256,6 +257,7 @@ export interface RecallInvocationOptions {
256
257
  * absent, so existing timing consumers are unaffected.
257
258
  */
258
259
  queueWaitMs?: number;
260
+ onContextComposition?: (composition: RecallContextComposition) => void | PromiseLike<void>;
259
261
  }
260
262
 
261
263
  export type QueryAwarePrefilter = {
@@ -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
+ }