@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
package/src/extraction.ts CHANGED
@@ -40,6 +40,7 @@ import { normalizeProcedureSteps } from "./procedural/procedure-types.js";
40
40
  import { normalizeReasoningTrace } from "./reasoning-trace-types.js";
41
41
  import { looksLikeMechanicalTelemetryTranscript } from "./telemetry-transcript.js";
42
42
  import { buildFactProvenance, type ProvenanceTurnInput } from "./provenance.js";
43
+ import { isMemoryCategory } from "./write-envelope.js";
43
44
  import { classifyExtractionThrownError, classifyFallbackParseFailure } from "./extraction-error-classification.js";
44
45
  export { classifyExtractionThrownError, classifyFallbackParseFailure } from "./extraction-error-classification.js";
45
46
  import { resolvePipelineProcessingCapabilities } from "./capabilities.js";
@@ -83,6 +84,10 @@ const EXTRACTION_RESPONSE_SHAPE = `{
83
84
  "identityReflection": "<conversation-grounded agent reflection>",
84
85
  "relationships": [{"source": "<normalized-name>", "target": "<normalized-name>", "label": "<source-grounded relationship>"}]
85
86
  }`;
87
+ const EXTRACTION_RESPONSE_PLACEHOLDERS: Record<string, true> = {};
88
+ for (const placeholder of EXTRACTION_RESPONSE_SHAPE.match(/<[^<>\r\n]+>/g) ?? []) {
89
+ EXTRACTION_RESPONSE_PLACEHOLDERS[placeholder] = true;
90
+ }
86
91
  const CONSOLIDATION_RESPONSE_SCHEMA = `{
87
92
  "items": [
88
93
  {
@@ -101,6 +106,46 @@ function isPlainRecord(value: unknown): value is Record<string, unknown> {
101
106
  return typeof value === "object" && value !== null && !Array.isArray(value);
102
107
  }
103
108
 
109
+ function containsExtractionPlaceholder(value: unknown): boolean {
110
+ if (typeof value === "string") return EXTRACTION_RESPONSE_PLACEHOLDERS[value.trim()] === true;
111
+ if (Array.isArray(value)) return value.some(containsExtractionPlaceholder);
112
+ return isPlainRecord(value) && Object.values(value).some(containsExtractionPlaceholder);
113
+ }
114
+
115
+ function extractionText(value: unknown): string | undefined {
116
+ if (typeof value !== "string") return undefined;
117
+ const text = value.trim();
118
+ return text.length > 0 && !containsExtractionPlaceholder(text) ? text : undefined;
119
+ }
120
+
121
+ function extractionAttributes(value: unknown): Record<string, string> | undefined {
122
+ if (!isPlainRecord(value)) return undefined;
123
+ const attributes: Record<string, string> = {};
124
+ for (const [key, candidate] of Object.entries(value)) {
125
+ const normalizedKey = extractionText(key);
126
+ const normalizedValue = extractionText(candidate);
127
+ if (normalizedKey !== undefined && normalizedValue !== undefined) {
128
+ attributes[normalizedKey] = normalizedValue;
129
+ }
130
+ }
131
+ return Object.keys(attributes).length > 0 ? attributes : undefined;
132
+ }
133
+
134
+ function extractionEntityType(value: unknown): ExtractedEntityResult["type"] | undefined {
135
+ const type = extractionText(value);
136
+ if (
137
+ type === "person" ||
138
+ type === "project" ||
139
+ type === "tool" ||
140
+ type === "company" ||
141
+ type === "place" ||
142
+ type === "other"
143
+ ) {
144
+ return type;
145
+ }
146
+ return undefined;
147
+ }
148
+
104
149
  function normalizeQuestion(question: ExtractionQuestion): ExtractionQuestion {
105
150
  const priority = Number.isFinite(question.priority)
106
151
  ? Math.max(0, Math.min(1, question.priority))
@@ -276,182 +321,168 @@ export class ExtractionEngine {
276
321
  }
277
322
 
278
323
  private normalizeExtractionResultPayload(parsed: any): ExtractionResult {
279
- const entities = Array.isArray(parsed?.entities)
324
+ const entities: ExtractedEntityResult[] = Array.isArray(parsed?.entities)
280
325
  ? parsed.entities
281
- .map((e: any) => this.normalizeEntityUpdate(e))
282
- .filter((e: any) => e.name.length > 0)
326
+ .map((candidate: unknown): ExtractedEntityResult | undefined => this.normalizeEntityUpdate(candidate))
327
+ .filter((entity: ExtractedEntityResult | undefined): entity is ExtractedEntityResult => (
328
+ entity !== undefined && entity.name.length > 0
329
+ ))
283
330
  : [];
284
331
 
285
332
  const facts = Array.isArray(parsed?.facts)
286
333
  ? parsed.facts
287
- .map((f: any) => ({
288
- category: typeof f?.category === "string" ? f.category : "fact",
289
- content: typeof f?.content === "string" ? f.content : typeof f?.text === "string" ? f.text : "",
290
- confidence: typeof f?.confidence === "number" ? f.confidence : 0.7,
291
- tags: Array.isArray(f?.tags) ? f.tags.filter((t: any) => typeof t === "string") : [],
292
- entityRef: typeof f?.entityRef === "string" ? f.entityRef : undefined,
293
- promptedByQuestion:
294
- typeof f?.promptedByQuestion === "string" ? f.promptedByQuestion : undefined,
295
- scope:
296
- f?.scope === "global" || f?.scope === "project" ? f.scope : undefined,
297
- structuredAttributes:
298
- f?.structuredAttributes && typeof f.structuredAttributes === "object" && !Array.isArray(f.structuredAttributes)
299
- ? Object.fromEntries(
300
- Object.entries(f.structuredAttributes)
301
- .filter(([k, v]) => typeof k === "string" && typeof v === "string")
302
- ) as Record<string, string>
303
- : undefined,
304
- procedureSteps: Array.isArray(f?.procedureSteps)
334
+ .map((candidate: unknown) => {
335
+ const f = isPlainRecord(candidate) ? candidate : {};
336
+ const category = typeof f.category === "string" ? f.category.trim() : "fact";
337
+ const reasoningTraceInput = isPlainRecord(f.reasoningTrace)
338
+ ? f.reasoningTrace
339
+ : isPlainRecord(f?.reasoning_trace)
340
+ ? f.reasoning_trace
341
+ : undefined;
342
+ if (!isMemoryCategory(category)) return undefined;
343
+ const procedureSteps = Array.isArray(f.procedureSteps)
305
344
  ? normalizeProcedureSteps(f.procedureSteps)
306
- : undefined,
307
- reasoningTrace: (() => {
308
- // Accept both camelCase and snake_case payload keys. The
309
- // category itself is snake_case and we already tolerate
310
- // snake_case nested fields in normalizeReasoningTrace, so a
311
- // loose local/direct LLM that outputs `reasoning_trace` on the
312
- // fact should not silently drop the structured chain.
313
- const candidate =
314
- f?.reasoningTrace && typeof f.reasoningTrace === "object" && !Array.isArray(f.reasoningTrace)
315
- ? f.reasoningTrace
316
- : f?.reasoning_trace && typeof f.reasoning_trace === "object" && !Array.isArray(f.reasoning_trace)
317
- ? f.reasoning_trace
318
- : null;
319
- return candidate ? normalizeReasoningTrace(candidate) ?? undefined : undefined;
320
- })(),
321
- // Issue #1575 PR 2: the LLM provides a verbatim supporting quote
322
- // per fact. Optional/nullable per repo gotcha 6 (OpenAI Responses
323
- // API emits null for absent optional fields). The post-parse
324
- // validator (buildFactProvenance) locates this quote in the
325
- // buffered turns and builds verified ProvenanceSource[] entries.
326
- quote:
327
- typeof f?.quote === "string" && f.quote.trim().length > 0
328
- ? f.quote
329
- : undefined,
330
- eventTime:
331
- typeof f?.eventTime === "string" && f.eventTime.trim().length > 0
332
- ? f.eventTime.trim()
333
- : typeof f?.event_time === "string" && f.event_time.trim().length > 0
334
- ? f.event_time.trim()
335
- : undefined,
336
- }))
337
- .filter((f: any) => f.content.length > 0)
338
- : [];
339
-
340
- const questions = Array.isArray(parsed?.questions)
341
- ? parsed.questions
342
- .map((q: any) => {
343
- if (typeof q === "string") return { question: q, context: "", priority: 0.5 };
345
+ : undefined;
346
+ const reasoningTrace = reasoningTraceInput
347
+ ? normalizeReasoningTrace(reasoningTraceInput) ?? undefined
348
+ : undefined;
349
+ if (
350
+ containsExtractionPlaceholder(procedureSteps) ||
351
+ containsExtractionPlaceholder(reasoningTrace)
352
+ ) {
353
+ return undefined;
354
+ }
344
355
  return {
345
- question: typeof q?.question === "string" ? q.question : typeof q?.text === "string" ? q.text : "",
346
- context: typeof q?.context === "string" ? q.context : "",
347
- priority: typeof q?.priority === "number" ? q.priority : 0.5,
356
+ category,
357
+ content: extractionText(f.content) ?? extractionText(f.text) ?? "",
358
+ confidence: typeof f.confidence === "number" ? f.confidence : 0.7,
359
+ tags: Array.isArray(f.tags)
360
+ ? f.tags.flatMap((tag: unknown) => {
361
+ const text = extractionText(tag);
362
+ return text === undefined ? [] : [text];
363
+ })
364
+ : [],
365
+ entityRef: extractionText(f.entityRef),
366
+ promptedByQuestion: extractionText(f.promptedByQuestion),
367
+ scope:
368
+ f.scope === "global" || f.scope === "project" ? f.scope : undefined,
369
+ structuredAttributes: extractionAttributes(f.structuredAttributes),
370
+ procedureSteps,
371
+ reasoningTrace,
372
+ quote: extractionText(f.quote),
373
+ eventTime: extractionText(f.eventTime) ?? extractionText(f.event_time),
348
374
  };
349
375
  })
350
- .filter((q: any) => q.question.length > 0)
376
+ .filter((fact: ExtractedFactResult | undefined): fact is ExtractedFactResult => (
377
+ fact !== undefined && fact.content.length > 0
378
+ ))
351
379
  : [];
352
380
 
381
+ const questions: ExtractionQuestion[] = Array.isArray(parsed?.questions)
382
+ ? parsed.questions.flatMap((candidate: unknown) => {
383
+ const record = isPlainRecord(candidate) ? candidate : undefined;
384
+ const question =
385
+ extractionText(record?.question) ??
386
+ extractionText(record?.text) ??
387
+ extractionText(candidate);
388
+ if (question === undefined) return [];
389
+ return [{
390
+ question,
391
+ context: extractionText(record?.context) ?? "",
392
+ priority: typeof record?.priority === "number" ? record.priority : 0.5,
393
+ }];
394
+ })
395
+ : [];
396
+
397
+ const profileUpdates = Array.isArray(parsed?.profileUpdates)
398
+ ? parsed.profileUpdates.flatMap((candidate: unknown) => {
399
+ const update = extractionText(candidate);
400
+ return update === undefined ? [] : [update];
401
+ })
402
+ : [];
403
+
404
+ const relationships: ExtractedRelationshipResult[] | undefined = Array.isArray(parsed?.relationships)
405
+ ? parsed.relationships.flatMap((candidate: unknown) => {
406
+ const relationship = isPlainRecord(candidate) ? candidate : undefined;
407
+ const source = extractionText(relationship?.source);
408
+ const target = extractionText(relationship?.target);
409
+ const label = extractionText(relationship?.label);
410
+ if (source === undefined || target === undefined || label === undefined) return [];
411
+ return [{
412
+ source,
413
+ target,
414
+ label,
415
+ promptedByQuestion: extractionText(relationship?.promptedByQuestion),
416
+ }];
417
+ })
418
+ : undefined;
419
+
353
420
  return {
354
421
  facts,
355
422
  entities,
356
- profileUpdates: Array.isArray(parsed?.profileUpdates)
357
- ? parsed.profileUpdates.filter((u: any) => typeof u === "string" && u.trim().length > 0)
358
- : [],
423
+ profileUpdates,
359
424
  questions,
360
- identityReflection: parsed?.identityReflection ?? undefined,
361
- relationships: Array.isArray(parsed?.relationships)
362
- ? parsed.relationships.filter(
363
- (r: any) =>
364
- typeof r?.source === "string" &&
365
- typeof r?.target === "string" &&
366
- typeof r?.label === "string",
367
- )
368
- .map((r: any) => ({
369
- source: r.source,
370
- target: r.target,
371
- label: r.label,
372
- promptedByQuestion:
373
- typeof r?.promptedByQuestion === "string" ? r.promptedByQuestion : undefined,
374
- }))
375
- : undefined,
425
+ identityReflection: extractionText(parsed?.identityReflection),
426
+ relationships,
376
427
  };
377
428
  }
378
429
 
379
- private normalizeEntityUpdate(entity: any): ExtractedEntityResult {
380
- const rawUpdates = isPlainRecord(entity?.updates) ? entity.updates : null;
381
- const directFacts = Array.isArray(entity?.facts)
382
- ? entity.facts
383
- .filter((fact: any) => typeof fact === "string")
384
- .map((fact: string) => fact.trim())
385
- .filter((fact: string) => fact.length > 0)
386
- : [];
387
- const updateFacts = rawUpdates && Array.isArray(rawUpdates.facts)
388
- ? rawUpdates.facts
389
- .filter((fact: unknown) => typeof fact === "string")
390
- .map((fact: string) => fact.trim())
391
- .filter((fact: string) => fact.length > 0)
392
- : [];
430
+ private normalizeEntityUpdate(entity: unknown): ExtractedEntityResult | undefined {
431
+ const record = isPlainRecord(entity) ? entity : {};
432
+ const rawUpdates = isPlainRecord(record.updates) ? record.updates : undefined;
433
+ const normalizedTexts = (value: unknown): string[] =>
434
+ Array.isArray(value)
435
+ ? value.flatMap((candidate: unknown) => {
436
+ const text = extractionText(candidate);
437
+ return text === undefined ? [] : [text];
438
+ })
439
+ : [];
440
+ const directFacts = normalizedTexts(record.facts);
441
+ const updateFacts = normalizedTexts(rawUpdates?.facts);
393
442
  const scalarUpdateFacts = rawUpdates
394
- ? Object.keys(rawUpdates)
395
- .sort((a, b) => a.localeCompare(b))
396
- .filter((key) => !["facts", "name", "promptedByQuestion", "structuredSections", "type"].includes(key))
397
- .flatMap((key) => {
398
- const value = rawUpdates[key];
399
- if (typeof value === "string" && value.trim().length > 0) {
400
- return [`${key}: ${value.trim()}`];
443
+ ? Object.entries(rawUpdates)
444
+ .sort(([left], [right]) => left.localeCompare(right))
445
+ .flatMap(([key, value]) => {
446
+ if (["facts", "name", "promptedByQuestion", "structuredSections", "type"].includes(key)) {
447
+ return [];
401
448
  }
449
+ const normalizedKey = extractionText(key);
450
+ if (normalizedKey === undefined) return [];
451
+ const normalizedValue = extractionText(value);
452
+ if (normalizedValue !== undefined) return [`${normalizedKey}: ${normalizedValue}`];
402
453
  if (typeof value === "number" || typeof value === "boolean") {
403
- return [`${key}: ${String(value)}`];
454
+ return [`${normalizedKey}: ${String(value)}`];
404
455
  }
405
456
  return [];
406
457
  })
407
458
  : [];
408
- const structuredSectionsSource = Array.isArray(entity?.structuredSections)
409
- ? entity.structuredSections
459
+ const structuredSectionsSource = Array.isArray(record.structuredSections)
460
+ ? record.structuredSections
410
461
  : Array.isArray(rawUpdates?.structuredSections)
411
462
  ? rawUpdates.structuredSections
412
463
  : [];
413
- const name =
414
- typeof entity?.name === "string"
415
- ? entity.name.trim()
416
- : typeof entity?.entityId === "string"
417
- ? entity.entityId.trim()
418
- : typeof rawUpdates?.name === "string"
419
- ? rawUpdates.name.trim()
420
- : "";
421
- const type =
422
- typeof entity?.type === "string" && entity.type.trim().length > 0
423
- ? entity.type.trim()
424
- : typeof rawUpdates?.type === "string" && rawUpdates.type.trim().length > 0
425
- ? rawUpdates.type.trim()
426
- : "other";
464
+ const structuredSections = structuredSectionsSource.flatMap((candidate: unknown) => {
465
+ const section = isPlainRecord(candidate) ? candidate : {};
466
+ const key = extractionText(section.key);
467
+ const title = extractionText(section.title);
468
+ const facts = normalizedTexts(section.facts);
469
+ if (key === undefined || title === undefined || facts.length === 0) return [];
470
+ return [{ key, title, facts }];
471
+ });
427
472
 
473
+ const rawType = record.type ?? rawUpdates?.type;
474
+ const type = rawType === undefined ? "other" : extractionEntityType(rawType);
475
+ if (type === undefined) return undefined;
428
476
  return {
429
- name,
477
+ name:
478
+ extractionText(record.name) ??
479
+ extractionText(record.entityId) ??
480
+ extractionText(rawUpdates?.name) ??
481
+ "",
430
482
  type,
431
483
  facts: [...directFacts, ...updateFacts, ...scalarUpdateFacts],
432
- structuredSections: structuredSectionsSource.length > 0
433
- ? structuredSectionsSource
434
- .map((section: any) => ({
435
- key: typeof section?.key === "string" ? section.key.trim() : "",
436
- title: typeof section?.title === "string" ? section.title.trim() : "",
437
- facts: Array.isArray(section?.facts)
438
- ? section.facts.filter((fact: any) => typeof fact === "string")
439
- .map((fact: string) => fact.trim())
440
- .filter((fact: string) => fact.length > 0)
441
- : [],
442
- }))
443
- .filter((section: any) => (
444
- section.key.length > 0 &&
445
- section.title.length > 0 &&
446
- section.facts.length > 0
447
- ))
448
- : undefined,
449
- promptedByQuestion:
450
- typeof entity?.promptedByQuestion === "string"
451
- ? entity.promptedByQuestion
452
- : typeof rawUpdates?.promptedByQuestion === "string"
453
- ? rawUpdates.promptedByQuestion
454
- : undefined,
484
+ structuredSections: structuredSections.length > 0 ? structuredSections : undefined,
485
+ promptedByQuestion: extractionText(record.promptedByQuestion) ?? extractionText(rawUpdates?.promptedByQuestion),
455
486
  };
456
487
  }
457
488
 
@@ -615,8 +646,10 @@ export class ExtractionEngine {
615
646
  )
616
647
  .filter((update) => update.length > 0);
617
648
  const entityUpdates = (Array.isArray(result.entityUpdates) ? result.entityUpdates : [])
618
- .map((entity: any) => this.normalizeEntityUpdate(entity))
619
- .filter((entity: ExtractedEntityResult) => entity.name.length > 0);
649
+ .map((entity: unknown): ExtractedEntityResult | undefined => this.normalizeEntityUpdate(entity))
650
+ .filter((entity: ExtractedEntityResult | undefined): entity is ExtractedEntityResult => (
651
+ entity !== undefined && entity.name.length > 0
652
+ ));
620
653
  return { items, profileUpdates, entityUpdates };
621
654
  }
622
655
 
@@ -1310,37 +1343,8 @@ export class ExtractionEngine {
1310
1343
  log.debug(
1311
1344
  `extracted ${result.facts.length} facts, ${result.entities.length} entities, ${(result.questions ?? []).length} questions via fallback (${detailed.modelUsed})`,
1312
1345
  );
1313
- // Zod schema accepts snake_case aliases (final_answer / observed_outcome)
1314
- // alongside camelCase for gateway-tolerance, but the downstream
1315
- // ExtractedFact contract only exposes camelCase. Collapse each fact's
1316
- // reasoningTrace through normalizeReasoningTrace before passing it on so
1317
- // gateway output matches the shape local/direct-client paths produce.
1318
- const normalizedFacts = result.facts.map((f: any) => {
1319
- if (!f) return f;
1320
- // Gateway tolerance: collapse snake_case event_time → camelCase
1321
- // eventTime so the gateway path matches the local/direct/proactive
1322
- // normalization (#1578 r3 — cursor bugbot).
1323
- const eventTime =
1324
- typeof f.eventTime === "string" && f.eventTime.trim().length > 0
1325
- ? f.eventTime.trim()
1326
- : typeof f.event_time === "string" && f.event_time.trim().length > 0
1327
- ? f.event_time.trim()
1328
- : undefined;
1329
- if (!f.reasoningTrace && eventTime === undefined) return f;
1330
- return {
1331
- ...f,
1332
- ...(f.reasoningTrace
1333
- ? { reasoningTrace: normalizeReasoningTrace(f.reasoningTrace) ?? undefined }
1334
- : {}),
1335
- ...(eventTime !== undefined ? { eventTime } : {}),
1336
- };
1337
- });
1338
- const sanitized = this.sanitizeExtractionResult({
1339
- ...result,
1340
- facts: normalizedFacts,
1341
- questions: result.questions ?? [],
1342
- identityReflection: result.identityReflection ?? undefined,
1343
- } as ExtractionResult, messageTimestamp);
1346
+ const normalized = this.normalizeExtractionResultPayload(result);
1347
+ const sanitized = this.sanitizeExtractionResult(normalized, messageTimestamp);
1344
1348
  const finalResult = await this.applyProactiveQuestionPass(conversation, sanitized);
1345
1349
  return this.attachProvenanceToResult(finalResult, boundedTurns);
1346
1350
  }
@@ -1567,29 +1571,6 @@ ${truncatedConversation}`;
1567
1571
  * Local LLMs sometimes hit token limits mid-JSON. This tries to salvage valid facts.
1568
1572
  */
1569
1573
  private extractPartialFacts(jsonStr: string): ExtractionResult {
1570
- const allowedCategories = new Set([
1571
- "fact",
1572
- "preference",
1573
- "correction",
1574
- "entity",
1575
- "decision",
1576
- "relationship",
1577
- "principle",
1578
- "commitment",
1579
- "moment",
1580
- "skill",
1581
- "rule",
1582
- "procedure",
1583
- "reasoning_trace",
1584
- ]);
1585
- const allowedEntityTypes = new Set([
1586
- "person",
1587
- "project",
1588
- "tool",
1589
- "company",
1590
- "place",
1591
- "other",
1592
- ]);
1593
1574
 
1594
1575
  const facts: ExtractionResult["facts"] = [];
1595
1576
  const entities: ExtractionResult["entities"] = [];
@@ -1599,8 +1580,8 @@ ${truncatedConversation}`;
1599
1580
  const factRegex = /\{\s*"category"\s*:\s*"([^"]+)"\s*,\s*"content"\s*:\s*"([^"]+)"\s*,\s*"confidence"\s*:\s*([0-9.]+)/g;
1600
1581
  let match;
1601
1582
  while ((match = factRegex.exec(jsonStr)) !== null) {
1602
- const rawCat = match[1];
1603
- const category = allowedCategories.has(rawCat) ? (rawCat as ExtractionResult["facts"][number]["category"]) : "fact";
1583
+ const category = match[1]?.trim() ?? "";
1584
+ if (!isMemoryCategory(category)) continue;
1604
1585
  facts.push({
1605
1586
  category,
1606
1587
  content: match[2].replace(/\\n/g, '\n').replace(/\\"/g, '"'),
@@ -1612,8 +1593,8 @@ ${truncatedConversation}`;
1612
1593
  // Find all complete entity objects
1613
1594
  const entityRegex = /\{\s*"name"\s*:\s*"([^"]+)"\s*,\s*"type"\s*:\s*"([^"]+)"/g;
1614
1595
  while ((match = entityRegex.exec(jsonStr)) !== null) {
1615
- const rawType = match[2];
1616
- const type = allowedEntityTypes.has(rawType) ? (rawType as ExtractionResult["entities"][number]["type"]) : "other";
1596
+ const type = extractionEntityType(match[2]);
1597
+ if (type === undefined) continue;
1617
1598
  entities.push({
1618
1599
  name: match[1],
1619
1600
  type,
@@ -1624,7 +1605,7 @@ ${truncatedConversation}`;
1624
1605
  // Ignore regex errors
1625
1606
  }
1626
1607
 
1627
- return { facts, entities, profileUpdates: [], questions: [] };
1608
+ return this.normalizeExtractionResultPayload({ facts, entities, profileUpdates: [], questions: [] });
1628
1609
  }
1629
1610
 
1630
1611
  /**
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 = {