@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
@@ -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[];
@@ -12,6 +12,11 @@ const SOURCE_TURN: BufferTurn = {
12
12
  timestamp: "2026-07-25T12:00:00.000Z",
13
13
  };
14
14
 
15
+ const TAG_SOURCE_TURN: BufferTurn = {
16
+ ...SOURCE_TURN,
17
+ content: "The page uses <main> as the primary landmark.",
18
+ };
19
+
15
20
  const SOURCE_GROUNDED_EXTRACTION = {
16
21
  facts: [{
17
22
  category: "fact",
@@ -23,6 +28,81 @@ const SOURCE_GROUNDED_EXTRACTION = {
23
28
  entities: [],
24
29
  questions: [],
25
30
  };
31
+ const STRUCTURAL_PLACEHOLDER_EXTRACTION = {
32
+ facts: [{
33
+ category: "<category>",
34
+ content: "<source-grounded statement>",
35
+ confidence: 0,
36
+ tags: ["<tag>"],
37
+ entityRef: "<optional normalized-name>",
38
+ promptedByQuestion: "<optional source-grounded question>",
39
+ quote: "<optional exact contiguous source span>",
40
+ },
41
+ {
42
+ category: "<category>",
43
+ content: SOURCE_TURN.content,
44
+ confidence: 0.95,
45
+ tags: ["theme"],
46
+ },
47
+ {
48
+ category: "procedure",
49
+ content: SOURCE_TURN.content,
50
+ confidence: 0.95,
51
+ tags: ["theme"],
52
+ procedureSteps: [
53
+ { order: 1, intent: "<step>" },
54
+ { order: 2, intent: "<step>" },
55
+ ],
56
+ },
57
+ {
58
+ category: "reasoning_trace",
59
+ content: SOURCE_TURN.content,
60
+ confidence: 0.95,
61
+ tags: ["theme"],
62
+ reasoningTrace: {
63
+ steps: [
64
+ { order: 1, description: "<step>" },
65
+ { order: 2, description: "<step>" },
66
+ ],
67
+ finalAnswer: "<answer>",
68
+ },
69
+ }],
70
+ profileUpdates: ["<source-grounded profile update>"],
71
+ entities: [{
72
+ name: "<normalized-name>",
73
+ type: "<entity-type>",
74
+ facts: ["<source-grounded statement>"],
75
+ promptedByQuestion: "<optional source-grounded question>",
76
+ },
77
+ {
78
+ name: "Moonlight",
79
+ type: "<entity-type>",
80
+ facts: [SOURCE_TURN.content],
81
+ }],
82
+ questions: [{
83
+ question: "<source-grounded unresolved question>",
84
+ context: "<source-grounded context>",
85
+ priority: 0,
86
+ }],
87
+ identityReflection: "<conversation-grounded agent reflection>",
88
+ relationships: [{
89
+ source: "<normalized-name>",
90
+ target: "<normalized-name>",
91
+ label: "<source-grounded relationship>",
92
+ }],
93
+ };
94
+
95
+ const GATEWAY_PLACEHOLDER_EXTRACTION = {
96
+ ...STRUCTURAL_PLACEHOLDER_EXTRACTION,
97
+ facts: [{
98
+ ...STRUCTURAL_PLACEHOLDER_EXTRACTION.facts[0],
99
+ category: "fact",
100
+ }],
101
+ entities: [{
102
+ ...STRUCTURAL_PLACEHOLDER_EXTRACTION.entities[0],
103
+ type: "other",
104
+ }],
105
+ };
26
106
 
27
107
  type ChatMessage = { role: string; content: string };
28
108
 
@@ -100,6 +180,105 @@ test("local extraction prompt uses placeholders and accepts an empty question li
100
180
  assert.doesNotMatch(prompt, /^- rule:/m);
101
181
  });
102
182
 
183
+ test("local extraction discards literal structural placeholders", async () => {
184
+ const engine = new ExtractionEngine(parseConfig({
185
+ localLlmEnabled: true,
186
+ localLlmModel: "fixture-local",
187
+ localLlmFallback: false,
188
+ }));
189
+ const localLlm: LocalLlmFixture = {
190
+ async chatCompletion() {
191
+ return { content: JSON.stringify(STRUCTURAL_PLACEHOLDER_EXTRACTION) };
192
+ },
193
+ };
194
+ const modelRegistry = {
195
+ calculateContextSizes: () => ({ maxInputChars: 8_000, maxOutputTokens: 1_000, description: "fixture" }),
196
+ };
197
+ assert.equal(Reflect.set(engine, "localLlm", localLlm), true);
198
+ assert.equal(Reflect.set(engine, "modelRegistry", modelRegistry), true);
199
+
200
+ const result = await engine.extract([SOURCE_TURN]);
201
+
202
+ assert.deepEqual(result.facts, []);
203
+ assert.deepEqual(result.profileUpdates, []);
204
+ assert.deepEqual(result.entities, []);
205
+ assert.deepEqual(result.questions, []);
206
+ assert.deepEqual(result.relationships, []);
207
+ assert.equal(result.identityReflection, undefined);
208
+ });
209
+
210
+ test("local extraction preserves source text that looks like a tag", async () => {
211
+ const engine = new ExtractionEngine(parseConfig({
212
+ localLlmEnabled: true,
213
+ localLlmModel: "fixture-local",
214
+ localLlmFallback: false,
215
+ }));
216
+ const localLlm: LocalLlmFixture = {
217
+ async chatCompletion() {
218
+ return {
219
+ content: JSON.stringify({
220
+ facts: [{ category: "fact", content: "<main>", confidence: 0.95, tags: [] }],
221
+ profileUpdates: [],
222
+ entities: [],
223
+ questions: [],
224
+ }),
225
+ };
226
+ },
227
+ };
228
+ const modelRegistry = {
229
+ calculateContextSizes: () => ({ maxInputChars: 8_000, maxOutputTokens: 1_000, description: "fixture" }),
230
+ };
231
+ assert.equal(Reflect.set(engine, "localLlm", localLlm), true);
232
+ assert.equal(Reflect.set(engine, "modelRegistry", modelRegistry), true);
233
+
234
+ const result = await engine.extract([TAG_SOURCE_TURN]);
235
+
236
+ assert.equal(result.facts[0]?.content, "<main>");
237
+ });
238
+
239
+ test("local truncated recovery discards literal structural placeholders", async () => {
240
+ const engine = new ExtractionEngine(parseConfig({
241
+ localLlmEnabled: true,
242
+ localLlmModel: "fixture-local",
243
+ localLlmFallback: false,
244
+ }));
245
+ const localLlm: LocalLlmFixture = {
246
+ async chatCompletion() {
247
+ return {
248
+ content: "{\"facts\":[{\"category\":\"<category>\",\"content\":\"<source-grounded statement>\",\"confidence\":0.7}",
249
+ };
250
+ },
251
+ };
252
+ const modelRegistry = {
253
+ calculateContextSizes: () => ({ maxInputChars: 8_000, maxOutputTokens: 1_000, description: "fixture" }),
254
+ };
255
+ assert.equal(Reflect.set(engine, "localLlm", localLlm), true);
256
+ assert.equal(Reflect.set(engine, "modelRegistry", modelRegistry), true);
257
+
258
+ const result = await engine.extract([SOURCE_TURN]);
259
+
260
+ assert.deepEqual(result.facts, []);
261
+ });
262
+
263
+ test("gateway extraction discards literal structural placeholders", async () => {
264
+ const engine = new ExtractionEngine(parseConfig({ modelSource: "gateway" }));
265
+ const fallbackLlm: GatewayFixture = {
266
+ async parseWithSchemaDetailed() {
267
+ return { modelUsed: "fixture-gateway", result: GATEWAY_PLACEHOLDER_EXTRACTION };
268
+ },
269
+ };
270
+ assert.equal(Reflect.set(engine, "fallbackLlm", fallbackLlm), true);
271
+
272
+ const result = await engine.extract([SOURCE_TURN]);
273
+
274
+ assert.deepEqual(result.facts, []);
275
+ assert.deepEqual(result.profileUpdates, []);
276
+ assert.deepEqual(result.entities, []);
277
+ assert.deepEqual(result.questions, []);
278
+ assert.deepEqual(result.relationships, []);
279
+ assert.equal(result.identityReflection, undefined);
280
+ });
281
+
103
282
  test("gateway extraction prompt uses placeholders and accepts an empty question list", async () => {
104
283
  const engine = new ExtractionEngine(parseConfig({ modelSource: "gateway" }));
105
284
  let calls = 0;