@remnic/core 1.1.8 → 1.1.9

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 (237) hide show
  1. package/dist/access-cli.js +30 -30
  2. package/dist/access-http.d.ts +7 -7
  3. package/dist/access-http.js +13 -13
  4. package/dist/access-mcp.d.ts +7 -7
  5. package/dist/access-mcp.js +12 -12
  6. package/dist/{access-service-C0Rkioec.d.ts → access-service-BJCIjVRY.d.ts} +9 -9
  7. package/dist/access-service.d.ts +7 -7
  8. package/dist/access-service.js +11 -11
  9. package/dist/active-memory-bridge.d.ts +1 -1
  10. package/dist/active-recall.d.ts +2 -2
  11. package/dist/active-recall.js +2 -2
  12. package/dist/active-recall.js.map +1 -1
  13. package/dist/behavior-learner.d.ts +1 -1
  14. package/dist/behavior-signals.d.ts +1 -1
  15. package/dist/bootstrap.d.ts +6 -6
  16. package/dist/briefing.d.ts +2 -2
  17. package/dist/briefing.js +6 -6
  18. package/dist/buffer-surprise-report.d.ts +1 -1
  19. package/dist/buffer.d.ts +2 -2
  20. package/dist/calibration.d.ts +1 -1
  21. package/dist/calibration.js +2 -2
  22. package/dist/causal-behavior.d.ts +1 -1
  23. package/dist/causal-consolidation.d.ts +2 -2
  24. package/dist/causal-consolidation.js +8 -8
  25. package/dist/{chunk-ETA2JXP5.js → chunk-2MVUXO4H.js} +2 -2
  26. package/dist/{chunk-GZCUW5IC.js → chunk-3IQ2TR4N.js} +5 -5
  27. package/dist/chunk-3IQ2TR4N.js.map +1 -0
  28. package/dist/{chunk-KNQ5YJTO.js → chunk-3VRIIII5.js} +149 -1
  29. package/dist/chunk-3VRIIII5.js.map +1 -0
  30. package/dist/{chunk-TUFG6VXY.js → chunk-4DWOBS2A.js} +2 -2
  31. package/dist/chunk-4DWOBS2A.js.map +1 -0
  32. package/dist/{chunk-L2IO2QPY.js → chunk-4IS4SXIQ.js} +17 -13
  33. package/dist/chunk-4IS4SXIQ.js.map +1 -0
  34. package/dist/{chunk-RLV2F337.js → chunk-6OAQEOGV.js} +2 -2
  35. package/dist/{chunk-QJZ77K7F.js → chunk-6Z6UH6TK.js} +26 -12
  36. package/dist/chunk-6Z6UH6TK.js.map +1 -0
  37. package/dist/{chunk-4IT6WL23.js → chunk-7SFAENUZ.js} +2 -2
  38. package/dist/{chunk-ODWDQNRE.js → chunk-7SI52C65.js} +7 -3
  39. package/dist/chunk-7SI52C65.js.map +1 -0
  40. package/dist/{chunk-Q5TJRAGE.js → chunk-A6PGANSE.js} +3 -3
  41. package/dist/{chunk-OJMD2LIW.js → chunk-BIHCWSWA.js} +3 -3
  42. package/dist/{chunk-MYH2IBSP.js → chunk-CTYRIJ5E.js} +3 -3
  43. package/dist/{chunk-T65SHTJP.js → chunk-ET4BL42V.js} +1 -1
  44. package/dist/chunk-ET4BL42V.js.map +1 -0
  45. package/dist/{chunk-DWMXVUGO.js → chunk-FLBYSB2V.js} +6 -4
  46. package/dist/chunk-FLBYSB2V.js.map +1 -0
  47. package/dist/{chunk-GRDDGNYQ.js → chunk-FPWUENQH.js} +34 -32
  48. package/dist/chunk-FPWUENQH.js.map +1 -0
  49. package/dist/chunk-FVQJYWH7.js +52 -0
  50. package/dist/chunk-FVQJYWH7.js.map +1 -0
  51. package/dist/{chunk-STB3GUYU.js → chunk-G3G3LY22.js} +2 -2
  52. package/dist/{chunk-FIXIX6DE.js → chunk-G6NX57V2.js} +33 -43
  53. package/dist/chunk-G6NX57V2.js.map +1 -0
  54. package/dist/{chunk-3FPTCC3Z.js → chunk-GVPWB7EY.js} +2 -2
  55. package/dist/{chunk-AV2WSYZY.js → chunk-ICULSMDG.js} +2 -2
  56. package/dist/{chunk-WXPPM426.js → chunk-J3P6WSFZ.js} +2 -2
  57. package/dist/{chunk-FCGWNWG4.js → chunk-KIF7QNKL.js} +28 -28
  58. package/dist/chunk-KIF7QNKL.js.map +1 -0
  59. package/dist/{chunk-XVOIMCVW.js → chunk-KMWZXT5T.js} +2 -2
  60. package/dist/{chunk-SWRJFKYW.js → chunk-M3DK45UM.js} +5 -5
  61. package/dist/{chunk-XGX4TUF6.js → chunk-MJLUHRSF.js} +5 -5
  62. package/dist/{chunk-4KAN3GZ3.js → chunk-NN2DKE4T.js} +1 -1
  63. package/dist/chunk-NN2DKE4T.js.map +1 -0
  64. package/dist/{chunk-R2XRID2N.js → chunk-NN3LPQ5D.js} +5 -5
  65. package/dist/chunk-NN3LPQ5D.js.map +1 -0
  66. package/dist/{chunk-RJSVRPNU.js → chunk-OWGGXPKV.js} +16 -9
  67. package/dist/chunk-OWGGXPKV.js.map +1 -0
  68. package/dist/{chunk-WSZIHQBK.js → chunk-P77UEOU2.js} +4 -1
  69. package/dist/{chunk-WSZIHQBK.js.map → chunk-P77UEOU2.js.map} +1 -1
  70. package/dist/{chunk-65ZPH7QA.js → chunk-PHQH2VUO.js} +4 -4
  71. package/dist/{chunk-KHJRMWO4.js → chunk-QPLYTPYL.js} +15 -15
  72. package/dist/{chunk-FEMOX5AD.js → chunk-QR3C7BKQ.js} +7 -7
  73. package/dist/chunk-QR3C7BKQ.js.map +1 -0
  74. package/dist/{chunk-3LCWFNVS.js → chunk-SKE7JYKA.js} +2 -2
  75. package/dist/{chunk-E27HOXMX.js → chunk-U4SZXGEO.js} +2 -2
  76. package/dist/{chunk-67YLUWLG.js → chunk-XJKFSSDW.js} +3 -3
  77. package/dist/chunk-XJKFSSDW.js.map +1 -0
  78. package/dist/{chunk-SYWJJTNL.js → chunk-XL3UCAZA.js} +22 -22
  79. package/dist/{chunk-ASIQZXYO.js → chunk-XMVFHBHT.js} +2 -2
  80. package/dist/{chunk-SRIDOT64.js → chunk-XN4D6Z7X.js} +3 -3
  81. package/dist/{chunk-S5SQDIF5.js → chunk-Y3VT6ZCP.js} +4 -4
  82. package/dist/{cli-CIATRu8o.d.ts → cli-BojuyOOp.d.ts} +4 -4
  83. package/dist/cli.d.ts +8 -8
  84. package/dist/cli.js +24 -24
  85. package/dist/{codex-materialize-xVqbEmcm.d.ts → codex-materialize-YVC2wb6n.d.ts} +1 -1
  86. package/dist/compression-optimizer.d.ts +1 -1
  87. package/dist/config.d.ts +1 -1
  88. package/dist/config.js +1 -1
  89. package/dist/consolidation-provenance-check.d.ts +2 -2
  90. package/dist/consolidation-undo.d.ts +2 -2
  91. package/dist/day-summary.d.ts +1 -1
  92. package/dist/day-summary.js +1 -1
  93. package/dist/delinearize.d.ts +1 -1
  94. package/dist/direct-answer-wiring.d.ts +1 -1
  95. package/dist/direct-answer.d.ts +1 -1
  96. package/dist/embedding-fallback.d.ts +1 -1
  97. package/dist/{engine-MEAYUA7A.js → engine-EDFFOWDD.js} +7 -7
  98. package/dist/entity-retrieval.d.ts +2 -2
  99. package/dist/entity-retrieval.js +6 -6
  100. package/dist/entity-schema.d.ts +1 -1
  101. package/dist/explicit-capture.d.ts +6 -6
  102. package/dist/explicit-capture.js +2 -2
  103. package/dist/explicit-cue-recall.js +1 -1
  104. package/dist/extraction-judge-telemetry.d.ts +1 -1
  105. package/dist/extraction-judge-training.d.ts +1 -1
  106. package/dist/extraction-judge.d.ts +1 -1
  107. package/dist/extraction.d.ts +1 -1
  108. package/dist/extraction.js +7 -7
  109. package/dist/fallback-llm.d.ts +1 -1
  110. package/dist/fallback-llm.js +2 -2
  111. package/dist/identity-continuity.d.ts +1 -1
  112. package/dist/importance.d.ts +1 -1
  113. package/dist/index.d.ts +13 -13
  114. package/dist/index.js +147 -147
  115. package/dist/index.js.map +1 -1
  116. package/dist/intent.d.ts +1 -1
  117. package/dist/lifecycle.d.ts +1 -1
  118. package/dist/live-connectors-runner.d.ts +1 -1
  119. package/dist/live-connectors-runner.js +2 -2
  120. package/dist/local-llm.d.ts +1 -1
  121. package/dist/local-llm.js +1 -1
  122. package/dist/memory-action-policy.d.ts +1 -1
  123. package/dist/memory-cache.d.ts +1 -1
  124. package/dist/{memory-governance-G3XODEXW.js → memory-governance-AAQPBZEP.js} +7 -7
  125. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  126. package/dist/{memory-projection-store-lCzmu4JX.d.ts → memory-projection-store-BW8u5U0u.d.ts} +1 -1
  127. package/dist/memory-projection-store.d.ts +2 -2
  128. package/dist/memory-projection-store.js +1 -1
  129. package/dist/memory-worth-outcomes.d.ts +2 -2
  130. package/dist/{migrate-from-identity-anchor-TTEDEJGX.js → migrate-from-identity-anchor-G27MCD6A.js} +2 -2
  131. package/dist/model-registry.js +1 -1
  132. package/dist/models-json.d.ts +1 -1
  133. package/dist/models-json.js +1 -1
  134. package/dist/native-knowledge.d.ts +1 -1
  135. package/dist/operator-toolkit.d.ts +2 -2
  136. package/dist/operator-toolkit.js +10 -10
  137. package/dist/opik-exporter.js +2 -2
  138. package/dist/opik-exporter.js.map +1 -1
  139. package/dist/{orchestrator-CvUYwuaL.d.ts → orchestrator-CYqmqxco.d.ts} +5 -5
  140. package/dist/orchestrator.d.ts +6 -6
  141. package/dist/orchestrator.js +25 -25
  142. package/dist/patterns-cli.d.ts +1 -1
  143. package/dist/{peers-6OSQ3NK6.js → peers-HCVGHMAE.js} +3 -3
  144. package/dist/policy-runtime.d.ts +1 -1
  145. package/dist/{port-BkWL7hqo.d.ts → port-Br27H8dy.d.ts} +7 -1
  146. package/dist/qmd-recall-cache.d.ts +2 -2
  147. package/dist/qmd.d.ts +3 -2
  148. package/dist/qmd.js +1 -1
  149. package/dist/recall-disclosure-escalation.d.ts +1 -1
  150. package/dist/recall-explain-renderer.d.ts +1 -1
  151. package/dist/recall-explain-renderer.js +3 -3
  152. package/dist/recall-state.d.ts +1 -1
  153. package/dist/recall-tag-filter.d.ts +1 -1
  154. package/dist/recall-xray-cli.d.ts +1 -1
  155. package/dist/recall-xray-cli.js +4 -4
  156. package/dist/recall-xray-renderer.d.ts +1 -1
  157. package/dist/recall-xray-renderer.js +3 -3
  158. package/dist/recall-xray.d.ts +1 -1
  159. package/dist/recall-xray.js +2 -2
  160. package/dist/resolve-auth-token.d.ts +1 -1
  161. package/dist/resume-bundles.js +2 -2
  162. package/dist/retrieval-agents.d.ts +2 -2
  163. package/dist/retrieval-tiers.d.ts +1 -1
  164. package/dist/sanitize.js +1 -1
  165. package/dist/schemas.d.ts +22 -22
  166. package/dist/{semantic-consolidation-CGiH52qa.d.ts → semantic-consolidation-GPcLr9BQ.d.ts} +2 -2
  167. package/dist/semantic-consolidation.d.ts +3 -3
  168. package/dist/semantic-consolidation.js +6 -6
  169. package/dist/semantic-rule-promotion.js +6 -6
  170. package/dist/semantic-rule-verifier.d.ts +1 -1
  171. package/dist/semantic-rule-verifier.js +6 -6
  172. package/dist/session-observer-bands.d.ts +1 -1
  173. package/dist/session-observer-state.d.ts +1 -1
  174. package/dist/signal.d.ts +1 -1
  175. package/dist/source-attribution.d.ts +1 -1
  176. package/dist/source-attribution.js +1 -1
  177. package/dist/storage.d.ts +2 -2
  178. package/dist/storage.js +5 -5
  179. package/dist/summarizer.d.ts +1 -1
  180. package/dist/summarizer.js +5 -5
  181. package/dist/summary-snapshot.d.ts +1 -1
  182. package/dist/temporal-supersession.d.ts +2 -2
  183. package/dist/temporal-validity.d.ts +1 -1
  184. package/dist/threading.d.ts +1 -1
  185. package/dist/tier-migration.d.ts +3 -3
  186. package/dist/tier-routing.d.ts +1 -1
  187. package/dist/topics.d.ts +1 -1
  188. package/dist/transcript.d.ts +1 -1
  189. package/dist/{types-H85grL1f.d.ts → types-Bmp9ssU2.d.ts} +3 -3
  190. package/dist/types.d.ts +1 -1
  191. package/dist/types.js +1 -1
  192. package/dist/utility-runtime.d.ts +1 -1
  193. package/dist/verified-recall.js +6 -6
  194. package/package.json +1 -1
  195. package/dist/chunk-4KAN3GZ3.js.map +0 -1
  196. package/dist/chunk-67YLUWLG.js.map +0 -1
  197. package/dist/chunk-DWMXVUGO.js.map +0 -1
  198. package/dist/chunk-FCGWNWG4.js.map +0 -1
  199. package/dist/chunk-FEMOX5AD.js.map +0 -1
  200. package/dist/chunk-FIXIX6DE.js.map +0 -1
  201. package/dist/chunk-GRDDGNYQ.js.map +0 -1
  202. package/dist/chunk-GZCUW5IC.js.map +0 -1
  203. package/dist/chunk-KNQ5YJTO.js.map +0 -1
  204. package/dist/chunk-L2IO2QPY.js.map +0 -1
  205. package/dist/chunk-M62O4P4T.js +0 -41
  206. package/dist/chunk-M62O4P4T.js.map +0 -1
  207. package/dist/chunk-ODWDQNRE.js.map +0 -1
  208. package/dist/chunk-QJZ77K7F.js.map +0 -1
  209. package/dist/chunk-R2XRID2N.js.map +0 -1
  210. package/dist/chunk-RJSVRPNU.js.map +0 -1
  211. package/dist/chunk-T65SHTJP.js.map +0 -1
  212. package/dist/chunk-TUFG6VXY.js.map +0 -1
  213. /package/dist/{chunk-ETA2JXP5.js.map → chunk-2MVUXO4H.js.map} +0 -0
  214. /package/dist/{chunk-RLV2F337.js.map → chunk-6OAQEOGV.js.map} +0 -0
  215. /package/dist/{chunk-4IT6WL23.js.map → chunk-7SFAENUZ.js.map} +0 -0
  216. /package/dist/{chunk-Q5TJRAGE.js.map → chunk-A6PGANSE.js.map} +0 -0
  217. /package/dist/{chunk-OJMD2LIW.js.map → chunk-BIHCWSWA.js.map} +0 -0
  218. /package/dist/{chunk-MYH2IBSP.js.map → chunk-CTYRIJ5E.js.map} +0 -0
  219. /package/dist/{chunk-STB3GUYU.js.map → chunk-G3G3LY22.js.map} +0 -0
  220. /package/dist/{chunk-3FPTCC3Z.js.map → chunk-GVPWB7EY.js.map} +0 -0
  221. /package/dist/{chunk-AV2WSYZY.js.map → chunk-ICULSMDG.js.map} +0 -0
  222. /package/dist/{chunk-WXPPM426.js.map → chunk-J3P6WSFZ.js.map} +0 -0
  223. /package/dist/{chunk-XVOIMCVW.js.map → chunk-KMWZXT5T.js.map} +0 -0
  224. /package/dist/{chunk-SWRJFKYW.js.map → chunk-M3DK45UM.js.map} +0 -0
  225. /package/dist/{chunk-XGX4TUF6.js.map → chunk-MJLUHRSF.js.map} +0 -0
  226. /package/dist/{chunk-65ZPH7QA.js.map → chunk-PHQH2VUO.js.map} +0 -0
  227. /package/dist/{chunk-KHJRMWO4.js.map → chunk-QPLYTPYL.js.map} +0 -0
  228. /package/dist/{chunk-3LCWFNVS.js.map → chunk-SKE7JYKA.js.map} +0 -0
  229. /package/dist/{chunk-E27HOXMX.js.map → chunk-U4SZXGEO.js.map} +0 -0
  230. /package/dist/{chunk-SYWJJTNL.js.map → chunk-XL3UCAZA.js.map} +0 -0
  231. /package/dist/{chunk-ASIQZXYO.js.map → chunk-XMVFHBHT.js.map} +0 -0
  232. /package/dist/{chunk-SRIDOT64.js.map → chunk-XN4D6Z7X.js.map} +0 -0
  233. /package/dist/{chunk-S5SQDIF5.js.map → chunk-Y3VT6ZCP.js.map} +0 -0
  234. /package/dist/{engine-MEAYUA7A.js.map → engine-EDFFOWDD.js.map} +0 -0
  235. /package/dist/{memory-governance-G3XODEXW.js.map → memory-governance-AAQPBZEP.js.map} +0 -0
  236. /package/dist/{migrate-from-identity-anchor-TTEDEJGX.js.map → migrate-from-identity-anchor-G27MCD6A.js.map} +0 -0
  237. /package/dist/{peers-6OSQ3NK6.js.map → peers-HCVGHMAE.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/source-attribution.ts"],"sourcesContent":["/**\n * Inline Source Attribution Protocol (issue #369)\n *\n * Extracted facts carry provenance inline inside the fact body, so the\n * citation survives hostile memory text, copy/paste, and LLM quoting. This\n * complements — never replaces — the YAML frontmatter provenance stored on\n * disk.\n *\n * Default format (matches issue #369 proposal):\n *\n * The foo service uses Redis for rate limiting. [Source: agent=planner, session=abc123, ts=2026-04-10T14:25:07Z]\n *\n * Key properties:\n * - Inline (part of the body, not metadata).\n * - Compact (typically <80 chars of overhead per fact).\n * - Machine-parseable by a single regex.\n * - Opt-in via `inlineSourceAttributionEnabled` config flag (default off\n * for backwards compatibility with existing downstream consumers).\n * - Legacy facts without a citation remain fully readable.\n *\n * The format template is configurable via `inlineSourceAttributionFormat`\n * with supported placeholders:\n *\n * {agent} — principal / agent identifier\n * {session} — full session key (colon-delimited)\n * {sessionId} — short stable session id (trailing component)\n * {ts} — extraction timestamp (ISO 8601)\n * {date} — extraction date (YYYY-MM-DD)\n *\n * Any privacy-sensitive identifiers should be normalized before being passed\n * to `formatCitation` — the helper treats them as opaque strings.\n */\n\n/** Default citation format template (matches issue #369). */\nexport const DEFAULT_CITATION_FORMAT =\n \"[Source: agent={agent}, session={sessionId}, ts={ts}]\";\n\n/** Sentinel value used when a provenance field is missing. */\nexport const CITATION_UNKNOWN = \"unknown\";\n\nexport interface CitationContext {\n /** Principal / agent identifier (e.g. resolved via resolvePrincipal). */\n agent?: string;\n /** Full session key (e.g. \"agent:planner:main\"). */\n session?: string;\n /**\n * Opaque short session id. Derived from the trailing component of the\n * session key when not provided explicitly. Use this for compact formats\n * that do not need the full colon-delimited session key.\n */\n sessionId?: string;\n /** Extraction timestamp as an ISO 8601 string. */\n ts?: string;\n}\n\nexport interface ParsedCitation {\n /** Agent identifier parsed from the citation (never crashes on malformed input). */\n agent?: string;\n /** Session identifier parsed from the citation. */\n session?: string;\n /** Extraction timestamp parsed from the citation. */\n ts?: string;\n /** The full matched citation substring. */\n raw: string;\n}\n\n/**\n * Regex that matches the default `[Source: agent=X, session=Y, ts=Z]` shape\n * as well as human-edited variants (extra whitespace, reordered fields,\n * subset of fields). Matches non-greedily so it can be anchored anywhere in\n * the text. Kept as a getter factory so callers do not share regex state.\n */\nfunction defaultCitationMatcher(): RegExp {\n return /\\[Source:\\s*([^\\]\\n]+?)\\]/gi;\n}\n\n/**\n * Derive a short session id from a full session key.\n * Falls back to the raw session string if no colon is present.\n */\nexport function deriveSessionId(session: string | undefined): string | undefined {\n if (!session) return undefined;\n const trimmed = session.trim();\n if (trimmed.length === 0) return undefined;\n const parts = trimmed.split(\":\").filter((p) => p.length > 0);\n if (parts.length === 0) return trimmed;\n return parts[parts.length - 1];\n}\n\n/**\n * Format an inline citation tag using the provided template.\n *\n * Missing context fields fall back to {@link CITATION_UNKNOWN} — the caller\n * should always get a non-empty, parseable tag.\n *\n * Uses a single-pass substitution so that values which themselves contain\n * placeholder syntax (e.g. an agent literally named `\"{ts}\"`) cannot be\n * re-interpreted by subsequent replacement steps. Each placeholder slot\n * receives exactly one lookup and the substituted value is treated as\n * terminal text, not template source.\n */\nexport function formatCitation(\n ctx: CitationContext,\n template: string = DEFAULT_CITATION_FORMAT,\n): string {\n const session = ctx.session ?? \"\";\n const sessionId = ctx.sessionId ?? deriveSessionId(session) ?? CITATION_UNKNOWN;\n const ts = ctx.ts ?? CITATION_UNKNOWN;\n const agent = ctx.agent && ctx.agent.trim().length > 0 ? ctx.agent : CITATION_UNKNOWN;\n const date = ts && ts !== CITATION_UNKNOWN ? ts.slice(0, 10) : CITATION_UNKNOWN;\n const sessionForTemplate = session.trim().length > 0 ? session : CITATION_UNKNOWN;\n\n // Map from recognised placeholder names to their resolved value. Unknown\n // placeholder names are left intact (returning the original `{name}`).\n const values: Record<string, string> = {\n agent,\n session: sessionForTemplate,\n sessionId,\n ts,\n date,\n };\n\n // Single-pass scan: replace every recognised `{name}` in one sweep so that\n // substituted values cannot themselves be treated as template source on a\n // subsequent pass. The replacer-function form also guarantees that `$` /\n // `$&` / `$1` sequences inside values are emitted literally.\n return template.replace(/\\{([a-zA-Z_][\\w]*)\\}/g, (match, name: string) => {\n return Object.prototype.hasOwnProperty.call(values, name)\n ? values[name]!\n : match;\n });\n}\n\n/**\n * Returns true if the text already carries at least one citation marker.\n * Safe to call on any string — never throws.\n */\nexport function hasCitation(text: string): boolean {\n if (typeof text !== \"string\" || text.length === 0) return false;\n return defaultCitationMatcher().test(text);\n}\n\n/**\n * Escape a string for use as a regex literal.\n */\nfunction escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Escape a single character for use INSIDE a character class `[...]`.\n * Special chars inside character classes: `]`, `\\`, `^`, `-`.\n */\nfunction escapeRegExpCharClass(ch: string): string {\n if (ch === \"]\") return \"\\\\]\";\n if (ch === \"\\\\\") return \"\\\\\\\\\";\n if (ch === \"^\") return \"\\\\^\";\n if (ch === \"-\") return \"\\\\-\";\n // Other regex meta chars are NOT special inside [...] but escaping them is safe.\n return escapeRegExp(ch);\n}\n\n/**\n * Build a per-placeholder token pattern that excludes newlines, whitespace,\n * and any punctuation characters used as inner separators in the template.\n *\n * This prevents a placeholder span from consuming separator bytes and\n * matching strings that cross separator boundaries in user-supplied content.\n *\n * @param nonWordSepChars - Set of non-word (non-alphanumeric, non-`_`) chars\n * extracted from the inner separator literals of the template.\n */\nfunction buildTokenPattern(nonWordSepChars: Set<string>): string {\n // Always exclude newlines and whitespace.\n const base = \"\\\\n\\\\s\";\n if (nonWordSepChars.size === 0) {\n // No inner separator punctuation — the placeholder spans the full space\n // between prefix and suffix. Fall back to a generous no-newline match.\n return `[^\\\\n]+?`;\n }\n const escaped = [...nonWordSepChars].map(escapeRegExpCharClass).join(\"\");\n return `[^${base}${escaped}]+?`;\n}\n\n/** Regex that matches a `{placeholder}` token inside a template string. */\nconst PLACEHOLDER_REGEX = /\\{[a-zA-Z_][\\w]*\\}/g;\n\n/**\n * Build a regex that matches a citation produced by the given template.\n *\n * The approach depends on the shape of the template:\n *\n * - **Normal case (non-empty literal prefix or suffix).** Anchor the match\n * on the outer literal frame and reconstruct the interior as\n * `interToken + sep + interToken + sep + ... + sep + lastToken`.\n * All **intermediate** per-placeholder tokens exclude the combined set of\n * non-word separator characters used between any two adjacent placeholders,\n * preventing a value from consuming a separator and crossing placeholder\n * boundaries. The **last** token is only required to avoid newlines because\n * it is terminated by the literal suffix anchor — this lets placeholder\n * values that legitimately contain a separator character be recognised (e.g.\n * an ISO-8601 timestamp `2026-04-10T14:25:07Z` in `[src:{agent}:{ts}]`\n * where `:` is the inter-placeholder separator). A template like\n * `[src:{agent}/{sessionId}@{date}]` emits\n * `\\[src:[^\\n\\s/@]+?\\/[^\\n\\s/@]+?@[^\\n]+?\\]` so that `[src:foo/bar]`\n * is NOT matched (wrong separator count), `[src:foo/bar/extra@2026]`\n * is NOT matched (intermediate token crosses a `/` boundary), and\n * `[src:planner/main@2026-04-10]` IS matched correctly.\n *\n * - **Placeholder-bounded with whitespace separator.** Both prefix and\n * suffix are empty and the separator literal(s) between placeholders\n * contain at least one whitespace character (e.g. `{source}: {content}`,\n * `{agent} {sessionId}`). A whitespace-containing separator produces\n * output that is visually indistinguishable from ordinary prose, so the\n * safe strategy is to require a **hard bracket/paren/angle delimiter** on\n * both sides of the reconstructed match. Prose almost never places\n * `[...]` / `(...)` / `<...>` around a phrase, so this yields clean\n * false-positive rejection.\n *\n * - **Placeholder-bounded with compact (non-whitespace) separator.** Both\n * prefix and suffix are empty and the separator literal(s) contain NO\n * whitespace (e.g. `{agent}:{sessionId}`, `{agent}/{sessionId}`).\n * `formatCitation` emits a compact token like `planner:main` with no\n * surrounding delimiters, so the bracket strategy cannot detect it.\n * Instead, the pattern requires that the entire token is bordered by\n * whitespace or a bracket/paren/angle on each side:\n *\n * `(?:(?<=[\\[\\(\\<])|(?<!\\S))[\\w.-]+<sep>[\\w.-]+(?:(?=[\\]\\)\\>])|(?!\\S))`\n *\n * This accepts `planner:main` when it appears standalone or inside a\n * bracket-wrapped token, and rejects `host:80` embedded inside a URL like\n * `http://host:80` because `host` is immediately preceded by `/`\n * (non-whitespace, non-bracket).\n *\n * - **All-placeholder case (no literals between placeholders either).** No\n * reliable regex can be built — a template like `{agent}{sessionId}`\n * contains no anchor characters. Returns `null`; {@link\n * hasCitationForTemplate} treats this as \"cannot detect\" and returns\n * false, falling back on explicit sentinel/format detection only for the\n * default `[Source: ...]` shape.\n *\n * Returns `null` when the template has no placeholders (fully-literal\n * citation, handled by the string-equality fast path in {@link\n * hasCitationForTemplate}) **or** when the template is entirely placeholder-\n * only with no literal content whatsoever.\n */\nfunction templateMatcher(template: string): RegExp | null {\n // Split around all {placeholder} tokens.\n const parts = template.split(PLACEHOLDER_REGEX);\n if (parts.length <= 1) return null;\n\n const prefix = parts[0] ?? \"\";\n const suffix = parts[parts.length - 1] ?? \"\";\n\n // Normal case: at least one literal frame on the outside.\n // Tighten the per-placeholder token so it cannot consume separator\n // characters and match strings that cross separator boundaries\n // (Finding 3 — Uru3).\n if (prefix.length > 0 || suffix.length > 0) {\n const escapedPrefix = escapeRegExp(prefix);\n const escapedSuffix = escapeRegExp(suffix);\n\n // Inner parts: literal separators that sit between adjacent placeholders.\n // For `[src:{agent}/{sessionId}@{date}]`, parts = [\"[src:\", \"/\", \"@\", \"]\"]\n // so innerParts = [\"/\", \"@\"].\n const innerParts = parts.slice(1, -1);\n\n // Collect only the non-word (punctuation/symbol) characters from each\n // inner separator so alphabetic separator text (unlikely but valid) does\n // not exclude letters from the per-placeholder token pattern.\n const nonWordSepChars = new Set<string>();\n for (const sep of innerParts) {\n for (const ch of sep) {\n if (!/\\w/.test(ch)) {\n nonWordSepChars.add(ch);\n }\n }\n }\n\n // All intermediate tokens (every placeholder except the last) use the\n // combined exclusion so they cannot cross placeholder boundaries.\n //\n // The LAST token is different: it is terminated by the literal suffix anchor\n // (e.g. `\\]`), so it does not need to exclude inner-separator characters.\n // Dropping that restriction lets placeholder values that legitimately contain\n // a separator character (e.g. an ISO-8601 timestamp `2026-04-10T14:25:07Z`\n // in template `[src:{agent}:{ts}]`) be recognised correctly instead of\n // producing false-negative misses that trigger duplicate citation injection.\n //\n // Only the LAST token is relaxed. Intermediate tokens keep the combined\n // exclusion so that cross-boundary false positives are still rejected\n // (e.g. `[src:foo/bar/extra@2026-04-11]` for `[src:{a}/{b}@{c}]`).\n const interToken = buildTokenPattern(nonWordSepChars);\n // Last token: terminated by suffix anchor — exclude only newlines.\n const lastToken = buildTokenPattern(new Set<string>());\n\n // Reconstruct the interior: interToken sep interToken sep ... sep lastToken\n // (or just lastToken when there are no inner separators at all).\n const middle =\n innerParts.length === 0\n ? lastToken\n : interToken +\n innerParts\n .slice(0, -1)\n .map((sep) => escapeRegExp(sep) + interToken)\n .join(\"\") +\n escapeRegExp(innerParts[innerParts.length - 1]!) +\n lastToken;\n\n const pattern = escapedPrefix + middle + escapedSuffix;\n return new RegExp(pattern, \"i\");\n }\n\n // Placeholder-bounded case: prefix and suffix are both empty.\n const middleLiterals = parts.slice(1, -1);\n const hasNonEmptyMiddle = middleLiterals.some((p) => p.length > 0);\n if (!hasNonEmptyMiddle) {\n // All-placeholder template with no literal content. Impossible to anchor\n // reliably without sentinel markers; signal the caller.\n return null;\n }\n\n // Identifier token: one or more word chars, dots, dashes, or colons.\n // Colons are included to allow timestamp values like \"10:30\" or session\n // keys like \"agent:planner:main\" inside compact placeholder-bounded\n // templates. URL-like fragments (`http://host:80`) are still rejected\n // because the lead anchor requires whitespace or a bracket immediately\n // before the first id-token group (`http` is preceded by `/`).\n const idToken = \"[\\\\w.:-]+\";\n const body =\n idToken +\n middleLiterals.map((lit) => escapeRegExp(lit) + idToken).join(\"\");\n\n const separatorText = middleLiterals.join(\"\");\n if (/\\s/.test(separatorText)) {\n // Separator contains whitespace: the emitted citation looks like ordinary\n // prose (e.g. `planner main`). Require a hard bracket/paren/angle\n // delimiter on both sides to prevent false matches on English text.\n const opener = \"[\\\\[\\\\(\\\\<]\";\n const closer = \"[\\\\]\\\\)\\\\>]\";\n return new RegExp(opener + body + closer, \"i\");\n }\n\n // Separator is compact (no whitespace): `formatCitation` emits a token like\n // `planner:main` without surrounding delimiters. The challenge is that the\n // same token shape also matches ordinary hyphenated or slashed prose words\n // (e.g. `long-term`, `docs/setup`), causing `hasCitationForTemplate` to\n // return true on uncited fact bodies and silently suppress citation injection\n // from `attachCitation`.\n //\n // Fix (Finding 1): tighten the trail anchor so a bare compact token is only\n // accepted when it sits at the very end of the string (possibly followed by\n // optional trailing whitespace or a newline). Since `attachCitation` always\n // appends the citation at the trimmed end of the fact body, a real citation\n // token will always appear at the tail. Prose like `\"long-term solution\"`\n // has `long-term` in the middle of the string (followed by ` solution`), so\n // the end-of-string anchor rejects it — no false positive, no silent drop.\n //\n // The lead anchor still accepts either a bracket opener or a whitespace\n // boundary (or start of string), so `\"Fact. planner:main\"` and standalone\n // `\"planner:main\"` are both detected after the first attachment pass.\n //\n // Bracket-wrapped form (e.g. `[planner:main]`) is also accepted via the\n // opener/closer pair — bracket still takes precedence over end-of-string.\n //\n // Example — why `http://host:80` does NOT match:\n // Trying to match `host:80`: the char before `h` is `/` (non-whitespace,\n // non-bracket), so `(?<=[\\[\\(\\<])` and `(?<!\\S)` both fail ⟹ no match.\n // Trying to match `http:...`: after `http:` the next chars are `//` which\n // are not `[\\w.-]+`, so the second id-token group fails ⟹ no match.\n const leadAnchor = \"(?:(?<=[\\\\[\\\\(\\\\<])|(?<!\\\\S))\";\n // Trail: either a bracket closer (for `[token]` shape) or end-of-string\n // optionally preceded by whitespace. The `(?!\\S)` is deliberately removed\n // so that a compact token in the MIDDLE of a sentence does not match.\n const trailAnchor = \"(?:(?=[\\\\]\\\\)\\\\>])|(?=\\\\s*$))\";\n return new RegExp(leadAnchor + body + trailAnchor, \"i\");\n}\n\n/**\n * Returns true if `text` already carries a citation produced by `template`\n * **or** by the default `[Source: ...]` format (for facts that were tagged\n * before a config change).\n *\n * Use this instead of {@link hasCitation} whenever the caller has access to\n * the configured `inlineSourceAttributionFormat`.\n *\n * All-placeholder templates such as `{agent}{sessionId}` have no literal\n * content to anchor on and therefore cannot be reliably detected without\n * dedicated sentinel markers. In that case the function returns `false` —\n * callers that need idempotent dedup for such templates should either adopt\n * a template with literal delimiters (recommended) or rely on the default\n * `[Source: ...]` marker detection which is always available via\n * {@link hasCitation}.\n */\nexport function hasCitationForTemplate(text: string, template: string): boolean {\n if (typeof text !== \"string\" || text.length === 0) return false;\n // Always accept the default format as a fallback so facts tagged before a\n // configuration change are not double-tagged on reprocessing.\n if (hasCitation(text)) return true;\n // If the configured template matches the default, we're done.\n //\n // Known limitation (Thread 2 — Codex P2): this fast path exits without\n // checking whether the content carries a citation from a DIFFERENT custom\n // template that was active before the config was changed back to the default.\n // Such a fact would be detected by `hasCitation` above only if the prior\n // custom template happened to match the default `[Source: ...]` pattern.\n // In practice, template changes mid-stream are rare, and the false-negative\n // (missing an old custom citation) produces a benign duplicate citation rather\n // than data loss. A full fix would require storing the citation template used\n // at write time in the frontmatter and checking that here.\n if (template === DEFAULT_CITATION_FORMAT) return false;\n\n // Fully-literal template (no placeholders): exact inclusion check.\n if (!PLACEHOLDER_REGEX.test(template)) {\n // Reset lastIndex because PLACEHOLDER_REGEX is declared with /g.\n PLACEHOLDER_REGEX.lastIndex = 0;\n return text.includes(template);\n }\n // Reset lastIndex after the .test() probe above.\n PLACEHOLDER_REGEX.lastIndex = 0;\n\n const matcher = templateMatcher(template);\n if (!matcher) {\n // All-placeholder template: cannot build a reliable matcher. See the\n // docstring — callers should not rely on dedup for this shape.\n return false;\n }\n return matcher.test(text);\n}\n\n/**\n * Attach an inline citation to fact text.\n *\n * If the text already has a citation — either the default `[Source: ...]`\n * marker or one produced by the configured template — it is returned unchanged.\n * Existing provenance is respected and never overwritten. Otherwise the\n * citation is appended to the trimmed text with a single space separator,\n * which keeps the marker visually adjacent to the fact body.\n */\nexport function attachCitation(\n text: string,\n ctx: CitationContext,\n template: string = DEFAULT_CITATION_FORMAT,\n): string {\n if (typeof text !== \"string\") return text as unknown as string;\n if (hasCitationForTemplate(text, template)) return text;\n const trimmedEnd = text.replace(/\\s+$/u, \"\");\n if (trimmedEnd.length === 0) return text;\n const citation = formatCitation(ctx, template);\n // Preserve any trailing newline that callers rely on for markdown rendering.\n const trailing = text.slice(trimmedEnd.length);\n return `${trimmedEnd} ${citation}${trailing}`;\n}\n\n/**\n * Parse a single inline citation from a piece of text. Returns the first\n * citation encountered or `null` when none is present. Malformed citations\n * do not throw — fields that cannot be parsed simply remain `undefined`.\n */\nexport function parseCitation(text: string): ParsedCitation | null {\n if (typeof text !== \"string\" || text.length === 0) return null;\n const matcher = defaultCitationMatcher();\n const match = matcher.exec(text);\n if (!match) return null;\n\n const body = match[1] ?? \"\";\n const raw = match[0] ?? \"\";\n const parsed: ParsedCitation = { raw };\n\n const fields = body\n .split(\",\")\n .map((segment) => segment.trim())\n .filter((segment) => segment.length > 0);\n\n for (const field of fields) {\n const eqIdx = field.indexOf(\"=\");\n if (eqIdx <= 0) continue;\n const key = field.slice(0, eqIdx).trim().toLowerCase();\n const value = field.slice(eqIdx + 1).trim();\n if (value.length === 0) continue;\n switch (key) {\n case \"agent\":\n parsed.agent = value;\n break;\n case \"session\":\n case \"sessionid\":\n parsed.session = value;\n break;\n case \"ts\":\n case \"timestamp\":\n parsed.ts = value;\n break;\n default:\n // Unknown fields are ignored defensively so human edits never crash.\n break;\n }\n }\n\n return parsed;\n}\n\n/**\n * Parse every citation embedded in the text. Always returns an array; empty\n * when none are present.\n */\nexport function parseAllCitations(text: string): ParsedCitation[] {\n if (typeof text !== \"string\" || text.length === 0) return [];\n const matcher = defaultCitationMatcher();\n const results: ParsedCitation[] = [];\n let match: RegExpExecArray | null;\n while ((match = matcher.exec(text)) !== null) {\n const parsed = parseCitation(match[0]);\n if (parsed) results.push(parsed);\n }\n return results;\n}\n\n/**\n * Remove all inline citations from a piece of text.\n *\n * Callers that want the raw fact body (for dedup hashing, display, or\n * comparison) should use this helper instead of hand-rolled regexes so the\n * whole codebase agrees on the citation syntax.\n *\n * Finding 2 fix: when the input contains no citation marker, the input is\n * returned byte-for-byte unchanged. When a citation IS removed, whitespace\n * normalization is applied only at each join seam (the single space between\n * the preceding text and where the citation was), rather than across the\n * entire string. This preserves markdown hard-break spacing, aligned text,\n * and code-like snippets in fact bodies that happen to carry a citation.\n *\n * Implementation: each citation match is replaced by its \"seam fix\" — the\n * content before the match has its trailing whitespace trimmed and then a\n * single space is appended if any text remains, collapsing only the gap\n * left by the removed marker. Whitespace elsewhere in the body is untouched.\n */\nexport function stripCitation(text: string): string {\n if (typeof text !== \"string\" || text.length === 0) return text;\n // Early exit: no citation marker present — return the input unchanged so\n // that callers never lose formatting fidelity on uncited strings.\n if (!hasCitation(text)) return text;\n\n // Walk through all citations and slice them out one by one so that we can\n // normalise ONLY the whitespace at each seam rather than the entire string.\n const matcher = defaultCitationMatcher();\n let result = \"\";\n let lastIndex = 0;\n\n let match: RegExpExecArray | null;\n while ((match = matcher.exec(text)) !== null) {\n // Text before this citation. Trim trailing spaces/tabs at the seam only.\n const before = text.slice(lastIndex, match.index).replace(/[ \\t]+$/, \"\");\n result += before;\n lastIndex = match.index + match[0].length;\n }\n\n // Append any trailing text after the last citation. Trim leading\n // spaces/tabs and trailing whitespace at the join seam.\n const after = text.slice(lastIndex).replace(/^[ \\t]+/, \"\");\n if (after.length > 0) {\n if (result.length > 0) result += \" \";\n result += after;\n }\n\n return result.trimEnd();\n}\n\n/**\n * Strip an inline citation from text using a specific template regex.\n *\n * This is the template-aware counterpart to {@link stripCitation}. When the\n * caller holds the configured `inlineSourceAttributionFormat`, use this\n * function to strip citations produced by that template — including custom\n * templates that differ from the default `[Source: ...]` pattern.\n *\n * Behaviour:\n * - If the text has a **default-format** citation, delegates to\n * {@link stripCitation} (always safe).\n * - If the text has a **custom-template** citation detected by\n * `hasCitationForTemplate`, builds the template regex and removes every\n * occurrence (citations are appended at the end of the fact body by\n * {@link attachCitation}).\n * - All-placeholder templates (no literal prefix/suffix/separator) cannot\n * produce a reliable matcher. `hasCitationForTemplate` already returns\n * `false` for such templates, so this function never attempts to strip an\n * undetectable citation. The text is returned unchanged when no citation\n * is detected.\n * - If no citation is detected for the given template, returns the text\n * unchanged.\n *\n * @returns The stripped text (or the original text when no citation is found).\n */\nexport function stripCitationForTemplate(\n text: string,\n template: string,\n): string {\n if (typeof text !== \"string\" || text.length === 0) return text;\n\n // Fast path: default-format citation — delegate to the existing stripper.\n if (hasCitation(text)) return stripCitation(text);\n\n // No default citation; check whether the custom template produced one.\n // hasCitationForTemplate returns false for all-placeholder templates (no\n // reliable matcher), so those pass through unchanged below.\n if (!hasCitationForTemplate(text, template)) return text;\n\n // Build the template matcher. hasCitationForTemplate already returned true,\n // which means templateMatcher produced a non-null result. The null branch\n // here is a defensive fallback only — delegate to stripCitation.\n const matcher = templateMatcher(template);\n if (!matcher) return stripCitation(text);\n\n // The matcher regex was built without the global flag; add it for exec loop.\n const globalMatcher = new RegExp(\n matcher.source,\n matcher.flags.includes(\"g\") ? matcher.flags : matcher.flags + \"g\",\n );\n let result = \"\";\n let lastIndex = 0;\n let match: RegExpExecArray | null;\n\n while ((match = globalMatcher.exec(text)) !== null) {\n const before = text.slice(lastIndex, match.index).replace(/[ \\t]+$/, \"\");\n result += before;\n lastIndex = match.index + match[0].length;\n // Guard against zero-width matches causing an infinite loop.\n if (match[0].length === 0) {\n globalMatcher.lastIndex++;\n }\n }\n\n const after = text.slice(lastIndex).replace(/^[ \\t]+/, \"\");\n if (after.length > 0) {\n if (result.length > 0) result += \" \";\n result += after;\n }\n\n return result.trimEnd();\n}\n"],"mappings":"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","names":[]}
@@ -10,7 +10,7 @@ import {
10
10
  } from "./chunk-2ODBA7MQ.js";
11
11
 
12
12
  // src/local-llm.ts
13
- import { existsSync, readFileSync } from "fs";
13
+ import fs from "fs";
14
14
  import os from "os";
15
15
  function trimTrailingSlashes(s) {
16
16
  let end = s.length;
@@ -278,11 +278,11 @@ var LocalLlmClient = class _LocalLlmClient {
278
278
  try {
279
279
  const homeDir = this.resolveHomeDir();
280
280
  const settingsPath = `${homeDir}/.cache/lm-studio/settings.json`;
281
- if (!existsSync(settingsPath)) {
281
+ if (!fs.existsSync(settingsPath)) {
282
282
  log.debug(`LM Studio settings: file not found at ${settingsPath}`);
283
283
  return null;
284
284
  }
285
- const content = readFileSync(settingsPath, "utf-8");
285
+ const content = fs.readFileSync(settingsPath, "utf-8");
286
286
  const settings = JSON.parse(content);
287
287
  if (settings.defaultContextLength?.value) {
288
288
  const contextWindow = settings.defaultContextLength.value;
@@ -310,7 +310,7 @@ var LocalLlmClient = class _LocalLlmClient {
310
310
  "/usr/local/bin/lms",
311
311
  "/opt/homebrew/bin/lms"
312
312
  ];
313
- const lmsPath = lmsPaths.find((p) => p.length > 0 && existsSync(p));
313
+ const lmsPath = lmsPaths.find((p) => p.length > 0 && fs.existsSync(p));
314
314
  if (!lmsPath) {
315
315
  log.debug(`LMS CLI: not found in standard locations (checked: ${lmsPaths.join(", ")})`);
316
316
  return null;
@@ -933,4 +933,4 @@ var LocalLlmClient = class _LocalLlmClient {
933
933
  export {
934
934
  LocalLlmClient
935
935
  };
936
- //# sourceMappingURL=chunk-R2XRID2N.js.map
936
+ //# sourceMappingURL=chunk-NN3LPQ5D.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/local-llm.ts"],"sourcesContent":["import { log } from \"./logger.js\";\nimport type { PluginConfig } from \"./types.js\";\nimport fs from \"node:fs\";\nimport os from \"node:os\";\nimport type { ModelRegistry } from \"./model-registry.js\";\nimport { launchProcessSync } from \"./runtime/child-process.js\";\nimport { mergeEnv, readEnvVar } from \"./runtime/env.js\";\n\n/** Trim trailing slash characters without backtracking regex. */\nfunction trimTrailingSlashes(s: string): string {\n let end = s.length;\n while (end > 0 && s[end - 1] === \"/\") end--;\n return s.substring(0, end);\n}\n\n/**\n * Local LLM client for OpenAI-compatible endpoints (LM Studio, Ollama, MLX, etc.)\n *\n * Based on openclaw-tactician's provider detection patterns for consistency.\n * Provides privacy-preserving, cost-effective LLM operations with\n * graceful fallback to cloud providers when local LLM is unavailable.\n */\nexport type LocalLlmType = \"lmstudio\" | \"ollama\" | \"mlx\" | \"vllm\" | \"generic\";\n\n/**\n * Backends known to honor `chat_template_kwargs: { enable_thinking: false }`\n * on OpenAI-compatible `/v1/chat/completions`. LM Studio and vLLM both\n * forward this field to the jinja chat template, where thinking-capable\n * models (Qwen 3.5, Gemma 4, DeepSeek) suppress reasoning tokens.\n *\n * Strict OpenAI-compatible backends (standard OpenAI, Azure OpenAI, some\n * proxies) reject unknown request fields with 400 — which trips the\n * `localLlm400*` cooldown path. `LocalLlmClient` therefore only injects\n * the kwarg when the detected backend is in this set; unknown / `generic`\n * / `ollama` / `mlx` fail open (no injection, no 400 risk). Issue #548.\n */\nconst THINKING_COMPATIBLE_BACKENDS: ReadonlySet<LocalLlmType> = new Set([\n \"lmstudio\",\n \"vllm\",\n]);\n\ninterface LocalServerConfig {\n type: LocalLlmType;\n defaultPort: number;\n healthEndpoint: string;\n modelsEndpoint: string;\n detectFn: (response: unknown) => boolean;\n}\n\nconst LOCAL_SERVERS: LocalServerConfig[] = [\n {\n type: \"ollama\",\n defaultPort: 11434,\n healthEndpoint: \"/\",\n modelsEndpoint: \"/api/tags\",\n detectFn: (resp) => typeof resp === \"string\" && resp.includes(\"Ollama\"),\n },\n {\n type: \"mlx\",\n defaultPort: 8080,\n healthEndpoint: \"/v1/models\",\n modelsEndpoint: \"/v1/models\",\n detectFn: (resp) =>\n typeof resp === \"object\" &&\n resp !== null &&\n \"data\" in resp &&\n Array.isArray((resp as { data: unknown[] }).data),\n },\n {\n type: \"lmstudio\",\n defaultPort: 1234,\n healthEndpoint: \"/v1/models\",\n modelsEndpoint: \"/v1/models\",\n detectFn: (resp) =>\n typeof resp === \"object\" &&\n resp !== null &&\n \"data\" in resp &&\n Array.isArray((resp as { data: unknown[] }).data),\n },\n {\n type: \"vllm\",\n defaultPort: 8000,\n healthEndpoint: \"/health\",\n modelsEndpoint: \"/v1/models\",\n detectFn: (resp) => resp === \"\" || (typeof resp === \"object\" && resp !== null),\n },\n];\n\nexport interface LocalModelInfo {\n id: string;\n contextWindow?: number;\n maxTokens?: number;\n}\n\nexport type LocalLlmRequestPriority = \"recall-critical\" | \"background\";\n\ninterface LocalLlmChatCompletionOptions {\n temperature?: number;\n maxTokens?: number;\n responseFormat?: { type: string };\n timeoutMs?: number;\n operation?: string;\n priority?: LocalLlmRequestPriority;\n}\n\ninterface LocalLlmQueuedRequest {\n messages: Array<{ role: string; content: string }>;\n options: LocalLlmChatCompletionOptions;\n priority: LocalLlmRequestPriority;\n enqueuedAtMs: number;\n resolve: (value: LocalLlmChatCompletionResult | null) => void;\n}\n\ninterface LocalLlmChatCompletionResult {\n content: string;\n usage?: { promptTokens: number; completionTokens: number; totalTokens: number };\n}\n\nconst LOCAL_LLM_GLOBAL_BACKEND_STATE = \"__openclawEngramLocalLlmBackendState\";\n\ntype LocalLlmBackendState = {\n untilMs: number;\n reason: string;\n};\nexport class LocalLlmClient {\n private config: PluginConfig;\n private isAvailable: boolean | null = null;\n private lastHealthCheck: number = 0;\n private detectedType: LocalLlmType | null = null;\n private cachedModelInfo: LocalModelInfo | null = null;\n private cachedLmsContext: number | null = null;\n private lastLmsCheck: number = 0;\n private consecutive400s: number = 0;\n private cooldownUntilMs: number = 0;\n private modelRegistry?: ModelRegistry;\n private _disableThinking: boolean = false;\n private readonly requestQueues: Record<LocalLlmRequestPriority, LocalLlmQueuedRequest[]> = {\n \"recall-critical\": [],\n background: [],\n };\n private readonly queueProcessing = new Set<LocalLlmRequestPriority>();\n private queueDrainScheduled: boolean = false;\n private static readonly HEALTH_CHECK_INTERVAL_MS = 60000; // 1 minute\n private static readonly LMS_CACHE_INTERVAL_MS = 30000; // 30 seconds\n\n constructor(config: PluginConfig, modelRegistry?: ModelRegistry) {\n this.config = config;\n this.modelRegistry = modelRegistry;\n }\n\n /**\n * Request thinking/reasoning suppression on the next chat completion.\n *\n * When `true`, the client will inject\n * `chat_template_kwargs: { enable_thinking: false }` into the request\n * body — **but only when the detected backend is known to support it**\n * (LM Studio, vLLM; see `THINKING_COMPATIBLE_BACKENDS`). Strict\n * OpenAI-compat backends reject unknown fields with 400; on those the\n * client fails open (thinking runs normally). This is the safe\n * default for Remnic extraction / consolidation: measurable latency\n * win on thinking-capable backends, zero risk on others. Issue #548.\n */\n set disableThinking(value: boolean) {\n this._disableThinking = value;\n }\n\n private resolveHomeDir(): string {\n return this.config.localLlmHomeDir || readEnvVar(\"HOME\") || os.homedir();\n }\n\n private buildRequestHeaders(base: Record<string, string> = {}): Record<string, string> {\n const headers: Record<string, string> = {\n ...base,\n ...(this.config.localLlmHeaders ?? {}),\n };\n if (this.config.localLlmApiKey && this.config.localLlmAuthHeader !== false) {\n headers.Authorization = `Bearer ${this.config.localLlmApiKey}`;\n }\n return headers;\n }\n\n private isAbortError(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const maybe = err as { name?: string; message?: string };\n return (\n maybe.name === \"AbortError\" ||\n maybe.message === \"This operation was aborted\" ||\n maybe.message === \"The operation was aborted\"\n );\n }\n\n /**\n * Set the ModelRegistry for caching detected capabilities\n */\n setModelRegistry(registry: ModelRegistry): void {\n this.modelRegistry = registry;\n }\n\n /**\n * Get the detected server type (null if not detected)\n */\n getDetectedType(): LocalLlmType | null {\n return this.detectedType;\n }\n\n private getBackendKey(): string {\n return trimTrailingSlashes(\n this.config.localLlmUrl.replace(\"localhost\", \"127.0.0.1\"),\n ).replace(/\\/v1$/, \"\");\n }\n\n private getGlobalBackendState(): Map<string, LocalLlmBackendState> {\n const globalAny = globalThis as typeof globalThis & {\n [LOCAL_LLM_GLOBAL_BACKEND_STATE]?: Map<string, LocalLlmBackendState>;\n };\n if (!globalAny[LOCAL_LLM_GLOBAL_BACKEND_STATE]) {\n globalAny[LOCAL_LLM_GLOBAL_BACKEND_STATE] = new Map();\n }\n return globalAny[LOCAL_LLM_GLOBAL_BACKEND_STATE];\n }\n\n private getTrippedBackendState(now: number): LocalLlmBackendState | null {\n const state = this.getGlobalBackendState().get(this.getBackendKey()) ?? null;\n if (!state) return null;\n if (state.untilMs <= now) {\n this.getGlobalBackendState().delete(this.getBackendKey());\n this.lastHealthCheck = 0;\n return null;\n }\n return state;\n }\n\n private markBackendUnavailable(reason: string, durationMs: number): void {\n const normalizedReason = this.normalizeBackendTripReason(reason);\n if (durationMs > 0) {\n const untilMs = Date.now() + durationMs;\n this.getGlobalBackendState().set(this.getBackendKey(), { untilMs, reason: normalizedReason });\n } else {\n this.getGlobalBackendState().delete(this.getBackendKey());\n }\n this.isAvailable = false;\n this.lastHealthCheck = 0;\n log.warn(\n `local LLM backend unavailable for ${durationMs}ms: model=${this.config.localLlmModel} reason=${normalizedReason}`,\n );\n }\n\n private extractNonRecoverableBackendReason(reason: string): string | null {\n const match = reason.match(\n /Failed to load model|Library not loaded|different Team IDs|code signature|llm_engine_mlx_amphibian/i,\n );\n return match?.[0] ?? null;\n }\n\n private extractNonRecoverableBackendReasonFromErrorText(errorText: string): string | null {\n const directReason = this.extractNonRecoverableBackendReason(errorText);\n if (directReason) return directReason;\n try {\n const parsed = JSON.parse(errorText) as { error?: { message?: string } };\n return this.extractNonRecoverableBackendReason(parsed?.error?.message ?? \"\");\n } catch {\n return null;\n }\n }\n\n private normalizeBackendTripReason(reason: string): string {\n const cleaned = reason.replace(/\\s+/g, \" \").replace(/^[-:–—\\s]+/, \"\").trim();\n if (!cleaned) return \"unknown local backend failure\";\n return cleaned.length > 160 ? `${cleaned.slice(0, 157)}...` : cleaned;\n }\n\n /**\n * Fetch with timeout for health checks\n */\n private async fetchWithTimeout(\n url: string,\n timeoutMs: number = 2000,\n headers?: Record<string, string>,\n ): Promise<{ ok: boolean; data: unknown; status: number | null }> {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(url, {\n signal: controller.signal,\n headers: this.buildRequestHeaders({ Accept: \"application/json\", ...(headers ?? {}) }),\n });\n clearTimeout(timeout);\n\n if (!response.ok) {\n return { ok: false, data: null, status: response.status };\n }\n\n const contentType = response.headers.get(\"content-type\");\n if (contentType?.includes(\"application/json\")) {\n return { ok: true, data: await response.json(), status: response.status };\n } else {\n return { ok: true, data: await response.text(), status: response.status };\n }\n } catch (err) {\n clearTimeout(timeout);\n return { ok: false, data: null, status: null };\n }\n }\n\n /**\n * Check if local LLM is available\n * Uses 127.0.0.1 instead of localhost to avoid DNS issues (consistent with tactician)\n */\n async checkAvailability(): Promise<boolean> {\n // Cache health check results for 1 minute\n const now = Date.now();\n const trippedState = this.getTrippedBackendState(now);\n if (trippedState) {\n this.isAvailable = false;\n this.lastHealthCheck = 0;\n log.info(\n `local LLM availability: backend circuit open for ${Math.max(0, trippedState.untilMs - now)}ms (${trippedState.reason})`,\n );\n return false;\n }\n if (this.isAvailable !== null && now - this.lastHealthCheck < LocalLlmClient.HEALTH_CHECK_INTERVAL_MS) {\n return this.isAvailable;\n }\n\n // Normalize URL - replace localhost with 127.0.0.1, remove trailing slashes\n const baseUrl = trimTrailingSlashes(\n this.config.localLlmUrl.replace(\"localhost\", \"127.0.0.1\"),\n );\n let sawUnauthorizedProbe = false;\n\n // Try to detect which server type is running\n for (const serverConfig of LOCAL_SERVERS) {\n const healthUrl = `${baseUrl}${serverConfig.healthEndpoint}`;\n log.debug(`checking ${serverConfig.type} at ${healthUrl}`);\n\n const result = await this.fetchWithTimeout(healthUrl);\n if (result.ok && serverConfig.detectFn(result.data)) {\n this.isAvailable = true;\n this.detectedType = serverConfig.type;\n this.lastHealthCheck = now;\n log.info(`detected ${serverConfig.type} at ${baseUrl}`);\n return true;\n }\n if (result.status === 401 || result.status === 403) {\n sawUnauthorizedProbe = true;\n }\n }\n\n // Generic check if specific detection failed\n try {\n const modelsUrl = `${baseUrl}/v1/models`;\n const result = await this.fetchWithTimeout(modelsUrl);\n if (result.ok) {\n this.isAvailable = true;\n this.detectedType = \"generic\";\n this.lastHealthCheck = now;\n log.info(`detected generic OpenAI-compatible server at ${baseUrl}`);\n return true;\n }\n if (result.status === 401 || result.status === 403) {\n sawUnauthorizedProbe = true;\n }\n } catch {\n // Fall through to unavailable\n }\n\n this.isAvailable = false;\n this.detectedType = null;\n this.lastHealthCheck = now;\n if (sawUnauthorizedProbe) {\n log.warn(\n `local LLM availability probe was unauthorized at ${baseUrl}; verify localLlmApiKey and localLlmAuthHeader settings`,\n );\n }\n log.debug(\"local LLM not available at\", baseUrl);\n return false;\n }\n\n /**\n * Try to get context window from LM Studio settings.json as fallback.\n * This reads the defaultContextLength setting which is what LM Studio uses\n * when loading models without explicit context configuration.\n */\n private getContextFromLmStudioSettings(): number | null {\n try {\n const homeDir = this.resolveHomeDir();\n const settingsPath = `${homeDir}/.cache/lm-studio/settings.json`;\n\n if (!fs.existsSync(settingsPath)) {\n log.debug(`LM Studio settings: file not found at ${settingsPath}`);\n return null;\n }\n\n const content = fs.readFileSync(settingsPath, \"utf-8\");\n const settings = JSON.parse(content) as {\n defaultContextLength?: {\n type?: string;\n value?: number;\n };\n };\n\n if (settings.defaultContextLength?.value) {\n const contextWindow = settings.defaultContextLength.value;\n log.debug(`LM Studio settings: found default context length: ${contextWindow}`);\n return contextWindow;\n }\n\n return null;\n } catch (err) {\n const errorMsg = err instanceof Error ? err.message : String(err);\n log.debug(`LM Studio settings: failed to read - ${errorMsg}`);\n return null;\n }\n }\n\n /**\n * Try to get context window from LMS CLI (LM Studio specific).\n * Uses --json flag for reliable parsing.\n * Returns null if LMS CLI is not available or model not found.\n */\n private getContextFromLmsCli(modelId: string): number | null {\n try {\n // Check if lms CLI exists in common locations.\n // HOME may be absent in launchd environments, so prefer the resolved helper.\n const homeDir = this.resolveHomeDir();\n const lmsPaths = [\n this.config.localLmsCliPath || \"\",\n `${homeDir}/.cache/lm-studio/bin/lms`,\n \"/usr/local/bin/lms\",\n \"/opt/homebrew/bin/lms\",\n ];\n\n const lmsPath = lmsPaths.find((p) => p.length > 0 && fs.existsSync(p));\n if (!lmsPath) {\n log.debug(`LMS CLI: not found in standard locations (checked: ${lmsPaths.join(\", \")})`);\n return null;\n }\n\n // Run lms ps --json to get loaded models with context\n // Use spawnSync with shell and explicit PATH to ensure lms can find its dependencies\n log.debug(`LMS CLI: running: ${lmsPath} ps --json`);\n const existingPath = readEnvVar(\"PATH\") || \"\";\n const result = launchProcessSync(lmsPath, [\"ps\", \"--json\"], {\n encoding: \"utf-8\",\n timeout: 5000,\n shell: false, // Don't use shell for JSON output - more reliable\n env: mergeEnv({\n PATH: `${this.config.localLmsBinDir || `${homeDir}/.cache/lm-studio/bin`}:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:${existingPath}`,\n HOME: homeDir,\n }),\n });\n\n if (result.error) {\n log.debug(`LMS CLI: spawn error - ${result.error.message}`);\n return null;\n }\n\n if (result.stderr && result.stderr.trim()) {\n log.debug(`LMS CLI: stderr - ${result.stderr.slice(0, 200)}`);\n }\n\n const output = result.stdout || \"\";\n if (!output.trim()) {\n log.debug(\"LMS CLI: empty output - LM Studio may not be running or no models loaded\");\n return null;\n }\n\n // Parse JSON output\n let models: Array<{\n identifier?: string;\n modelKey?: string;\n contextLength?: number;\n maxContextLength?: number;\n }>;\n\n try {\n models = JSON.parse(output) as typeof models;\n } catch (parseErr) {\n log.debug(`LMS CLI: JSON parse error - ${parseErr}`);\n return null;\n }\n\n if (!Array.isArray(models) || models.length === 0) {\n log.debug(\"LMS CLI: no models loaded\");\n return null;\n }\n\n // Find the model matching our configured model ID\n const model = models.find((m) =>\n m.identifier === modelId ||\n m.modelKey === modelId ||\n (m.identifier?.includes(modelId.replace(/@\\d+bit$/, \"\")))\n );\n\n if (!model) {\n log.debug(`LMS CLI: model \"${modelId}\" not found in loaded models: ${models.map(m => m.identifier).join(\", \")}`);\n return null;\n }\n\n // Use contextLength (actual configured) or fall back to maxContextLength (model max)\n const contextWindow = model.contextLength || model.maxContextLength;\n\n if (contextWindow) {\n log.info(`LMS CLI detected context window: ${contextWindow} for ${modelId} (max: ${model.maxContextLength})`);\n return contextWindow;\n }\n\n return null;\n } catch (err) {\n // LMS CLI not available or failed\n const errorMsg = err instanceof Error ? err.message : String(err);\n log.debug(`LMS CLI: failed - ${errorMsg}`);\n return null;\n }\n }\n\n /**\n * Get full model info from LMS CLI including context length and max context length.\n * Returns null if LMS CLI is unavailable or model not found.\n */\n private getLmsModelInfo(modelId: string): { contextLength: number; maxContextLength: number; identifier: string } | null {\n try {\n const result = launchProcessSync(\"lms\", [\"ps\", \"--json\"], {\n encoding: \"utf-8\",\n timeout: 5000,\n shell: false,\n });\n\n if (result.error) {\n return null;\n }\n\n const output = result.stdout || \"\";\n if (!output.trim()) {\n return null;\n }\n\n let models: Array<{\n identifier?: string;\n modelKey?: string;\n contextLength?: number;\n maxContextLength?: number;\n }>;\n\n try {\n models = JSON.parse(output) as typeof models;\n } catch {\n return null;\n }\n\n if (!Array.isArray(models) || models.length === 0) {\n return null;\n }\n\n const model = models.find((m) =>\n m.identifier === modelId ||\n m.modelKey === modelId ||\n (m.identifier?.includes(modelId.replace(/@\\d+bit$/, \"\")))\n );\n\n if (!model || !model.contextLength) {\n return null;\n }\n\n return {\n contextLength: model.contextLength,\n maxContextLength: model.maxContextLength || model.contextLength,\n identifier: model.identifier || modelId,\n };\n } catch {\n return null;\n }\n }\n\n /**\n * Get context window for the configured model, using cache if available.\n * This method caches the result to avoid repeated LMS CLI calls.\n * Order: ModelRegistry (persistent) -> memory cache -> LMS CLI -> settings.json\n */\n getCachedContextWindow(modelId: string): number | null {\n const now = Date.now();\n\n // 1. Check ModelRegistry for persisted context window\n if (this.modelRegistry) {\n const caps = this.modelRegistry.getCapabilities(modelId);\n if (caps.source === \"lmstudio\" && caps.contextWindow) {\n log.debug(`ModelRegistry: using persisted LM Studio context: ${caps.contextWindow}`);\n // Also update memory cache\n this.cachedLmsContext = caps.contextWindow;\n this.lastLmsCheck = now;\n return caps.contextWindow;\n }\n }\n\n // 2. Return in-memory cached value if still valid\n if (this.cachedLmsContext && now - this.lastLmsCheck < LocalLlmClient.LMS_CACHE_INTERVAL_MS) {\n log.debug(`LMS CLI: returning in-memory cached context: ${this.cachedLmsContext}`);\n return this.cachedLmsContext;\n }\n\n // 3. Try LMS CLI (authoritative source)\n const lmsInfo = this.getLmsModelInfo(modelId);\n if (lmsInfo?.contextLength) {\n this.cachedLmsContext = lmsInfo.contextLength;\n this.lastLmsCheck = now;\n // Calculate appropriate output tokens based on context size\n // Use 12.5% of context window, capped at 16K (generous but safe)\n const calculatedOutputTokens = Math.min(Math.floor(lmsInfo.contextLength / 8), 16384);\n const outputTokens = Math.max(calculatedOutputTokens, 4096); // Minimum 4K\n // Persist to ModelRegistry with detected capabilities\n if (this.modelRegistry) {\n this.modelRegistry.setCapabilities(modelId, {\n maxPositionEmbeddings: lmsInfo.maxContextLength || lmsInfo.contextLength,\n contextWindow: lmsInfo.contextLength,\n supportsExtendedContext: (lmsInfo.maxContextLength || lmsInfo.contextLength) > 65536,\n typicalOutputTokens: outputTokens,\n source: \"lmstudio\",\n });\n log.info(`LMS CLI: Stored capabilities for ${modelId}: ${lmsInfo.contextLength} context, ${outputTokens} output tokens`);\n }\n return lmsInfo.contextLength;\n }\n\n // Legacy: Try LMS CLI context only (fallback)\n const legacyContext = this.getContextFromLmsCli(modelId);\n if (legacyContext) {\n this.cachedLmsContext = legacyContext;\n this.lastLmsCheck = now;\n // Persist to ModelRegistry with calculated output tokens\n if (this.modelRegistry) {\n const calculatedOutputTokens = Math.min(Math.floor(legacyContext / 8), 16384);\n const outputTokens = Math.max(calculatedOutputTokens, 4096);\n this.modelRegistry.setCapabilities(modelId, {\n maxPositionEmbeddings: legacyContext,\n contextWindow: legacyContext,\n supportsExtendedContext: false,\n typicalOutputTokens: outputTokens,\n source: \"lmstudio\",\n });\n }\n return legacyContext;\n }\n\n // 4. Fall back to LM Studio settings.json\n const settingsContext = this.getContextFromLmStudioSettings();\n if (settingsContext) {\n log.info(`LM Studio settings: using default context: ${settingsContext}`);\n this.cachedLmsContext = settingsContext;\n this.lastLmsCheck = now;\n return settingsContext;\n }\n\n return null;\n }\n\n /**\n * Clear the LMS context cache. Call this when the model changes.\n */\n clearContextCache(): void {\n this.cachedLmsContext = null;\n this.lastLmsCheck = 0;\n log.debug(\"LMS CLI: context cache cleared\");\n }\n\n private remainingCooldownMs(now: number = Date.now()): number {\n return Math.max(0, this.cooldownUntilMs - now);\n }\n\n private scheduleQueueDrain(): void {\n if (this.queueDrainScheduled) return;\n this.queueDrainScheduled = true;\n\n queueMicrotask(() => {\n this.queueDrainScheduled = false;\n this.startAvailableQueuedRequests();\n });\n }\n\n private hasQueuedRequests(): boolean {\n return (\n this.requestQueues[\"recall-critical\"].length > 0 ||\n this.requestQueues.background.length > 0\n );\n }\n\n private dequeueQueuedRequest(priority: LocalLlmRequestPriority): LocalLlmQueuedRequest | null {\n const next = this.requestQueues[priority].shift();\n return next ?? null;\n }\n\n private failOpenQueuedRequestsForCooldown(): number {\n let dropped = 0;\n for (const priority of [\"recall-critical\", \"background\"] as const) {\n while (this.requestQueues[priority].length > 0) {\n const queued = this.requestQueues[priority].shift();\n queued?.resolve(null);\n dropped += 1;\n }\n }\n return dropped;\n }\n\n private startAvailableQueuedRequests(): void {\n if (!this.queueProcessing.has(\"recall-critical\")) {\n const nextCritical = this.dequeueQueuedRequest(\"recall-critical\");\n if (nextCritical) {\n this.queueProcessing.add(\"recall-critical\");\n void this.runQueuedRequest(nextCritical);\n }\n }\n\n if (!this.queueProcessing.has(\"background\")) {\n const nextBackground = this.dequeueQueuedRequest(\"background\");\n if (nextBackground) {\n this.queueProcessing.add(\"background\");\n void this.runQueuedRequest(nextBackground);\n }\n }\n }\n\n private async runQueuedRequest(next: LocalLlmQueuedRequest): Promise<void> {\n try {\n const remainingCooldownMs = this.remainingCooldownMs();\n if (remainingCooldownMs > 0) {\n const additionalDropped = this.failOpenQueuedRequestsForCooldown();\n log.warn(\n `local LLM: cooldown active (${remainingCooldownMs}ms remaining), dropping ${additionalDropped + 1} queued request(s) fail-open`,\n );\n next.resolve(null);\n return;\n }\n\n let result: LocalLlmChatCompletionResult | null = null;\n try {\n result = await this.runChatCompletionRequest(next.messages, next.options, {\n priority: next.priority,\n enqueuedAtMs: next.enqueuedAtMs,\n });\n } catch (err) {\n log.warn(`local LLM queue drain failed open: ${err instanceof Error ? err.message : String(err)}`);\n }\n next.resolve(result);\n } finally {\n this.queueProcessing.delete(next.priority);\n if (this.hasQueuedRequests()) {\n this.scheduleQueueDrain();\n }\n }\n }\n\n private async runChatCompletionRequest(\n messages: Array<{ role: string; content: string }>,\n options: LocalLlmChatCompletionOptions,\n queueMeta?: { priority: LocalLlmRequestPriority; enqueuedAtMs: number },\n ): Promise<LocalLlmChatCompletionResult | null> {\n log.debug(\n `local LLM chatCompletion: localLlmEnabled=${this.config.localLlmEnabled}, model=${this.config.localLlmModel}`,\n );\n\n const operation = options.operation ?? \"unspecified\";\n const startedAtMs = Date.now();\n if (queueMeta) {\n log.debug(\n `local LLM queue start: priority=${queueMeta.priority} waitMs=${startedAtMs - queueMeta.enqueuedAtMs} op=${operation}`,\n );\n }\n\n try {\n const isAvailable = await this.checkAvailability();\n if (!isAvailable) {\n log.debug(\n `local LLM: checkAvailability returned false for ${this.config.localLlmUrl}`,\n );\n return null;\n }\n\n const promptChars = messages.reduce((sum, m) => sum + (m.content?.length ?? 0), 0);\n const requestBody: Record<string, unknown> = {\n model: this.config.localLlmModel,\n messages,\n temperature: options.temperature ?? 0.7,\n // Use max_tokens consistent with cloud models\n max_tokens: options.maxTokens ?? 4096,\n };\n\n // Skip response_format for local LLMs - they don't support json_object type\n // The prompts already instruct the model to output JSON\n // Only send if it's json_schema type which some local LLMs support\n if (options.responseFormat?.type === \"json_schema\") {\n requestBody.response_format = options.responseFormat;\n }\n\n // Suppress thinking/reasoning for thinking-capable models\n // (Qwen 3.5, Gemma 4, DeepSeek). These models default to\n // thinking-on via their chat template; sending\n // `chat_template_kwargs: { enable_thinking: false }` tells the\n // template to skip reasoning tokens.\n //\n // Gate the injection on detected backend support (issue #548,\n // Codex P1 on PR #550): `chat_template_kwargs` is an LM Studio /\n // vLLM / llama.cpp extension, not part of standard OpenAI chat\n // completions. Strict OpenAI-compatible backends reject\n // unknown fields with 400, which trips the 400-cooldown path and\n // can effectively disable local extraction. Fail open when the\n // backend hasn't been positively identified as thinking-capable.\n if (\n this._disableThinking &&\n this.detectedType !== null &&\n THINKING_COMPATIBLE_BACKENDS.has(this.detectedType)\n ) {\n requestBody.chat_template_kwargs = { enable_thinking: false };\n }\n\n // Normalize URL (use 127.0.0.1 instead of localhost)\n const baseUrl = trimTrailingSlashes(\n this.config.localLlmUrl.replace(\"localhost\", \"127.0.0.1\"),\n );\n const chatUrl = baseUrl.endsWith(\"/v1\")\n ? `${baseUrl}/chat/completions`\n : `${baseUrl}/v1/chat/completions`;\n\n const requestBodyJson = JSON.stringify(requestBody);\n log.debug(\n `local LLM: sending request to ${chatUrl} with model ${this.config.localLlmModel}`,\n );\n // Avoid logging request bodies by default (can contain sensitive user content).\n log.debug(`local LLM: request body length=${requestBodyJson.length}`);\n\n // Write request body to file for debugging\n if (this.config.debug) {\n try {\n const { writeFileSync } = await import(\"node:fs\");\n writeFileSync(\"/tmp/engram-last-request.json\", requestBodyJson);\n } catch {\n /* ignore */\n }\n }\n\n const effectiveTimeoutMs =\n typeof options.timeoutMs === \"number\"\n ? Math.min(this.config.localLlmTimeoutMs, options.timeoutMs)\n : this.config.localLlmTimeoutMs;\n const maxAttempts = 1 + Math.max(0, this.config.localLlmRetry5xxCount);\n let response: Response | null = null;\n let lastAbortError: Error | null = null;\n for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {\n const attemptAbort = new AbortController();\n const attemptTimeout = setTimeout(() => attemptAbort.abort(), effectiveTimeoutMs);\n try {\n response = await fetch(chatUrl, {\n method: \"POST\",\n headers: this.buildRequestHeaders({\n \"Content-Type\": \"application/json\",\n }),\n body: JSON.stringify(requestBody),\n signal: attemptAbort.signal,\n });\n } catch (err) {\n if (!this.isAbortError(err)) throw err;\n lastAbortError = err instanceof Error ? err : new Error(String(err));\n if (attempt < maxAttempts) {\n const backoffMs = this.config.localLlmRetryBackoffMs * attempt;\n log.warn(\n `local LLM request aborted: op=${operation} attempt=${attempt}/${maxAttempts} timeoutMs=${effectiveTimeoutMs} model=${this.config.localLlmModel}; retrying after ${backoffMs}ms`,\n );\n await new Promise((resolve) => setTimeout(resolve, backoffMs));\n continue;\n }\n break;\n } finally {\n clearTimeout(attemptTimeout);\n }\n\n if (response.ok) break;\n if (response.status >= 500 && attempt < maxAttempts) {\n try {\n const errorText = await response.clone().text();\n const nonRecoverableReason =\n this.extractNonRecoverableBackendReasonFromErrorText(errorText);\n if (nonRecoverableReason) {\n this.markBackendUnavailable(\n nonRecoverableReason,\n this.config.localLlm400CooldownMs,\n );\n this.consecutive400s = 0;\n return null;\n }\n } catch (e) {\n log.debug(`local LLM failed to inspect retryable error body: ${e}`);\n }\n }\n if (response.status < 500 || attempt >= maxAttempts) break;\n\n const backoffMs = this.config.localLlmRetryBackoffMs * attempt;\n log.warn(\n `local LLM request got ${response.status}; retrying (attempt ${attempt + 1}/${maxAttempts}) after ${backoffMs}ms`,\n );\n await new Promise((resolve) => setTimeout(resolve, backoffMs));\n }\n log.debug(\n `local LLM: received response, status=${response?.status}, ok=${response?.ok}`,\n );\n\n if (!response) {\n if (lastAbortError) {\n log.warn(\n `local LLM request aborted after ${maxAttempts} attempt(s): op=${operation} timeoutMs=${effectiveTimeoutMs} model=${this.config.localLlmModel} promptChars=${promptChars} durationMs=${Date.now() - startedAtMs}`,\n );\n } else {\n log.warn(\n `local LLM request failed: no response object (op=${operation} model=${this.config.localLlmModel} durationMs=${Date.now() - startedAtMs})`,\n );\n }\n return null;\n }\n\n if (!response.ok) {\n let reason = \"\";\n let errorText = \"\";\n try {\n errorText = await response.text();\n // Try to extract a stable error message without logging content.\n try {\n const parsed = JSON.parse(errorText) as { error?: { message?: string } };\n reason = parsed?.error?.message ? ` — ${parsed.error.message}` : \"\";\n } catch {\n // Keep a short preview in debug only.\n log.debug(`local LLM error body: ${errorText.slice(0, 500)}`);\n }\n } catch (e) {\n log.debug(`local LLM failed to read error body: ${e}`);\n }\n log.warn(\n `local LLM request failed: ${response.status} ${response.statusText}${reason} ` +\n `(op=${operation}, model=${this.config.localLlmModel}, url=${chatUrl}, promptChars=${promptChars}, maxTokens=${requestBody.max_tokens as number})`,\n );\n const nonRecoverableReason =\n this.extractNonRecoverableBackendReason(reason) ??\n this.extractNonRecoverableBackendReasonFromErrorText(errorText);\n if (nonRecoverableReason) {\n this.markBackendUnavailable(\n nonRecoverableReason,\n this.config.localLlm400CooldownMs,\n );\n this.consecutive400s = 0;\n return null;\n }\n if (response.status === 400) {\n this.consecutive400s += 1;\n if (this.consecutive400s >= this.config.localLlm400TripThreshold) {\n this.cooldownUntilMs = Date.now() + this.config.localLlm400CooldownMs;\n log.warn(\n `local LLM: entering cooldown for ${this.config.localLlm400CooldownMs}ms ` +\n `after ${this.consecutive400s} consecutive 400 responses`,\n );\n this.consecutive400s = 0;\n }\n } else {\n this.consecutive400s = 0;\n }\n return null;\n }\n this.consecutive400s = 0;\n\n const data = (await response.json()) as {\n choices?: Array<{\n message?: { content?: string; reasoning_content?: string };\n }>;\n usage?: {\n prompt_tokens?: number;\n completion_tokens?: number;\n total_tokens?: number;\n };\n };\n\n log.debug(\n `local LLM response: choices=${data.choices?.length}, usage=${JSON.stringify(data.usage)}`,\n );\n\n // Thinking models (e.g. Qwen 3.5) may put their response in\n // `reasoning_content` and leave `content` empty. Fall back to\n // reasoning_content so engram still gets a usable result.\n const msg = data.choices?.[0]?.message;\n const content = msg?.content || msg?.reasoning_content || \"\";\n if (!content) {\n log.warn(`local LLM returned empty content. choices=${JSON.stringify(data.choices)?.slice(0, 200)}`);\n return null;\n }\n\n // Estimate tokens if not provided by local LLM\n const usage = data.usage\n ? {\n promptTokens: data.usage.prompt_tokens ?? 0,\n completionTokens: data.usage.completion_tokens ?? 0,\n totalTokens: data.usage.total_tokens ?? 0,\n }\n : this.estimateTokens(messages, content);\n\n const durationMs = Date.now() - startedAtMs;\n if (this.config.slowLogEnabled && durationMs >= this.config.slowLogThresholdMs) {\n const promptChars = messages.reduce((sum, m) => sum + (m.content?.length ?? 0), 0);\n const op = options.operation ? ` op=${options.operation}` : \"\";\n log.warn(\n `SLOW local LLM:${op} durationMs=${durationMs} model=${this.config.localLlmModel} url=${chatUrl} promptChars=${promptChars} outputTokens=${usage.completionTokens} totalTokens=${usage.totalTokens}`,\n );\n }\n\n log.debug(\"local LLM: request succeeded, tokens:\", usage.totalTokens);\n return { content, usage };\n } catch (err) {\n const errMsg = err instanceof Error ? err.message : String(err);\n const durationMs = Date.now() - startedAtMs;\n if (this.isAbortError(err)) {\n log.warn(\n `local LLM request aborted: op=${operation} timeoutMs=${options.timeoutMs ?? this.config.localLlmTimeoutMs} model=${this.config.localLlmModel} durationMs=${durationMs} error=${errMsg}`,\n );\n return null;\n }\n log.warn(`local LLM request error: op=${operation} error=${errMsg}`);\n this.isAvailable = false; // Mark as unavailable on non-abort errors\n const nonRecoverableReason = this.extractNonRecoverableBackendReason(errMsg);\n if (nonRecoverableReason) {\n this.markBackendUnavailable(\n nonRecoverableReason,\n this.config.localLlm400CooldownMs,\n );\n }\n return null;\n } finally {\n if (queueMeta) {\n const finishedAtMs = Date.now();\n const waitMs = startedAtMs - queueMeta.enqueuedAtMs;\n log.debug(\n `local LLM queue finish: priority=${queueMeta.priority} waitMs=${waitMs} runMs=${finishedAtMs - startedAtMs} totalMs=${finishedAtMs - queueMeta.enqueuedAtMs} op=${operation}`,\n );\n }\n }\n }\n\n /**\n * Query the local LLM server for loaded model information.\n * Returns null if unavailable or if the model is not found.\n */\n async getLoadedModelInfo(): Promise<LocalModelInfo | null> {\n const baseUrl = trimTrailingSlashes(\n this.config.localLlmUrl.replace(\"localhost\", \"127.0.0.1\"),\n );\n\n // Handle URL construction - localLlmUrl may already include /v1\n const modelsUrl = baseUrl.endsWith(\"/v1\")\n ? `${baseUrl}/models`\n : `${baseUrl}/v1/models`;\n log.debug(`Fetching model info from ${modelsUrl}`);\n\n try {\n const result = await this.fetchWithTimeout(modelsUrl, 3000);\n if (!result.ok) {\n if (result.status === 401 || result.status === 403) {\n log.warn(\n `Local LLM: unauthorized while fetching models from ${modelsUrl}; verify localLlmApiKey and localLlmAuthHeader settings`,\n );\n }\n log.warn(`Local LLM: Failed to fetch models from ${modelsUrl} - server returned error`);\n return null;\n }\n if (!result.data) {\n log.warn(`Local LLM: No data returned from ${modelsUrl}`);\n return null;\n }\n\n const data = result.data as {\n data?: Array<{\n id?: string;\n object?: string;\n owned_by?: string;\n // LM Studio specific fields\n max_context_length?: number;\n max_tokens?: number;\n // Ollama specific\n name?: string;\n details?: {\n parameter_size?: string;\n family?: string;\n };\n }>;\n };\n\n if (!Array.isArray(data.data) || data.data.length === 0) {\n log.warn(\"Local LLM returned no models\");\n return null;\n }\n\n // Verbose model listings are noisy on every gateway restart. Keep it debug-only.\n const modelIds = data.data.map((m) => m.id).filter(Boolean);\n log.debug(\n `Local LLM: Found ${modelIds.length} model(s). First 10: ${modelIds.slice(0, 10).join(\", \")}`,\n );\n\n // Find the model matching our configured model ID\n const configuredModel = this.config.localLlmModel;\n let model = data.data.find((m) => m.id === configuredModel);\n\n // If not found by exact match, try partial match (handle suffixes like @4bit)\n if (!model) {\n model = data.data.find((m) =>\n configuredModel.includes(m.id || \"\") ||\n (m.id || \"\").includes(configuredModel.replace(/@\\d+bit$/, \"\"))\n );\n }\n\n // If still not found, use the first loaded model and warn\n if (!model) {\n model = data.data[0];\n const availablePreview = data.data\n .map((m) => m.id)\n .filter(Boolean)\n .slice(0, 10)\n .join(\", \");\n log.warn(\n `Configured model \"${configuredModel}\" not found in local LLM. ` +\n `Using \"${model.id}\" instead. Available (first 10): ${availablePreview}`\n );\n }\n\n // Extract context window - try multiple field names\n let contextWindow = model.max_context_length || model.max_tokens;\n\n // If API doesn't report context window, try LMS CLI (LM Studio specific)\n if (!contextWindow) {\n log.info(\"Local LLM: API did not report context window, trying LMS CLI...\");\n const lmsContext = this.getCachedContextWindow(model.id || \"\");\n if (lmsContext) {\n contextWindow = lmsContext;\n }\n }\n\n this.cachedModelInfo = {\n id: model.id || \"unknown\",\n contextWindow: contextWindow,\n maxTokens: model.max_tokens,\n };\n\n log.info(\n `Local LLM model detected: ${this.cachedModelInfo.id}, ` +\n `context window: ${contextWindow?.toLocaleString() || \"unknown (may use default)\"}`\n );\n\n return this.cachedModelInfo;\n } catch (err) {\n log.warn(`Failed to fetch model info: ${err}`);\n return null;\n }\n }\n\n /**\n * Check if the configured model is available and get its actual context window.\n * Warns if there's a mismatch between expected and actual context.\n */\n async validateModelConfig(expectedContextWindow?: number): Promise<{\n available: boolean;\n actualContextWindow?: number;\n warnings: string[];\n }> {\n const warnings: string[] = [];\n\n const modelInfo = await this.getLoadedModelInfo();\n if (!modelInfo) {\n return { available: false, warnings: [\"Could not query local LLM for model info\"] };\n }\n\n // If we have expected context and the server reports one, check for mismatch\n if (expectedContextWindow && modelInfo.contextWindow) {\n if (modelInfo.contextWindow < expectedContextWindow) {\n warnings.push(\n `Context window mismatch: Model ${modelInfo.id} supports ${modelInfo.contextWindow.toLocaleString()} tokens, ` +\n `but engram is configured for ${expectedContextWindow.toLocaleString()}. ` +\n `Set localLlmMaxContext: ${modelInfo.contextWindow} in config to avoid errors.`\n );\n }\n }\n\n // Warn if server doesn't report context window (common with some local LLM setups)\n if (!modelInfo.contextWindow) {\n warnings.push(\n `Local LLM server did not report context window for ${modelInfo.id}. ` +\n `If you get \"context length exceeded\" errors, set localLlmMaxContext in config.`\n );\n }\n\n return {\n available: true,\n actualContextWindow: modelInfo.contextWindow,\n warnings,\n };\n }\n\n /**\n * Make a chat completion request to local LLM\n */\n async chatCompletion(\n messages: Array<{ role: string; content: string }>,\n options: LocalLlmChatCompletionOptions = {},\n ): Promise<LocalLlmChatCompletionResult | null> {\n if (!this.config.localLlmEnabled) {\n log.debug(\"local LLM: disabled, returning null\");\n return null;\n }\n\n const remainingMs = this.remainingCooldownMs();\n if (remainingMs > 0) {\n log.debug(`local LLM: cooldown active (${remainingMs}ms remaining), skipping request`);\n return null;\n }\n if (options.priority) {\n const priority = options.priority;\n return await new Promise<LocalLlmChatCompletionResult | null>((resolve) => {\n this.requestQueues[priority].push({\n messages,\n options,\n priority,\n enqueuedAtMs: Date.now(),\n resolve,\n });\n this.scheduleQueueDrain();\n });\n }\n\n return await this.runChatCompletionRequest(messages, options);\n }\n\n /**\n * Estimate tokens when local LLM doesn't return usage stats\n * Rough estimate: 1 token ≈ 4 characters\n */\n private estimateTokens(\n messages: Array<{ role: string; content: string }>,\n response: string\n ): { promptTokens: number; completionTokens: number; totalTokens: number } {\n const promptChars = messages.reduce((sum, m) => sum + m.content.length, 0);\n const promptTokens = Math.ceil(promptChars / 4);\n const completionTokens = Math.ceil(response.length / 4);\n\n return {\n promptTokens,\n completionTokens,\n totalTokens: promptTokens + completionTokens,\n };\n }\n\n /**\n * Try local LLM first, fallback to cloud provider if configured\n */\n async withFallback<T>(\n localOperation: () => Promise<T | null>,\n fallbackOperation: () => Promise<T>,\n operationName: string\n ): Promise<T> {\n // Try local LLM first if enabled\n if (this.config.localLlmEnabled) {\n const localResult = await localOperation();\n if (localResult !== null) {\n log.debug(`${operationName}: used local LLM`);\n return localResult;\n }\n\n // Local failed or unavailable\n if (this.config.localLlmFallback) {\n log.info(`${operationName}: local LLM unavailable, falling back to cloud`);\n } else {\n throw new Error(`${operationName}: local LLM unavailable and fallback disabled`);\n }\n }\n\n // Use fallback (cloud provider)\n return fallbackOperation();\n }\n}\n"],"mappings":"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","names":["backoffMs","promptChars"]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  QmdClient
3
- } from "./chunk-WSZIHQBK.js";
3
+ } from "./chunk-P77UEOU2.js";
4
4
  import {
5
5
  isSafeRouteNamespace
6
6
  } from "./chunk-2LGMW3DJ.js";
@@ -9,7 +9,7 @@ import {
9
9
  } from "./chunk-OR64ZGRZ.js";
10
10
  import {
11
11
  StorageManager
12
- } from "./chunk-XGX4TUF6.js";
12
+ } from "./chunk-MJLUHRSF.js";
13
13
  import {
14
14
  log
15
15
  } from "./chunk-2ODBA7MQ.js";
@@ -44,6 +44,9 @@ var NoopSearchBackend = class {
44
44
  }
45
45
  async updateCollection(_collection, _execution) {
46
46
  }
47
+ updatesAllCollections() {
48
+ return false;
49
+ }
47
50
  async embed() {
48
51
  }
49
52
  async embedCollection(_collection) {
@@ -1813,15 +1816,19 @@ var NamespaceSearchRouter = class {
1813
1816
  */
1814
1817
  async updateNamespaces(namespaces, execution) {
1815
1818
  const unique = Array.from(new Set(namespaces.map((value) => value.trim()).filter(Boolean)));
1816
- const results = await Promise.all(
1819
+ const eligible = (await Promise.all(
1817
1820
  unique.map(async (namespace) => {
1818
1821
  const record = await this.backendRecordFor(namespace);
1819
- if (!record.available || record.collectionState === "missing") return 0;
1820
- await record.backend.update(execution);
1821
- return 1;
1822
+ return record.available && record.collectionState !== "missing" ? record : null;
1822
1823
  })
1823
- );
1824
- return results.reduce((sum, v) => sum + v, 0);
1824
+ )).filter((record) => record !== null);
1825
+ const globalRecord = eligible.find((record) => record.backend.updatesAllCollections?.() === true);
1826
+ const scopedRecords = globalRecord ? eligible.filter((record) => record.backend.updatesAllCollections?.() !== true) : eligible;
1827
+ await Promise.all([
1828
+ globalRecord ? globalRecord.backend.update(execution) : Promise.resolve(),
1829
+ ...scopedRecords.map((record) => record.backend.update(execution))
1830
+ ]);
1831
+ return (globalRecord ? 1 : 0) + scopedRecords.length;
1825
1832
  }
1826
1833
  async embedNamespaces(namespaces) {
1827
1834
  const unique = Array.from(new Set(namespaces.map((value) => value.trim()).filter(Boolean)));
@@ -1904,4 +1911,4 @@ export {
1904
1911
  namespaceCollectionName,
1905
1912
  NamespaceSearchRouter
1906
1913
  };
1907
- //# sourceMappingURL=chunk-RJSVRPNU.js.map
1914
+ //# sourceMappingURL=chunk-OWGGXPKV.js.map