@remnic/core 9.3.664 → 9.3.666

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 (150) hide show
  1. package/dist/access-audit.js +2 -2
  2. package/dist/access-cli.js +41 -40
  3. package/dist/access-cli.js.map +1 -1
  4. package/dist/access-http.d.ts +3 -2
  5. package/dist/access-http.js +25 -25
  6. package/dist/access-mcp.d.ts +3 -2
  7. package/dist/access-mcp.js +22 -22
  8. package/dist/access-schema.js +3 -3
  9. package/dist/{access-service-D0SLB4MH.d.ts → access-service-DsS-TatL.d.ts} +1 -1
  10. package/dist/access-service.d.ts +3 -2
  11. package/dist/access-service.js +21 -21
  12. package/dist/adapters/index.js +4 -4
  13. package/dist/adapters/registry.js +2 -2
  14. package/dist/bootstrap.d.ts +2 -1
  15. package/dist/briefing.js +4 -3
  16. package/dist/capabilities.d.ts +73 -0
  17. package/dist/capabilities.js +8 -0
  18. package/dist/capabilities.js.map +1 -0
  19. package/dist/causal-behavior.js +2 -2
  20. package/dist/causal-chain.js +2 -2
  21. package/dist/causal-consolidation.js +7 -6
  22. package/dist/causal-consolidation.js.map +1 -1
  23. package/dist/causal-retrieval.js +2 -2
  24. package/dist/causal-trajectory.js +1 -1
  25. package/dist/{chunk-ROHLEUTH.js → chunk-23EBQ27U.js} +5 -5
  26. package/dist/{chunk-YW52BQSU.js → chunk-2TCHDANJ.js} +2 -2
  27. package/dist/{chunk-IROWLAWG.js → chunk-46WUVFOD.js} +4 -4
  28. package/dist/{chunk-XB5P5P2L.js → chunk-4T7P2HLJ.js} +3 -3
  29. package/dist/{chunk-7XH7VJN4.js → chunk-6T4LTI2F.js} +4 -4
  30. package/dist/{chunk-TVVEYCNW.js → chunk-7K5Q6COX.js} +4 -4
  31. package/dist/{chunk-BZG2CWOQ.js → chunk-A5TEHAR4.js} +3 -3
  32. package/dist/{chunk-C7AF236A.js → chunk-AARDBQTA.js} +2 -2
  33. package/dist/{chunk-IHG6CC7T.js → chunk-BQJUPECT.js} +2 -2
  34. package/dist/{chunk-7OGJQP7T.js → chunk-CRO4LCQ6.js} +5 -5
  35. package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
  36. package/dist/{chunk-LIERUFPO.js → chunk-GZ6QAYSH.js} +94 -74
  37. package/dist/chunk-GZ6QAYSH.js.map +1 -0
  38. package/dist/{chunk-UXA5L2DZ.js → chunk-HQCGRSRU.js} +2 -2
  39. package/dist/{chunk-RKNJBZ55.js → chunk-JBPKEARU.js} +4 -4
  40. package/dist/{chunk-XW3W4PV4.js → chunk-JTPXSXHC.js} +2 -2
  41. package/dist/{chunk-OHJFJ4HI.js → chunk-KOXGLQS7.js} +2 -2
  42. package/dist/{chunk-NLF54XMD.js → chunk-MPXYHC35.js} +26 -26
  43. package/dist/{chunk-6JBKHTQD.js → chunk-MR4PJ277.js} +2 -2
  44. package/dist/{chunk-EXXBA5OM.js → chunk-OI4BXFSB.js} +4 -4
  45. package/dist/{chunk-SQZ42MKH.js → chunk-OQH5XUH3.js} +6 -3
  46. package/dist/chunk-OQH5XUH3.js.map +1 -0
  47. package/dist/{chunk-2HEZXPYU.js → chunk-Q2LQZYQ7.js} +3 -3
  48. package/dist/{chunk-YKX63GBK.js → chunk-QHWJG5C5.js} +8 -8
  49. package/dist/{chunk-T2AN3BSP.js → chunk-QZ7ODIVL.js} +2 -2
  50. package/dist/chunk-RI5XBIZ6.js +23 -0
  51. package/dist/chunk-RI5XBIZ6.js.map +1 -0
  52. package/dist/{chunk-7ILWCUWH.js → chunk-TJ7HH5LB.js} +28 -3
  53. package/dist/chunk-TJ7HH5LB.js.map +1 -0
  54. package/dist/{chunk-V25ZAOSB.js → chunk-UOBLE67F.js} +4 -4
  55. package/dist/{chunk-JIX3ZL2J.js → chunk-UVUTV7CM.js} +15 -15
  56. package/dist/{chunk-VH6EIKVS.js → chunk-WKMCC4NQ.js} +35 -16
  57. package/dist/chunk-WKMCC4NQ.js.map +1 -0
  58. package/dist/{chunk-SSOMTUCA.js → chunk-WXGTC424.js} +1 -1
  59. package/dist/{chunk-KHGE6PMF.js → chunk-WXXLSZHA.js} +2 -2
  60. package/dist/{chunk-DSLUOQDY.js → chunk-XMWF6AU3.js} +2 -2
  61. package/dist/{chunk-DQY7NJ5L.js → chunk-XS2CWEHZ.js} +2 -2
  62. package/dist/{cli-BQRqR9N-.d.ts → cli-BypxcNqq.d.ts} +2 -2
  63. package/dist/cli.d.ts +4 -3
  64. package/dist/cli.js +42 -42
  65. package/dist/compounding/engine.js +4 -3
  66. package/dist/connectors/codex-materialize-runner.js +4 -3
  67. package/dist/connectors/index.js +4 -3
  68. package/dist/consolidation-provenance-check.js +2 -2
  69. package/dist/conversation-index/backend.js +2 -2
  70. package/dist/dashboard-runtime.js +2 -2
  71. package/dist/direct-answer-wiring.d.ts +13 -3
  72. package/dist/direct-answer-wiring.js +1 -1
  73. package/dist/entity-retrieval.js +4 -3
  74. package/dist/explicit-capture.d.ts +2 -1
  75. package/dist/index.d.ts +5 -4
  76. package/dist/index.js +66 -65
  77. package/dist/index.js.map +1 -1
  78. package/dist/lcm/engine.js +2 -2
  79. package/dist/lcm/index.js +4 -4
  80. package/dist/maintenance/memory-governance.js +4 -4
  81. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -3
  82. package/dist/maintenance/rebuild-memory-projection.js +5 -5
  83. package/dist/mcp-memory-inspector-app.d.ts +3 -2
  84. package/dist/namespaces/migrate.js +11 -11
  85. package/dist/namespaces/search.js +7 -7
  86. package/dist/namespaces/storage.d.ts +13 -0
  87. package/dist/namespaces/storage.js +4 -3
  88. package/dist/operator-toolkit.js +15 -15
  89. package/dist/{orchestrator-Cg1UkvmO.d.ts → orchestrator-DZqPVoMI.d.ts} +8 -0
  90. package/dist/orchestrator.d.ts +2 -1
  91. package/dist/orchestrator.js +32 -31
  92. package/dist/recall-planner-llm.d.ts +2 -1
  93. package/dist/recall-planner-llm.js +3 -2
  94. package/dist/recall-planner-llm.js.map +1 -1
  95. package/dist/search/factory.js +6 -6
  96. package/dist/search/index.js +10 -10
  97. package/dist/search/lancedb-backend.js +1 -1
  98. package/dist/search/meilisearch-backend.js +1 -1
  99. package/dist/search/orama-backend.js +1 -1
  100. package/dist/semantic-consolidation.js +5 -4
  101. package/dist/semantic-rule-promotion.js +4 -3
  102. package/dist/semantic-rule-verifier.js +4 -3
  103. package/dist/storage.js +3 -2
  104. package/dist/transfer/backup.js +2 -2
  105. package/dist/transfer/capsule-export.js +2 -2
  106. package/dist/transfer/capsule-import.js +1 -1
  107. package/dist/verified-recall.js +4 -3
  108. package/package.json +1 -1
  109. package/src/capabilities.test.ts +97 -0
  110. package/src/capabilities.ts +86 -0
  111. package/src/direct-answer-wiring.test.ts +53 -2
  112. package/src/direct-answer-wiring.ts +18 -5
  113. package/src/namespaces/catalog.test.ts +12 -12
  114. package/src/namespaces/storage.ts +28 -1
  115. package/src/orchestrator.ts +69 -19
  116. package/src/recall-planner-llm.test.ts +12 -11
  117. package/src/recall-planner-llm.ts +7 -1
  118. package/src/storage-fallback-category-dirs.test.ts +150 -1
  119. package/src/storage.ts +51 -14
  120. package/dist/chunk-7ILWCUWH.js.map +0 -1
  121. package/dist/chunk-LIERUFPO.js.map +0 -1
  122. package/dist/chunk-SQZ42MKH.js.map +0 -1
  123. package/dist/chunk-VH6EIKVS.js.map +0 -1
  124. /package/dist/{chunk-ROHLEUTH.js.map → chunk-23EBQ27U.js.map} +0 -0
  125. /package/dist/{chunk-YW52BQSU.js.map → chunk-2TCHDANJ.js.map} +0 -0
  126. /package/dist/{chunk-IROWLAWG.js.map → chunk-46WUVFOD.js.map} +0 -0
  127. /package/dist/{chunk-XB5P5P2L.js.map → chunk-4T7P2HLJ.js.map} +0 -0
  128. /package/dist/{chunk-7XH7VJN4.js.map → chunk-6T4LTI2F.js.map} +0 -0
  129. /package/dist/{chunk-TVVEYCNW.js.map → chunk-7K5Q6COX.js.map} +0 -0
  130. /package/dist/{chunk-BZG2CWOQ.js.map → chunk-A5TEHAR4.js.map} +0 -0
  131. /package/dist/{chunk-C7AF236A.js.map → chunk-AARDBQTA.js.map} +0 -0
  132. /package/dist/{chunk-IHG6CC7T.js.map → chunk-BQJUPECT.js.map} +0 -0
  133. /package/dist/{chunk-7OGJQP7T.js.map → chunk-CRO4LCQ6.js.map} +0 -0
  134. /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
  135. /package/dist/{chunk-UXA5L2DZ.js.map → chunk-HQCGRSRU.js.map} +0 -0
  136. /package/dist/{chunk-RKNJBZ55.js.map → chunk-JBPKEARU.js.map} +0 -0
  137. /package/dist/{chunk-XW3W4PV4.js.map → chunk-JTPXSXHC.js.map} +0 -0
  138. /package/dist/{chunk-OHJFJ4HI.js.map → chunk-KOXGLQS7.js.map} +0 -0
  139. /package/dist/{chunk-NLF54XMD.js.map → chunk-MPXYHC35.js.map} +0 -0
  140. /package/dist/{chunk-6JBKHTQD.js.map → chunk-MR4PJ277.js.map} +0 -0
  141. /package/dist/{chunk-EXXBA5OM.js.map → chunk-OI4BXFSB.js.map} +0 -0
  142. /package/dist/{chunk-2HEZXPYU.js.map → chunk-Q2LQZYQ7.js.map} +0 -0
  143. /package/dist/{chunk-YKX63GBK.js.map → chunk-QHWJG5C5.js.map} +0 -0
  144. /package/dist/{chunk-T2AN3BSP.js.map → chunk-QZ7ODIVL.js.map} +0 -0
  145. /package/dist/{chunk-V25ZAOSB.js.map → chunk-UOBLE67F.js.map} +0 -0
  146. /package/dist/{chunk-JIX3ZL2J.js.map → chunk-UVUTV7CM.js.map} +0 -0
  147. /package/dist/{chunk-SSOMTUCA.js.map → chunk-WXGTC424.js.map} +0 -0
  148. /package/dist/{chunk-KHGE6PMF.js.map → chunk-WXXLSZHA.js.map} +0 -0
  149. /package/dist/{chunk-DSLUOQDY.js.map → chunk-XMWF6AU3.js.map} +0 -0
  150. /package/dist/{chunk-DQY7NJ5L.js.map → chunk-XS2CWEHZ.js.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/recall-planner-llm.ts"],"sourcesContent":["import { z } from \"zod\";\n\nimport type { PluginConfig, RecallPlanMode } from \"./types.js\";\nimport { planRecallMode } from \"./intent.js\";\nimport {\n FallbackLlmClient,\n fallbackLlmRuntimeContextFromConfig,\n gatewayTaskChainOptions,\n type FallbackLlmOptions,\n} from \"./fallback-llm.js\";\nimport { log } from \"./logger.js\";\n\n/**\n * LLM-based recall planning (issue #1367, Option C).\n *\n * Classifies an incoming prompt into a {@link RecallPlanMode} using an LLM\n * instead of (or alongside) the regex heuristic in {@link planRecallMode}.\n *\n * Provider-agnostic by construction: it routes through {@link FallbackLlmClient},\n * which resolves the model chain from gateway providers (OpenAI, Anthropic,\n * Ollama, Codex, …) or gateway agent personas / `taskModelChain`. Nothing here\n * is hard-coded to a single provider or to OpenAI's Responses API — the API\n * dialect is chosen per-provider by the client based on each provider's `api`\n * field. The configured `recallPlannerModel` is tried first, with the broader\n * task chain (and the gateway default) as resilient fallbacks.\n *\n * Invariants:\n * - Never throws to the caller (gotcha #13). Any LLM failure, timeout, empty\n * response, or unavailable backend falls back to the heuristic result and\n * sets `fallbackUsed: true` (gotcha #34 — failures are distinct from a valid\n * classification).\n * - When `recallPlannerLlmEnabled` is false the LLM is never contacted.\n */\n\nexport type RecallPlannerSource = \"llm\" | \"heuristic\" | \"heuristic-fallback\";\n\nexport interface RecallPlannerLlmResult {\n /** The mode to act on. */\n mode: RecallPlanMode;\n /** The heuristic mode, always computed (the fallback floor / shadow baseline). */\n heuristicMode: RecallPlanMode;\n /** Where `mode` came from. */\n source: RecallPlannerSource;\n /** Short human-readable rationale (LLM reason, or why we fell back). */\n reason: string;\n /** Model that actually served the classification, when an LLM was used. */\n modelUsed?: string;\n /** Wall-clock spent in the LLM call (0 when no call was made). */\n latencyMs: number;\n /** True when the LLM was enabled but we had to fall back to the heuristic. */\n fallbackUsed: boolean;\n}\n\nconst PLANNER_SCHEMA = z.object({\n // gotcha #2: optional fields use .optional().nullable()\n mode: z.enum([\"no_recall\", \"minimal\", \"full\", \"graph_mode\"]),\n reason: z.string().max(280).optional().nullable(),\n});\n\nconst SYSTEM_PROMPT = [\n \"You are a recall-planning classifier for a long-term memory system.\",\n \"Given the user's latest message, decide how much stored memory should be retrieved before the assistant responds.\",\n \"Reply with a single JSON object: {\\\"mode\\\": <one of no_recall|minimal|full|graph_mode>, \\\"reason\\\": <short string>}.\",\n \"\",\n \"Modes:\",\n '- \"no_recall\": low-information acknowledgements or chit-chat with nothing to look up (e.g. \"ok\", \"thanks\", \"sounds good\"). Retrieve nothing.',\n '- \"minimal\": short, self-contained operational directives that rarely need history (e.g. \"restart the service\", \"run the tests\", \"show status\"). Retrieve a little.',\n '- \"full\": anything memory-seeking, analytical, or a question that benefits from prior context, decisions, or facts. This is the safe default when unsure.',\n '- \"graph_mode\": queries about timelines, sequences, history, causal chains, or root cause (\"how did we get here\", \"what led to this regression\"). Retrieve relationship/graph context.',\n \"\",\n \"When uncertain, prefer \\\"full\\\" over dropping recall. Never invent facts; only classify intent.\",\n].join(\"\\n\");\n\n/** Clamp a planner prompt to the configured character budget. */\nfunction clampPrompt(prompt: string, maxChars: number): string {\n const safeMax = Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) : 4000;\n if (prompt.length <= safeMax) return prompt;\n return prompt.slice(0, safeMax);\n}\n\n/** Trim and cap the optional memory hints used to ground the classification. */\nfunction clampHints(hints: string[] | undefined, maxHints: number): string[] {\n if (!Array.isArray(hints) || hints.length === 0) return [];\n const safeMax = Number.isFinite(maxHints) && maxHints > 0 ? Math.floor(maxHints) : 0;\n if (safeMax <= 0) return [];\n const cleaned: string[] = [];\n for (const hint of hints) {\n if (typeof hint !== \"string\") continue;\n const trimmed = hint.trim();\n if (trimmed.length === 0) continue;\n cleaned.push(trimmed);\n if (cleaned.length >= safeMax) break;\n }\n return cleaned;\n}\n\nfunction buildMessages(\n prompt: string,\n hints: string[],\n config: PluginConfig,\n): Array<{ role: \"system\" | \"user\" | \"assistant\"; content: string }> {\n const clampedPrompt = clampPrompt(prompt, config.recallPlannerMaxPromptChars);\n const userParts = [`User message:\\n${clampedPrompt}`];\n if (hints.length > 0) {\n userParts.push(\n `\\nRecent memory topics (for grounding only, do not treat as the message):\\n- ${hints.join(\"\\n- \")}`,\n );\n }\n userParts.push('\\nRespond with JSON only: {\"mode\": \"...\", \"reason\": \"...\"}.');\n return [\n { role: \"system\", content: SYSTEM_PROMPT },\n { role: \"user\", content: userParts.join(\"\\n\") },\n ];\n}\n\n/**\n * Resolve the FallbackLlmClient routing options for the recall planner.\n *\n * - The dedicated `recallPlannerModel` is tried first (as the `model`\n * override — it is prepended to the chain by FallbackLlmClient). If it does\n * not resolve to a configured provider it is silently skipped, so a stale\n * default never breaks routing.\n * - In gateway mode the shared `gatewayTaskChainOptions` (taskModelChain >\n * gatewayAgentId, gotcha #22) is layered in as the fallback chain, plus the\n * implicit gateway default appended by the client.\n * - In plugin mode only the explicit model + gateway providers apply.\n */\n/**\n * A `recallPlannerModel` value is only usable as a FallbackLlmClient `model`\n * override when it is provider-qualified (`provider/model`). The client's\n * `parseModelString` rejects bare names, so forwarding a bare value (e.g. the\n * legacy default `\"gpt-5.5\"`) would log \"invalid model format\" on every call\n * and never resolve. Bare values are dropped so routing falls through to the\n * gateway chain / agent / default instead (issue #1367 review on PR #1428).\n */\nfunction qualifiedPlannerModel(recallPlannerModel: string | undefined): string | undefined {\n if (typeof recallPlannerModel !== \"string\") return undefined;\n const trimmed = recallPlannerModel.trim();\n return trimmed.includes(\"/\") ? trimmed : undefined;\n}\n\nexport function resolveRecallPlannerLlmOptions(\n config: Pick<\n PluginConfig,\n \"modelSource\" | \"taskModelChain\" | \"gatewayAgentId\" | \"recallPlannerModel\" | \"recallPlannerTimeoutMs\"\n >,\n): FallbackLlmOptions {\n const chainOptions =\n config.modelSource === \"gateway\" ? gatewayTaskChainOptions(config) : {};\n return {\n ...chainOptions,\n model: qualifiedPlannerModel(config.recallPlannerModel),\n temperature: 0,\n maxTokens: 64,\n timeoutMs:\n typeof config.recallPlannerTimeoutMs === \"number\" && config.recallPlannerTimeoutMs > 0\n ? config.recallPlannerTimeoutMs\n : 1500,\n };\n}\n\n// One-time warning per distinct routing signature so an opted-in operator with\n// no usable model learns why planning silently uses the heuristic, without\n// spamming a line on every recall.\nconst warnedNoRoutingSignatures = new Set<string>();\n\nfunction heuristicResult(\n heuristicMode: RecallPlanMode,\n source: RecallPlannerSource,\n reason: string,\n latencyMs: number,\n fallbackUsed: boolean,\n): RecallPlannerLlmResult {\n return { mode: heuristicMode, heuristicMode, source, reason, latencyMs, fallbackUsed };\n}\n\n/**\n * Plan the recall mode for `prompt`, optionally consulting an LLM.\n *\n * Always safe to call: returns the heuristic result when the LLM is disabled,\n * unavailable, or fails.\n *\n * @param llm injectable client (tests pass a stub); constructed from gateway\n * config when omitted.\n */\nexport async function planRecallModeLLM(\n prompt: string,\n hints: string[] | undefined,\n config: PluginConfig,\n llm?: FallbackLlmClient,\n signal?: AbortSignal,\n): Promise<RecallPlannerLlmResult> {\n const heuristicMode = planRecallMode(prompt);\n\n if (!config.recallPlannerLlmEnabled) {\n return heuristicResult(heuristicMode, \"heuristic\", \"llm-disabled\", 0, false);\n }\n\n // Participate in the recall cancellation contract: if the outer recall is\n // already aborted (outer timeout / reset / session abort), don't start an LLM\n // round-trip — fall back to the heuristic immediately (#1428 review).\n if (signal?.aborted) {\n return heuristicResult(heuristicMode, \"heuristic-fallback\", \"aborted\", 0, true);\n }\n\n const safePrompt = typeof prompt === \"string\" ? prompt.trim() : \"\";\n if (safePrompt.length === 0) {\n // Empty prompts never need an LLM round-trip.\n return heuristicResult(heuristicMode, \"heuristic\", \"empty-prompt\", 0, false);\n }\n\n const client =\n llm ??\n new FallbackLlmClient(\n config.gatewayConfig,\n fallbackLlmRuntimeContextFromConfig(config),\n );\n\n // Forward the recall abort signal so an aborted/timed-out outer recall can\n // cancel an in-flight planner call (FallbackLlmClient honors `signal`).\n const options = { ...resolveRecallPlannerLlmOptions(config), signal };\n\n // Availability check uses the same routing options so plugin-mode / empty\n // chains short-circuit to the heuristic without a network attempt. `model`\n // here is already provider-qualified (bare names were dropped), so a present\n // model means the override is genuinely routable.\n const availabilityProbe = {\n agentId: options.agentId,\n modelChain: options.modelChain,\n };\n if (!client.isAvailable(availabilityProbe) && !options.model) {\n // Opted-in but nothing routable resolves (e.g. plugin mode with the bare\n // default `recallPlannerModel` and no gateway chain). Warn once so it's not\n // a silent no-op, then fall back to the heuristic.\n const signature = `${config.modelSource}:${config.recallPlannerModel ?? \"\"}`;\n if (!warnedNoRoutingSignatures.has(signature)) {\n warnedNoRoutingSignatures.add(signature);\n log.warn(\n \"[recall-planner] recallPlannerLlmEnabled is on but no routable model resolves — \" +\n \"set recallPlannerModel to a 'provider/model' value or configure a gateway model chain. \" +\n \"Falling back to the heuristic planner.\",\n );\n }\n return heuristicResult(heuristicMode, \"heuristic-fallback\", \"llm-no-model\", 0, true);\n }\n\n const clampedHints = clampHints(hints, config.recallPlannerMaxMemoryHints);\n const messages = buildMessages(safePrompt, clampedHints, config);\n\n const start = Date.now();\n try {\n const detailed = await client.parseWithSchemaDetailed(messages, PLANNER_SCHEMA, options);\n const latencyMs = Date.now() - start;\n if (!detailed?.result) {\n // Distinguish failure from a valid empty (gotcha #34): a null here means\n // no parseable classification, so fall back to the heuristic.\n return heuristicResult(heuristicMode, \"heuristic-fallback\", \"llm-empty\", latencyMs, true);\n }\n const mode = detailed.result.mode;\n const reason =\n typeof detailed.result.reason === \"string\" && detailed.result.reason.trim().length > 0\n ? detailed.result.reason.trim()\n : \"llm-classified\";\n return {\n mode,\n heuristicMode,\n source: \"llm\",\n reason,\n modelUsed: detailed.modelUsed,\n latencyMs,\n fallbackUsed: false,\n };\n } catch (err) {\n const latencyMs = Date.now() - start;\n if (signal?.aborted) {\n // Cancelled by the outer recall — expected, not an error worth warning on.\n return heuristicResult(heuristicMode, \"heuristic-fallback\", \"aborted\", latencyMs, true);\n }\n const message = err instanceof Error ? err.message : String(err);\n log.warn(`[recall-planner] LLM failed, falling back to heuristic: ${message}`);\n return heuristicResult(\n heuristicMode,\n \"heuristic-fallback\",\n `llm-error:${message}`,\n latencyMs,\n true,\n );\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/recall-planner-llm.ts"],"sourcesContent":["import { z } from \"zod\";\n\nimport type { PluginConfig, RecallPlanMode } from \"./types.js\";\nimport type { CapabilitySet } from \"./capabilities.js\";\nimport { planRecallMode } from \"./intent.js\";\nimport {\n FallbackLlmClient,\n fallbackLlmRuntimeContextFromConfig,\n gatewayTaskChainOptions,\n type FallbackLlmOptions,\n} from \"./fallback-llm.js\";\nimport { log } from \"./logger.js\";\n\n/**\n * LLM-based recall planning (issue #1367, Option C).\n *\n * Classifies an incoming prompt into a {@link RecallPlanMode} using an LLM\n * instead of (or alongside) the regex heuristic in {@link planRecallMode}.\n *\n * Provider-agnostic by construction: it routes through {@link FallbackLlmClient},\n * which resolves the model chain from gateway providers (OpenAI, Anthropic,\n * Ollama, Codex, …) or gateway agent personas / `taskModelChain`. Nothing here\n * is hard-coded to a single provider or to OpenAI's Responses API — the API\n * dialect is chosen per-provider by the client based on each provider's `api`\n * field. The configured `recallPlannerModel` is tried first, with the broader\n * task chain (and the gateway default) as resilient fallbacks.\n *\n * Invariants:\n * - Never throws to the caller (gotcha #13). Any LLM failure, timeout, empty\n * response, or unavailable backend falls back to the heuristic result and\n * sets `fallbackUsed: true` (gotcha #34 — failures are distinct from a valid\n * classification).\n * - When `recallPlannerLlmEnabled` is false the LLM is never contacted.\n */\n\nexport type RecallPlannerSource = \"llm\" | \"heuristic\" | \"heuristic-fallback\";\n\nexport interface RecallPlannerLlmResult {\n /** The mode to act on. */\n mode: RecallPlanMode;\n /** The heuristic mode, always computed (the fallback floor / shadow baseline). */\n heuristicMode: RecallPlanMode;\n /** Where `mode` came from. */\n source: RecallPlannerSource;\n /** Short human-readable rationale (LLM reason, or why we fell back). */\n reason: string;\n /** Model that actually served the classification, when an LLM was used. */\n modelUsed?: string;\n /** Wall-clock spent in the LLM call (0 when no call was made). */\n latencyMs: number;\n /** True when the LLM was enabled but we had to fall back to the heuristic. */\n fallbackUsed: boolean;\n}\n\nconst PLANNER_SCHEMA = z.object({\n // gotcha #2: optional fields use .optional().nullable()\n mode: z.enum([\"no_recall\", \"minimal\", \"full\", \"graph_mode\"]),\n reason: z.string().max(280).optional().nullable(),\n});\n\nconst SYSTEM_PROMPT = [\n \"You are a recall-planning classifier for a long-term memory system.\",\n \"Given the user's latest message, decide how much stored memory should be retrieved before the assistant responds.\",\n \"Reply with a single JSON object: {\\\"mode\\\": <one of no_recall|minimal|full|graph_mode>, \\\"reason\\\": <short string>}.\",\n \"\",\n \"Modes:\",\n '- \"no_recall\": low-information acknowledgements or chit-chat with nothing to look up (e.g. \"ok\", \"thanks\", \"sounds good\"). Retrieve nothing.',\n '- \"minimal\": short, self-contained operational directives that rarely need history (e.g. \"restart the service\", \"run the tests\", \"show status\"). Retrieve a little.',\n '- \"full\": anything memory-seeking, analytical, or a question that benefits from prior context, decisions, or facts. This is the safe default when unsure.',\n '- \"graph_mode\": queries about timelines, sequences, history, causal chains, or root cause (\"how did we get here\", \"what led to this regression\"). Retrieve relationship/graph context.',\n \"\",\n \"When uncertain, prefer \\\"full\\\" over dropping recall. Never invent facts; only classify intent.\",\n].join(\"\\n\");\n\n/** Clamp a planner prompt to the configured character budget. */\nfunction clampPrompt(prompt: string, maxChars: number): string {\n const safeMax = Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) : 4000;\n if (prompt.length <= safeMax) return prompt;\n return prompt.slice(0, safeMax);\n}\n\n/** Trim and cap the optional memory hints used to ground the classification. */\nfunction clampHints(hints: string[] | undefined, maxHints: number): string[] {\n if (!Array.isArray(hints) || hints.length === 0) return [];\n const safeMax = Number.isFinite(maxHints) && maxHints > 0 ? Math.floor(maxHints) : 0;\n if (safeMax <= 0) return [];\n const cleaned: string[] = [];\n for (const hint of hints) {\n if (typeof hint !== \"string\") continue;\n const trimmed = hint.trim();\n if (trimmed.length === 0) continue;\n cleaned.push(trimmed);\n if (cleaned.length >= safeMax) break;\n }\n return cleaned;\n}\n\nfunction buildMessages(\n prompt: string,\n hints: string[],\n config: PluginConfig,\n): Array<{ role: \"system\" | \"user\" | \"assistant\"; content: string }> {\n const clampedPrompt = clampPrompt(prompt, config.recallPlannerMaxPromptChars);\n const userParts = [`User message:\\n${clampedPrompt}`];\n if (hints.length > 0) {\n userParts.push(\n `\\nRecent memory topics (for grounding only, do not treat as the message):\\n- ${hints.join(\"\\n- \")}`,\n );\n }\n userParts.push('\\nRespond with JSON only: {\"mode\": \"...\", \"reason\": \"...\"}.');\n return [\n { role: \"system\", content: SYSTEM_PROMPT },\n { role: \"user\", content: userParts.join(\"\\n\") },\n ];\n}\n\n/**\n * Resolve the FallbackLlmClient routing options for the recall planner.\n *\n * - The dedicated `recallPlannerModel` is tried first (as the `model`\n * override — it is prepended to the chain by FallbackLlmClient). If it does\n * not resolve to a configured provider it is silently skipped, so a stale\n * default never breaks routing.\n * - In gateway mode the shared `gatewayTaskChainOptions` (taskModelChain >\n * gatewayAgentId, gotcha #22) is layered in as the fallback chain, plus the\n * implicit gateway default appended by the client.\n * - In plugin mode only the explicit model + gateway providers apply.\n */\n/**\n * A `recallPlannerModel` value is only usable as a FallbackLlmClient `model`\n * override when it is provider-qualified (`provider/model`). The client's\n * `parseModelString` rejects bare names, so forwarding a bare value (e.g. the\n * legacy default `\"gpt-5.5\"`) would log \"invalid model format\" on every call\n * and never resolve. Bare values are dropped so routing falls through to the\n * gateway chain / agent / default instead (issue #1367 review on PR #1428).\n */\nfunction qualifiedPlannerModel(recallPlannerModel: string | undefined): string | undefined {\n if (typeof recallPlannerModel !== \"string\") return undefined;\n const trimmed = recallPlannerModel.trim();\n return trimmed.includes(\"/\") ? trimmed : undefined;\n}\n\nexport function resolveRecallPlannerLlmOptions(\n config: Pick<\n PluginConfig,\n \"modelSource\" | \"taskModelChain\" | \"gatewayAgentId\" | \"recallPlannerModel\" | \"recallPlannerTimeoutMs\"\n >,\n): FallbackLlmOptions {\n const chainOptions =\n config.modelSource === \"gateway\" ? gatewayTaskChainOptions(config) : {};\n return {\n ...chainOptions,\n model: qualifiedPlannerModel(config.recallPlannerModel),\n temperature: 0,\n maxTokens: 64,\n timeoutMs:\n typeof config.recallPlannerTimeoutMs === \"number\" && config.recallPlannerTimeoutMs > 0\n ? config.recallPlannerTimeoutMs\n : 1500,\n };\n}\n\n// One-time warning per distinct routing signature so an opted-in operator with\n// no usable model learns why planning silently uses the heuristic, without\n// spamming a line on every recall.\nconst warnedNoRoutingSignatures = new Set<string>();\n\nfunction heuristicResult(\n heuristicMode: RecallPlanMode,\n source: RecallPlannerSource,\n reason: string,\n latencyMs: number,\n fallbackUsed: boolean,\n): RecallPlannerLlmResult {\n return { mode: heuristicMode, heuristicMode, source, reason, latencyMs, fallbackUsed };\n}\n\n/**\n * Plan the recall mode for `prompt`, optionally consulting an LLM.\n *\n * Always safe to call: returns the heuristic result when the LLM is disabled,\n * unavailable, or fails.\n *\n * @param llm injectable client (tests pass a stub); constructed from gateway\n * config when omitted.\n */\nexport async function planRecallModeLLM(\n prompt: string,\n hints: string[] | undefined,\n config: PluginConfig,\n llm?: FallbackLlmClient,\n signal?: AbortSignal,\n caps?: CapabilitySet,\n): Promise<RecallPlannerLlmResult> {\n const heuristicMode = planRecallMode(prompt);\n\n // `caps` is OPTIONAL and additive (issue #1523). Prefer the resolved\n // capability when supplied; fall back to the config flag so existing callers\n // that pass only `config` keep identical gating.\n const plannerLlmEnabled = caps?.recallPlannerLlm ?? config.recallPlannerLlmEnabled;\n if (!plannerLlmEnabled) {\n return heuristicResult(heuristicMode, \"heuristic\", \"llm-disabled\", 0, false);\n }\n\n // Participate in the recall cancellation contract: if the outer recall is\n // already aborted (outer timeout / reset / session abort), don't start an LLM\n // round-trip — fall back to the heuristic immediately (#1428 review).\n if (signal?.aborted) {\n return heuristicResult(heuristicMode, \"heuristic-fallback\", \"aborted\", 0, true);\n }\n\n const safePrompt = typeof prompt === \"string\" ? prompt.trim() : \"\";\n if (safePrompt.length === 0) {\n // Empty prompts never need an LLM round-trip.\n return heuristicResult(heuristicMode, \"heuristic\", \"empty-prompt\", 0, false);\n }\n\n const client =\n llm ??\n new FallbackLlmClient(\n config.gatewayConfig,\n fallbackLlmRuntimeContextFromConfig(config),\n );\n\n // Forward the recall abort signal so an aborted/timed-out outer recall can\n // cancel an in-flight planner call (FallbackLlmClient honors `signal`).\n const options = { ...resolveRecallPlannerLlmOptions(config), signal };\n\n // Availability check uses the same routing options so plugin-mode / empty\n // chains short-circuit to the heuristic without a network attempt. `model`\n // here is already provider-qualified (bare names were dropped), so a present\n // model means the override is genuinely routable.\n const availabilityProbe = {\n agentId: options.agentId,\n modelChain: options.modelChain,\n };\n if (!client.isAvailable(availabilityProbe) && !options.model) {\n // Opted-in but nothing routable resolves (e.g. plugin mode with the bare\n // default `recallPlannerModel` and no gateway chain). Warn once so it's not\n // a silent no-op, then fall back to the heuristic.\n const signature = `${config.modelSource}:${config.recallPlannerModel ?? \"\"}`;\n if (!warnedNoRoutingSignatures.has(signature)) {\n warnedNoRoutingSignatures.add(signature);\n log.warn(\n \"[recall-planner] recallPlannerLlmEnabled is on but no routable model resolves — \" +\n \"set recallPlannerModel to a 'provider/model' value or configure a gateway model chain. \" +\n \"Falling back to the heuristic planner.\",\n );\n }\n return heuristicResult(heuristicMode, \"heuristic-fallback\", \"llm-no-model\", 0, true);\n }\n\n const clampedHints = clampHints(hints, config.recallPlannerMaxMemoryHints);\n const messages = buildMessages(safePrompt, clampedHints, config);\n\n const start = Date.now();\n try {\n const detailed = await client.parseWithSchemaDetailed(messages, PLANNER_SCHEMA, options);\n const latencyMs = Date.now() - start;\n if (!detailed?.result) {\n // Distinguish failure from a valid empty (gotcha #34): a null here means\n // no parseable classification, so fall back to the heuristic.\n return heuristicResult(heuristicMode, \"heuristic-fallback\", \"llm-empty\", latencyMs, true);\n }\n const mode = detailed.result.mode;\n const reason =\n typeof detailed.result.reason === \"string\" && detailed.result.reason.trim().length > 0\n ? detailed.result.reason.trim()\n : \"llm-classified\";\n return {\n mode,\n heuristicMode,\n source: \"llm\",\n reason,\n modelUsed: detailed.modelUsed,\n latencyMs,\n fallbackUsed: false,\n };\n } catch (err) {\n const latencyMs = Date.now() - start;\n if (signal?.aborted) {\n // Cancelled by the outer recall — expected, not an error worth warning on.\n return heuristicResult(heuristicMode, \"heuristic-fallback\", \"aborted\", latencyMs, true);\n }\n const message = err instanceof Error ? err.message : String(err);\n log.warn(`[recall-planner] LLM failed, falling back to heuristic: ${message}`);\n return heuristicResult(\n heuristicMode,\n \"heuristic-fallback\",\n `llm-error:${message}`,\n latencyMs,\n true,\n );\n }\n}\n"],"mappings":"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","names":[]}
@@ -2,24 +2,24 @@ import {
2
2
  createConversationIndexRuntime,
3
3
  createConversationSearchBackend,
4
4
  createSearchBackend
5
- } from "../chunk-ROHLEUTH.js";
5
+ } from "../chunk-23EBQ27U.js";
6
+ import "../chunk-OUWAQVDJ.js";
6
7
  import "../chunk-DOCTITOP.js";
7
8
  import "../chunk-CYEPCZN5.js";
8
9
  import "../chunk-Q5ZU3RNY.js";
9
10
  import "../chunk-JOASJWQR.js";
10
- import "../chunk-RN7MUWON.js";
11
- import "../chunk-OUWAQVDJ.js";
12
11
  import "../chunk-AER6MT24.js";
12
+ import "../chunk-RN7MUWON.js";
13
13
  import "../chunk-CINZGPSJ.js";
14
- import "../chunk-5GPPACXK.js";
15
- import "../chunk-7OGJQP7T.js";
14
+ import "../chunk-CRO4LCQ6.js";
15
+ import "../chunk-7DTASS5T.js";
16
16
  import "../chunk-E6ZDCOHM.js";
17
17
  import "../chunk-OIF36KGD.js";
18
- import "../chunk-7DTASS5T.js";
19
18
  import "../chunk-6RHNCKHG.js";
20
19
  import "../chunk-YNQ6DFSV.js";
21
20
  import "../chunk-O75CRYGF.js";
22
21
  import "../chunk-3ONXXHQO.js";
22
+ import "../chunk-5GPPACXK.js";
23
23
  import "../chunk-AWJ2FHCF.js";
24
24
  import "../chunk-42JKGUFJ.js";
25
25
  import "../chunk-JUC24CTX.js";
@@ -2,7 +2,10 @@ import {
2
2
  createConversationIndexRuntime,
3
3
  createConversationSearchBackend,
4
4
  createSearchBackend
5
- } from "../chunk-ROHLEUTH.js";
5
+ } from "../chunk-23EBQ27U.js";
6
+ import {
7
+ LanceDbBackend
8
+ } from "../chunk-OUWAQVDJ.js";
6
9
  import {
7
10
  MeilisearchBackend
8
11
  } from "../chunk-DOCTITOP.js";
@@ -15,25 +18,22 @@ import {
15
18
  import {
16
19
  RemoteSearchBackend
17
20
  } from "../chunk-JOASJWQR.js";
18
- import {
19
- EmbedHelper
20
- } from "../chunk-RN7MUWON.js";
21
- import {
22
- LanceDbBackend
23
- } from "../chunk-OUWAQVDJ.js";
24
21
  import {
25
22
  scanMemoryDir
26
23
  } from "../chunk-AER6MT24.js";
24
+ import {
25
+ EmbedHelper
26
+ } from "../chunk-RN7MUWON.js";
27
27
  import "../chunk-CINZGPSJ.js";
28
- import "../chunk-5GPPACXK.js";
29
- import "../chunk-7OGJQP7T.js";
28
+ import "../chunk-CRO4LCQ6.js";
29
+ import "../chunk-7DTASS5T.js";
30
30
  import "../chunk-E6ZDCOHM.js";
31
31
  import "../chunk-OIF36KGD.js";
32
- import "../chunk-7DTASS5T.js";
33
32
  import "../chunk-6RHNCKHG.js";
34
33
  import "../chunk-YNQ6DFSV.js";
35
34
  import "../chunk-O75CRYGF.js";
36
35
  import "../chunk-3ONXXHQO.js";
36
+ import "../chunk-5GPPACXK.js";
37
37
  import "../chunk-AWJ2FHCF.js";
38
38
  import "../chunk-42JKGUFJ.js";
39
39
  import "../chunk-JUC24CTX.js";
@@ -3,8 +3,8 @@ import {
3
3
  } from "../chunk-OUWAQVDJ.js";
4
4
  import "../chunk-AER6MT24.js";
5
5
  import "../chunk-CINZGPSJ.js";
6
- import "../chunk-5GPPACXK.js";
7
6
  import "../chunk-YNQ6DFSV.js";
7
+ import "../chunk-5GPPACXK.js";
8
8
  import "../chunk-2ODBA7MQ.js";
9
9
  import "../chunk-PVGDJXVK.js";
10
10
  import "../chunk-VS2IYZRU.js";
@@ -3,8 +3,8 @@ import {
3
3
  } from "../chunk-DOCTITOP.js";
4
4
  import "../chunk-AER6MT24.js";
5
5
  import "../chunk-CINZGPSJ.js";
6
- import "../chunk-5GPPACXK.js";
7
6
  import "../chunk-YNQ6DFSV.js";
7
+ import "../chunk-5GPPACXK.js";
8
8
  import "../chunk-2ODBA7MQ.js";
9
9
  import "../chunk-PVGDJXVK.js";
10
10
  import "../chunk-VS2IYZRU.js";
@@ -4,8 +4,8 @@ import {
4
4
  } from "../chunk-Q5ZU3RNY.js";
5
5
  import "../chunk-AER6MT24.js";
6
6
  import "../chunk-CINZGPSJ.js";
7
- import "../chunk-5GPPACXK.js";
8
7
  import "../chunk-YNQ6DFSV.js";
8
+ import "../chunk-5GPPACXK.js";
9
9
  import "../chunk-2ODBA7MQ.js";
10
10
  import "../chunk-PVGDJXVK.js";
11
11
  import "../chunk-VS2IYZRU.js";
@@ -7,16 +7,17 @@ import {
7
7
  materializeAfterSemanticConsolidation,
8
8
  parseConsolidationResponse,
9
9
  parseOperatorAwareConsolidationResponse
10
- } from "./chunk-XW3W4PV4.js";
11
- import "./chunk-C7AF236A.js";
10
+ } from "./chunk-JTPXSXHC.js";
11
+ import "./chunk-AARDBQTA.js";
12
12
  import "./chunk-LN4YGHTM.js";
13
13
  import {
14
14
  resolveExtensionsRoot
15
15
  } from "./chunk-JLNBQWZ2.js";
16
16
  import "./chunk-3UXOZBHV.js";
17
- import "./chunk-VH6EIKVS.js";
17
+ import "./chunk-WKMCC4NQ.js";
18
18
  import "./chunk-M7XQSUBB.js";
19
19
  import "./chunk-5UZXUTVO.js";
20
+ import "./chunk-5GPPACXK.js";
20
21
  import "./chunk-J6A3CX5N.js";
21
22
  import "./chunk-AZBV4RRY.js";
22
23
  import "./chunk-AWJ2FHCF.js";
@@ -28,13 +29,13 @@ import "./chunk-6KYMPV2O.js";
28
29
  import "./chunk-DM2T26WE.js";
29
30
  import "./chunk-LDXUBPMO.js";
30
31
  import "./chunk-FVQJYWH7.js";
32
+ import "./chunk-VF4XKTX3.js";
31
33
  import {
32
34
  CONSOLIDATION_OPERATORS,
33
35
  isConsolidationOperator,
34
36
  isSemanticConsolidationLlmOperator,
35
37
  isValidDerivedFromEntry
36
38
  } from "./chunk-G7D6GZ5J.js";
37
- import "./chunk-VF4XKTX3.js";
38
39
  import "./chunk-U3PN77QT.js";
39
40
  import "./chunk-4DJQYKMN.js";
40
41
  import "./chunk-EYIEWJNI.js";
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  promoteSemanticRuleFromMemory,
3
3
  setSemanticRulePromotionTestHooks
4
- } from "./chunk-IHG6CC7T.js";
5
- import "./chunk-VH6EIKVS.js";
4
+ } from "./chunk-BQJUPECT.js";
5
+ import "./chunk-WKMCC4NQ.js";
6
6
  import "./chunk-M7XQSUBB.js";
7
7
  import "./chunk-5UZXUTVO.js";
8
+ import "./chunk-5GPPACXK.js";
8
9
  import "./chunk-J6A3CX5N.js";
9
10
  import "./chunk-AZBV4RRY.js";
10
11
  import "./chunk-AWJ2FHCF.js";
@@ -16,8 +17,8 @@ import "./chunk-6KYMPV2O.js";
16
17
  import "./chunk-DM2T26WE.js";
17
18
  import "./chunk-LDXUBPMO.js";
18
19
  import "./chunk-FVQJYWH7.js";
19
- import "./chunk-G7D6GZ5J.js";
20
20
  import "./chunk-VF4XKTX3.js";
21
+ import "./chunk-G7D6GZ5J.js";
21
22
  import "./chunk-4DJQYKMN.js";
22
23
  import "./chunk-2ODBA7MQ.js";
23
24
  import "./chunk-J2HSAU72.js";
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  compareVerifiedSemanticRuleResults,
3
3
  searchVerifiedSemanticRules
4
- } from "./chunk-DSLUOQDY.js";
5
- import "./chunk-VH6EIKVS.js";
4
+ } from "./chunk-XMWF6AU3.js";
5
+ import "./chunk-WKMCC4NQ.js";
6
6
  import "./chunk-M7XQSUBB.js";
7
7
  import "./chunk-5UZXUTVO.js";
8
+ import "./chunk-5GPPACXK.js";
8
9
  import "./chunk-J6A3CX5N.js";
9
10
  import "./chunk-AZBV4RRY.js";
10
11
  import "./chunk-AWJ2FHCF.js";
@@ -16,8 +17,8 @@ import "./chunk-6KYMPV2O.js";
16
17
  import "./chunk-DM2T26WE.js";
17
18
  import "./chunk-LDXUBPMO.js";
18
19
  import "./chunk-FVQJYWH7.js";
19
- import "./chunk-G7D6GZ5J.js";
20
20
  import "./chunk-VF4XKTX3.js";
21
+ import "./chunk-G7D6GZ5J.js";
21
22
  import "./chunk-4DJQYKMN.js";
22
23
  import "./chunk-ZBJMUXZH.js";
23
24
  import "./chunk-2ODBA7MQ.js";
package/dist/storage.js CHANGED
@@ -9,9 +9,10 @@ import {
9
9
  parseEntityFile,
10
10
  serializeEntityFile,
11
11
  stripAttributesSuffix
12
- } from "./chunk-VH6EIKVS.js";
12
+ } from "./chunk-WKMCC4NQ.js";
13
13
  import "./chunk-M7XQSUBB.js";
14
14
  import "./chunk-5UZXUTVO.js";
15
+ import "./chunk-5GPPACXK.js";
15
16
  import "./chunk-J6A3CX5N.js";
16
17
  import "./chunk-AZBV4RRY.js";
17
18
  import "./chunk-AWJ2FHCF.js";
@@ -23,8 +24,8 @@ import "./chunk-6KYMPV2O.js";
23
24
  import "./chunk-DM2T26WE.js";
24
25
  import "./chunk-LDXUBPMO.js";
25
26
  import "./chunk-FVQJYWH7.js";
26
- import "./chunk-G7D6GZ5J.js";
27
27
  import "./chunk-VF4XKTX3.js";
28
+ import "./chunk-G7D6GZ5J.js";
28
29
  import "./chunk-4DJQYKMN.js";
29
30
  import "./chunk-2ODBA7MQ.js";
30
31
  import "./chunk-J2HSAU72.js";
@@ -4,11 +4,11 @@ import {
4
4
  import "../chunk-765K3SAT.js";
5
5
  import "../chunk-X7Y7WX73.js";
6
6
  import "../chunk-J4EB7DNW.js";
7
+ import "../chunk-UI3NYK34.js";
8
+ import "../chunk-GCGJW34D.js";
7
9
  import "../chunk-BJMBJZ2Y.js";
8
10
  import "../chunk-UKJAGEXH.js";
9
11
  import "../chunk-FP2373TW.js";
10
- import "../chunk-UI3NYK34.js";
11
- import "../chunk-GCGJW34D.js";
12
12
  import "../chunk-A6XUJE5D.js";
13
13
  import "../chunk-PZ5AY32C.js";
14
14
  export {
@@ -5,11 +5,11 @@ import {
5
5
  import "../chunk-WEHSQBFR.js";
6
6
  import "../chunk-X7Y7WX73.js";
7
7
  import "../chunk-J4EB7DNW.js";
8
+ import "../chunk-UI3NYK34.js";
9
+ import "../chunk-GCGJW34D.js";
8
10
  import "../chunk-BJMBJZ2Y.js";
9
11
  import "../chunk-UKJAGEXH.js";
10
12
  import "../chunk-FP2373TW.js";
11
- import "../chunk-UI3NYK34.js";
12
- import "../chunk-GCGJW34D.js";
13
13
  import "../chunk-A6XUJE5D.js";
14
14
  import "../chunk-PZ5AY32C.js";
15
15
  export {
@@ -4,10 +4,10 @@ import {
4
4
  import "../chunk-VF4XKTX3.js";
5
5
  import "../chunk-WEHSQBFR.js";
6
6
  import "../chunk-X7Y7WX73.js";
7
+ import "../chunk-GCGJW34D.js";
7
8
  import "../chunk-BJMBJZ2Y.js";
8
9
  import "../chunk-UKJAGEXH.js";
9
10
  import "../chunk-FP2373TW.js";
10
- import "../chunk-GCGJW34D.js";
11
11
  import "../chunk-A6XUJE5D.js";
12
12
  import "../chunk-VS2IYZRU.js";
13
13
  import "../chunk-PZ5AY32C.js";
@@ -1,11 +1,12 @@
1
1
  import {
2
2
  compareVerifiedEpisodeResults,
3
3
  searchVerifiedEpisodes
4
- } from "./chunk-YW52BQSU.js";
4
+ } from "./chunk-2TCHDANJ.js";
5
5
  import "./chunk-HQ6NIBL6.js";
6
- import "./chunk-VH6EIKVS.js";
6
+ import "./chunk-WKMCC4NQ.js";
7
7
  import "./chunk-M7XQSUBB.js";
8
8
  import "./chunk-5UZXUTVO.js";
9
+ import "./chunk-5GPPACXK.js";
9
10
  import "./chunk-J6A3CX5N.js";
10
11
  import "./chunk-AZBV4RRY.js";
11
12
  import "./chunk-AWJ2FHCF.js";
@@ -17,8 +18,8 @@ import "./chunk-6KYMPV2O.js";
17
18
  import "./chunk-DM2T26WE.js";
18
19
  import "./chunk-LDXUBPMO.js";
19
20
  import "./chunk-FVQJYWH7.js";
20
- import "./chunk-G7D6GZ5J.js";
21
21
  import "./chunk-VF4XKTX3.js";
22
+ import "./chunk-G7D6GZ5J.js";
22
23
  import "./chunk-4DJQYKMN.js";
23
24
  import "./chunk-ZBJMUXZH.js";
24
25
  import "./chunk-2ODBA7MQ.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.3.664",
3
+ "version": "9.3.666",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,97 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { parseConfig } from "./config.js";
5
+ import { resolveCapabilities, type CapabilitySet } from "./capabilities.js";
6
+
7
+ /**
8
+ * Characterization tests for the recall-operation CapabilitySet (issue #1523).
9
+ *
10
+ * These guard against composition drift: every capability field must project
11
+ * from its `<field>Enabled` config flag, so a future edit to
12
+ * `resolveCapabilities` that accidentally maps a field to the wrong flag (the
13
+ * rule-39 gate-divergence class, moved up one layer) fails loudly here.
14
+ */
15
+
16
+ /**
17
+ * Map of CapabilitySet field → the PluginConfig flag it projects from.
18
+ * Kept explicit (rather than derived by string concat) so the two graph flags
19
+ * with non-`<field>Enabled` names are covered too.
20
+ */
21
+ const FIELD_TO_FLAG: Record<keyof CapabilitySet, string> = {
22
+ rerankCache: "rerankCacheEnabled",
23
+ recallDirectAnswer: "recallDirectAnswerEnabled",
24
+ recallMemoryWorthFilter: "recallMemoryWorthFilterEnabled",
25
+ recallMmr: "recallMmrEnabled",
26
+ recallReasoningTraceBoost: "recallReasoningTraceBoostEnabled",
27
+ recallPlannerLlm: "recallPlannerLlmEnabled",
28
+ recallPlanner: "recallPlannerEnabled",
29
+ recallConfidenceGate: "recallConfidenceGateEnabled",
30
+ graphRecall: "graphRecallEnabled",
31
+ graphAssistInFullMode: "graphAssistInFullModeEnabled",
32
+ graphExpandedIntent: "graphExpandedIntentEnabled",
33
+ };
34
+
35
+ const FIELDS = Object.keys(FIELD_TO_FLAG) as Array<keyof CapabilitySet>;
36
+
37
+ test("resolveCapabilities projects every field from its <field>Enabled flag (true variant)", () => {
38
+ // Build a config where every migrated flag is explicitly true.
39
+ const overrides: Record<string, boolean> = {};
40
+ for (const flag of Object.values(FIELD_TO_FLAG)) overrides[flag] = true;
41
+ const config = parseConfig(overrides);
42
+ const caps = resolveCapabilities(config);
43
+
44
+ for (const field of FIELDS) {
45
+ const flag = FIELD_TO_FLAG[field];
46
+ assert.equal(
47
+ caps[field],
48
+ (config as unknown as Record<string, boolean>)[flag],
49
+ `caps.${field} must equal config.${flag} (true variant)`,
50
+ );
51
+ assert.equal(caps[field], true, `caps.${field} should be true here`);
52
+ }
53
+ });
54
+
55
+ test("resolveCapabilities projects every field from its <field>Enabled flag (false variant)", () => {
56
+ const overrides: Record<string, boolean> = {};
57
+ for (const flag of Object.values(FIELD_TO_FLAG)) overrides[flag] = false;
58
+ const config = parseConfig(overrides);
59
+ const caps = resolveCapabilities(config);
60
+
61
+ for (const field of FIELDS) {
62
+ const flag = FIELD_TO_FLAG[field];
63
+ // The two optional graph flags carry default-when-undefined semantics, but
64
+ // when explicitly set to a concrete boolean the projection must match it.
65
+ assert.equal(
66
+ caps[field],
67
+ (config as unknown as Record<string, boolean>)[flag],
68
+ `caps.${field} must equal config.${flag} (false variant)`,
69
+ );
70
+ assert.equal(caps[field], false, `caps.${field} should be false here`);
71
+ }
72
+ });
73
+
74
+ test("resolveCapabilities preserves optional-flag defaults when the flag is undefined", () => {
75
+ // parseConfig with no overrides exercises the documented defaults. The two
76
+ // optional graph flags encode asymmetric defaults on purpose:
77
+ // graphAssistInFullModeEnabled → default-ON (`!== false`)
78
+ // graphExpandedIntentEnabled → default-OFF (`=== true`)
79
+ const config = parseConfig({});
80
+ const caps = resolveCapabilities(config);
81
+
82
+ assert.equal(
83
+ caps.graphAssistInFullMode,
84
+ config.graphAssistInFullModeEnabled !== false,
85
+ "graphAssistInFullMode must be default-on unless explicitly false",
86
+ );
87
+ assert.equal(
88
+ caps.graphExpandedIntent,
89
+ config.graphExpandedIntentEnabled === true,
90
+ "graphExpandedIntent must be default-off unless explicitly true",
91
+ );
92
+ });
93
+
94
+ test("resolveCapabilities returns a frozen object", () => {
95
+ const caps = resolveCapabilities(parseConfig({}));
96
+ assert.equal(Object.isFrozen(caps), true, "CapabilitySet must be frozen");
97
+ });
@@ -0,0 +1,86 @@
1
+ /**
2
+ * CapabilitySet — recall-operation feature gates resolved once, then threaded.
3
+ *
4
+ * Issue #1523 (Phase 1 of epic #1520). Root cause this addresses: 161+
5
+ * scattered `config.<flag>Enabled` reads mean each gate is re-derived at every
6
+ * call site, and reviews keep finding parallel code paths where one branch
7
+ * checks a gate the other forgot (CLAUDE.md rule 39 — the "gate divergence"
8
+ * defect class). The fix is to resolve a frozen capability projection ONCE at
9
+ * the top of the recall operation and pass it down explicitly.
10
+ *
11
+ * Scope of THIS module (first migration PR): only the recall-operation-scoped
12
+ * flags below. Flags that are also read in graph construction, writes, CLI, or
13
+ * the summarizer are deliberately deferred to a follow-up so we never leave a
14
+ * single flag half-migrated (some sites on `caps.`, some on `config.`).
15
+ *
16
+ * Field naming: each field is the config flag name with the trailing `Enabled`
17
+ * removed (`recallMmrEnabled` → `recallMmr`).
18
+ *
19
+ * This is plumbing, not a feature — there is deliberately NO `enabled` gate for
20
+ * the CapabilitySet itself (rule 30 governs behavior changes; resolving and
21
+ * threading a capability projection must stay behavior-preserving).
22
+ */
23
+
24
+ import type { PluginConfig } from "./types.js";
25
+
26
+ /**
27
+ * Frozen projection of recall-operation feature gates.
28
+ *
29
+ * Every field is `readonly boolean`. The composition that maps a config flag to
30
+ * a capability (including default-when-undefined semantics for optional flags)
31
+ * lives ONLY in {@link resolveCapabilities} — call sites must read the
32
+ * capability, never re-derive it from raw config.
33
+ */
34
+ export interface CapabilitySet {
35
+ /** `rerankCacheEnabled` — cache reranker scores across recall passes. */
36
+ readonly rerankCache: boolean;
37
+ /** `recallDirectAnswerEnabled` — observation-mode direct-answer tier. */
38
+ readonly recallDirectAnswer: boolean;
39
+ /** `recallMemoryWorthFilterEnabled` — Memory-Worth score reweighting. */
40
+ readonly recallMemoryWorthFilter: boolean;
41
+ /** `recallMmrEnabled` — maximal-marginal-relevance diversification. */
42
+ readonly recallMmr: boolean;
43
+ /** `recallReasoningTraceBoostEnabled` — boost reasoning-trace memories. */
44
+ readonly recallReasoningTraceBoost: boolean;
45
+ /** `recallPlannerLlmEnabled` — LLM-backed recall-mode planner. */
46
+ readonly recallPlannerLlm: boolean;
47
+ /** `recallPlannerEnabled` — recall-mode planner (heuristic + optional LLM). */
48
+ readonly recallPlanner: boolean;
49
+ /** `recallConfidenceGateEnabled` — Synapse-style confidence gate. */
50
+ readonly recallConfidenceGate: boolean;
51
+ /** `graphRecallEnabled` — graph-mode recall tier (gates planner graph mode). */
52
+ readonly graphRecall: boolean;
53
+ /** `graphAssistInFullModeEnabled` — graph-assist overlay in full mode. */
54
+ readonly graphAssistInFullMode: boolean;
55
+ /** `graphExpandedIntentEnabled` — promote broad-intent asks to graph mode. */
56
+ readonly graphExpandedIntent: boolean;
57
+ }
58
+
59
+ /**
60
+ * Resolve the recall-operation {@link CapabilitySet} from parsed config.
61
+ *
62
+ * Call this ONCE per recall operation (at the `recall()` / `recallInternal`
63
+ * entry) and thread the result down. Composition lives here and only here.
64
+ *
65
+ * Session toggles are intentionally not a parameter yet: `session-toggles.ts`
66
+ * is agent-scoped (per session/agent enable-disable of the whole plugin), not
67
+ * flag-scoped — none of the flags projected here have a per-session override,
68
+ * so there is nothing for a toggle argument to compose at this layer.
69
+ */
70
+ export function resolveCapabilities(config: PluginConfig): CapabilitySet {
71
+ return Object.freeze({
72
+ rerankCache: config.rerankCacheEnabled,
73
+ recallDirectAnswer: config.recallDirectAnswerEnabled,
74
+ recallMemoryWorthFilter: config.recallMemoryWorthFilterEnabled,
75
+ recallMmr: config.recallMmrEnabled,
76
+ recallReasoningTraceBoost: config.recallReasoningTraceBoostEnabled,
77
+ recallPlannerLlm: config.recallPlannerLlmEnabled,
78
+ recallPlanner: config.recallPlannerEnabled,
79
+ recallConfidenceGate: config.recallConfidenceGateEnabled,
80
+ graphRecall: config.graphRecallEnabled,
81
+ // Optional flags: preserve the exact default-when-undefined semantics the
82
+ // migrated call sites used (`!== false` = default-on, `=== true` = default-off).
83
+ graphAssistInFullMode: config.graphAssistInFullModeEnabled !== false,
84
+ graphExpandedIntent: config.graphExpandedIntentEnabled === true,
85
+ });
86
+ }