openclaw-hybrid-memory 2026.5.310 → 2026.6.10

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 (535) hide show
  1. package/api/plugin-runtime.ts +2 -0
  2. package/backends/facts-db/contradictions.ts +1 -1
  3. package/cli/cmd-extract-directives.ts +225 -11
  4. package/cli/cmd-extract-proposals.ts +5 -6
  5. package/cli/cmd-extract-reinforcement.ts +71 -0
  6. package/cli/cmd-feedback.ts +15 -9
  7. package/cli/commands/manage/register-reflection-pipeline.ts +247 -13
  8. package/cli/commands/manage/register-storage-maintenance.ts +224 -15
  9. package/cli/commands/manage/storage-stats-helpers.ts +13 -2
  10. package/cli/context.ts +9 -19
  11. package/cli/distill.ts +31 -1
  12. package/cli/register.ts +28 -38
  13. package/dist/api/plugin-runtime.js.map +1 -1
  14. package/dist/backends/agent-health-store.js.map +1 -1
  15. package/dist/backends/apitap-store.js.map +1 -1
  16. package/dist/backends/audit-store.js.map +1 -1
  17. package/dist/backends/base-sqlite-store.js.map +1 -1
  18. package/dist/backends/cost-tracker.js.map +1 -1
  19. package/dist/backends/credentials-db.js +2 -3
  20. package/dist/backends/credentials-db.js.map +1 -1
  21. package/dist/backends/crystallization-store.js.map +1 -1
  22. package/dist/backends/edict-store.js.map +1 -1
  23. package/dist/backends/event-bus.js.map +1 -1
  24. package/dist/backends/event-log.js.map +1 -1
  25. package/dist/backends/facts-db/cache-manager.js.map +1 -1
  26. package/dist/backends/facts-db/clusters.js.map +1 -1
  27. package/dist/backends/facts-db/contradictions.js +1 -1
  28. package/dist/backends/facts-db/contradictions.js.map +1 -1
  29. package/dist/backends/facts-db/crud.js.map +1 -1
  30. package/dist/backends/facts-db/db-connection.js.map +1 -1
  31. package/dist/backends/facts-db/entity-autolink.js.map +1 -1
  32. package/dist/backends/facts-db/entity-layer.js.map +1 -1
  33. package/dist/backends/facts-db/episodes.js.map +1 -1
  34. package/dist/backends/facts-db/fact-queries.js.map +1 -1
  35. package/dist/backends/facts-db/fact-read-queries.js.map +1 -1
  36. package/dist/backends/facts-db/facts-db-layer1.js.map +1 -1
  37. package/dist/backends/facts-db/facts-db-layer2.js.map +1 -1
  38. package/dist/backends/facts-db/facts-db-layer3.js.map +1 -1
  39. package/dist/backends/facts-db/fts-text.js.map +1 -1
  40. package/dist/backends/facts-db/generated-skills/policy.js.map +1 -1
  41. package/dist/backends/facts-db/generated-skills.js.map +1 -1
  42. package/dist/backends/facts-db/housekeeping.js.map +1 -1
  43. package/dist/backends/facts-db/links.js.map +1 -1
  44. package/dist/backends/facts-db/maintenance.js.map +1 -1
  45. package/dist/backends/facts-db/procedures/crud.js.map +1 -1
  46. package/dist/backends/facts-db/procedures/internal.js.map +1 -1
  47. package/dist/backends/facts-db/procedures/promotion.js.map +1 -1
  48. package/dist/backends/facts-db/procedures/search.js.map +1 -1
  49. package/dist/backends/facts-db/procedures/stats.js.map +1 -1
  50. package/dist/backends/facts-db/reinforcement.js.map +1 -1
  51. package/dist/backends/facts-db/row-mapper.js.map +1 -1
  52. package/dist/backends/facts-db/scan-cursors.js.map +1 -1
  53. package/dist/backends/facts-db/schema-bootstrap.js.map +1 -1
  54. package/dist/backends/facts-db/scope-sql.js.map +1 -1
  55. package/dist/backends/facts-db/search.js.map +1 -1
  56. package/dist/backends/facts-db/stats.js.map +1 -1
  57. package/dist/backends/facts-db/types.js.map +1 -1
  58. package/dist/backends/facts-db/variants.js.map +1 -1
  59. package/dist/backends/identity-reflection-store.js.map +1 -1
  60. package/dist/backends/issue-store.js.map +1 -1
  61. package/dist/backends/learnings-db.js.map +1 -1
  62. package/dist/backends/migrations/facts-migrations.js.map +1 -1
  63. package/dist/backends/migrations/procedures.js.map +1 -1
  64. package/dist/backends/narratives-db.js.map +1 -1
  65. package/dist/backends/persona-state-store.js.map +1 -1
  66. package/dist/backends/proposals-db.js.map +1 -1
  67. package/dist/backends/scope-filter-sql.js.map +1 -1
  68. package/dist/backends/sqlite-schema-meta.js.map +1 -1
  69. package/dist/backends/tool-proposal-store.js.map +1 -1
  70. package/dist/backends/vector-db/constants.js.map +1 -1
  71. package/dist/backends/vector-db/path-utils.js.map +1 -1
  72. package/dist/backends/vector-db/runtime-locks.js.map +1 -1
  73. package/dist/backends/vector-db/vector-db-class.js.map +1 -1
  74. package/dist/backends/wal.js.map +1 -1
  75. package/dist/backends/workflow-store.js.map +1 -1
  76. package/dist/benchmark/shadow-eval.js.map +1 -1
  77. package/dist/cli/active-tasks.js.map +1 -1
  78. package/dist/cli/backup.js.map +1 -1
  79. package/dist/cli/benchmark.js.map +1 -1
  80. package/dist/cli/cmd-backfill.js.map +1 -1
  81. package/dist/cli/cmd-config.js.map +1 -1
  82. package/dist/cli/cmd-credentials.js.map +1 -1
  83. package/dist/cli/cmd-demo.js.map +1 -1
  84. package/dist/cli/cmd-distill.js.map +1 -1
  85. package/dist/cli/cmd-doctor.js.map +1 -1
  86. package/dist/cli/cmd-examples.js.map +1 -1
  87. package/dist/cli/cmd-extract-daily.js.map +1 -1
  88. package/dist/cli/cmd-extract-directives.js +141 -10
  89. package/dist/cli/cmd-extract-directives.js.map +1 -1
  90. package/dist/cli/cmd-extract-procedures.js.map +1 -1
  91. package/dist/cli/cmd-extract-proposals.js +3 -2
  92. package/dist/cli/cmd-extract-proposals.js.map +1 -1
  93. package/dist/cli/cmd-extract-reinforcement.js +39 -0
  94. package/dist/cli/cmd-extract-reinforcement.js.map +1 -1
  95. package/dist/cli/cmd-extract-sessions.js.map +1 -1
  96. package/dist/cli/cmd-feedback.js +9 -4
  97. package/dist/cli/cmd-feedback.js.map +1 -1
  98. package/dist/cli/cmd-health.js.map +1 -1
  99. package/dist/cli/cmd-providers.js.map +1 -1
  100. package/dist/cli/cmd-selfcorrection.js.map +1 -1
  101. package/dist/cli/cmd-setup.js.map +1 -1
  102. package/dist/cli/cmd-status.js.map +1 -1
  103. package/dist/cli/cmd-store.js.map +1 -1
  104. package/dist/cli/cmd-user-friendly.js.map +1 -1
  105. package/dist/cli/cmd-verify.js.map +1 -1
  106. package/dist/cli/commands/manage/bindings.js.map +1 -1
  107. package/dist/cli/commands/manage/dream-cycle-followup.js.map +1 -1
  108. package/dist/cli/commands/manage/maintenance-heartbeat.js.map +1 -1
  109. package/dist/cli/commands/manage/register-agents-audit-runall.js.map +1 -1
  110. package/dist/cli/commands/manage/register-analyze-maintenance-logs.js.map +1 -1
  111. package/dist/cli/commands/manage/register-budget-proposals.js.map +1 -1
  112. package/dist/cli/commands/manage/register-config-cli.js.map +1 -1
  113. package/dist/cli/commands/manage/register-corrections-and-pipeline.js.map +1 -1
  114. package/dist/cli/commands/manage/register-corrections.js.map +1 -1
  115. package/dist/cli/commands/manage/register-council.js.map +1 -1
  116. package/dist/cli/commands/manage/register-credentials-scope.js.map +1 -1
  117. package/dist/cli/commands/manage/register-digest.js.map +1 -1
  118. package/dist/cli/commands/manage/register-lifecycle.js.map +1 -1
  119. package/dist/cli/commands/manage/register-procedure-lifecycle.js.map +1 -1
  120. package/dist/cli/commands/manage/register-reconcile-cron-ledgers.js.map +1 -1
  121. package/dist/cli/commands/manage/register-reflection-pipeline.js +144 -7
  122. package/dist/cli/commands/manage/register-reflection-pipeline.js.map +1 -1
  123. package/dist/cli/commands/manage/register-self-correction-feedback.js.map +1 -1
  124. package/dist/cli/commands/manage/register-storage-and-stats.js.map +1 -1
  125. package/dist/cli/commands/manage/register-storage-entities-decay.js.map +1 -1
  126. package/dist/cli/commands/manage/register-storage-graph-audit.js.map +1 -1
  127. package/dist/cli/commands/manage/register-storage-maintenance.js +152 -9
  128. package/dist/cli/commands/manage/register-storage-maintenance.js.map +1 -1
  129. package/dist/cli/commands/manage/register-validate-cron-exit.js.map +1 -1
  130. package/dist/cli/commands/manage/storage-stats-helpers.js +10 -3
  131. package/dist/cli/commands/manage/storage-stats-helpers.js.map +1 -1
  132. package/dist/cli/commands/register-manage-commands.js.map +1 -1
  133. package/dist/cli/config-feature-summaries.js.map +1 -1
  134. package/dist/cli/config-output-sink.js.map +1 -1
  135. package/dist/cli/distill-session-jsonl.js.map +1 -1
  136. package/dist/cli/distill.js +10 -1
  137. package/dist/cli/distill.js.map +1 -1
  138. package/dist/cli/global-verbose.js.map +1 -1
  139. package/dist/cli/goals.js.map +1 -1
  140. package/dist/cli/hybrid-mem-commander-utils.js.map +1 -1
  141. package/dist/cli/install/config-merge.js.map +1 -1
  142. package/dist/cli/install/cron-jobs.js.map +1 -1
  143. package/dist/cli/install/embedding-detect.js.map +1 -1
  144. package/dist/cli/install/run-install.js.map +1 -1
  145. package/dist/cli/install/workspace.js.map +1 -1
  146. package/dist/cli/proposals.js.map +1 -1
  147. package/dist/cli/register.js.map +1 -1
  148. package/dist/cli/shared.js.map +1 -1
  149. package/dist/cli/skills.js.map +1 -1
  150. package/dist/cli/task-queue-status.js.map +1 -1
  151. package/dist/cli/verified.js.map +1 -1
  152. package/dist/cli/verify/fact-count.js.map +1 -1
  153. package/dist/cli/verify/openclaw-config.js.map +1 -1
  154. package/dist/cli/verify/plugin-config-credentials.js.map +1 -1
  155. package/dist/cli/verify/sections/config-cron.js.map +1 -1
  156. package/dist/cli/verify/sections/embeddings.js.map +1 -1
  157. package/dist/cli/verify/sections/infrastructure.js.map +1 -1
  158. package/dist/cli/verify/sections/llm-models.js.map +1 -1
  159. package/dist/cli/verify/sections/reconcile.js.map +1 -1
  160. package/dist/cli/verify/verify-run-state.js.map +1 -1
  161. package/dist/cli/verify-llm-azure-auth.js.map +1 -1
  162. package/dist/cli/verify.js.map +1 -1
  163. package/dist/config/hybrid-schema.js.map +1 -1
  164. package/dist/config/index.js.map +1 -1
  165. package/dist/config/maintenance-fallback-policy.js.map +1 -1
  166. package/dist/config/parsers/capture.js.map +1 -1
  167. package/dist/config/parsers/core.js.map +1 -1
  168. package/dist/config/parsers/features.js.map +1 -1
  169. package/dist/config/parsers/index.js.map +1 -1
  170. package/dist/config/parsers/maintenance.js.map +1 -1
  171. package/dist/config/parsers/retrieval.js.map +1 -1
  172. package/dist/config/parsers/sensors.js.map +1 -1
  173. package/dist/config/skill-sections.js.map +1 -1
  174. package/dist/config/skill-size-limits.js.map +1 -1
  175. package/dist/config/types/agents.js.map +1 -1
  176. package/dist/config/types/bootstrap.js.map +1 -1
  177. package/dist/config/types/core.js.map +1 -1
  178. package/dist/config/types/index.js.map +1 -1
  179. package/dist/config/utils.js.map +1 -1
  180. package/dist/index-help.js.map +1 -1
  181. package/dist/index-testing-exports.js.map +1 -1
  182. package/dist/index.d.ts +1 -1
  183. package/dist/index.js +2 -2
  184. package/dist/index.js.map +1 -1
  185. package/dist/lifecycle/hook-resolution-api.js.map +1 -1
  186. package/dist/lifecycle/hooks.js +0 -1
  187. package/dist/lifecycle/hooks.js.map +1 -1
  188. package/dist/lifecycle/resolve-agent-id.js.map +1 -1
  189. package/dist/lifecycle/session-state.js.map +1 -1
  190. package/dist/lifecycle/stage-active-task.js.map +1 -1
  191. package/dist/lifecycle/stage-auth-failure.js.map +1 -1
  192. package/dist/lifecycle/stage-capture/run-capture.js.map +1 -1
  193. package/dist/lifecycle/stage-capture.js.map +1 -1
  194. package/dist/lifecycle/stage-cleanup.js.map +1 -1
  195. package/dist/lifecycle/stage-credential-hint.js.map +1 -1
  196. package/dist/lifecycle/stage-frustration.js.map +1 -1
  197. package/dist/lifecycle/stage-goal-stewardship.js.map +1 -1
  198. package/dist/lifecycle/stage-goal-subagent.js.map +1 -1
  199. package/dist/lifecycle/stage-injection.js +1 -1
  200. package/dist/lifecycle/stage-injection.js.map +1 -1
  201. package/dist/lifecycle/stage-recall/run-recall.js.map +1 -1
  202. package/dist/lifecycle/stage-recall.js.map +1 -1
  203. package/dist/lifecycle/stage-setup.js.map +1 -1
  204. package/dist/routes/dashboard/collectors.js.map +1 -1
  205. package/dist/routes/dashboard/html.js.map +1 -1
  206. package/dist/routes/dashboard/server.js.map +1 -1
  207. package/dist/routes/dashboard-graph.js.map +1 -1
  208. package/dist/routes/graphql-resolvers.js.map +1 -1
  209. package/dist/routes/graphql-server.js.map +1 -1
  210. package/dist/services/active-task-checkpoint.js.map +1 -1
  211. package/dist/services/active-task-injection.js.map +1 -1
  212. package/dist/services/active-task.js.map +1 -1
  213. package/dist/services/adaptive-catch-up-pacing.js +25 -0
  214. package/dist/services/adaptive-catch-up-pacing.js.map +1 -0
  215. package/dist/services/adaptive-maintenance-llm.js.map +1 -1
  216. package/dist/services/adaptive-model-limits.js.map +1 -1
  217. package/dist/services/ambient-retrieval.js.map +1 -1
  218. package/dist/services/apitap-service.js.map +1 -1
  219. package/dist/services/audit-health-exit-info.js.map +1 -1
  220. package/dist/services/audit-health-json.js.map +1 -1
  221. package/dist/services/auth-failure-detect.js.map +1 -1
  222. package/dist/services/auto-capture.js.map +1 -1
  223. package/dist/services/auto-classifier.js.map +1 -1
  224. package/dist/services/auto-skills-audit.js.map +1 -1
  225. package/dist/services/bootstrap-optional.js.map +1 -1
  226. package/dist/services/bootstrap-priority.js.map +1 -1
  227. package/dist/services/bootstrap.js.map +1 -1
  228. package/dist/services/capture-provenance.js.map +1 -1
  229. package/dist/services/capture-utils.js.map +1 -1
  230. package/dist/services/chat.js +22 -3
  231. package/dist/services/chat.js.map +1 -1
  232. package/dist/services/classification-scope.js.map +1 -1
  233. package/dist/services/classification.js.map +1 -1
  234. package/dist/services/cli-sql-dump.js.map +1 -1
  235. package/dist/services/consolidation.js.map +1 -1
  236. package/dist/services/context-audit.js +1 -1
  237. package/dist/services/context-audit.js.map +1 -1
  238. package/dist/services/context-budget.js.map +1 -1
  239. package/dist/services/context-engine.js.map +1 -1
  240. package/dist/services/contextual-variants.js.map +1 -1
  241. package/dist/services/continuous-verifier.js.map +1 -1
  242. package/dist/services/contradiction-adjudicator.js.map +1 -1
  243. package/dist/services/cost-context.js.map +1 -1
  244. package/dist/services/cost-feature-labels.js.map +1 -1
  245. package/dist/services/credential-migration.js.map +1 -1
  246. package/dist/services/credential-scanner.js.map +1 -1
  247. package/dist/services/credential-validation.js.map +1 -1
  248. package/dist/services/cron-exit-validator.js.map +1 -1
  249. package/dist/services/cron-guard.js.map +1 -1
  250. package/dist/services/cron-job-bash-harness.js +52 -5
  251. package/dist/services/cron-job-bash-harness.js.map +1 -1
  252. package/dist/services/cron-maintenance-reconciler.js +1 -3
  253. package/dist/services/cron-maintenance-reconciler.js.map +1 -1
  254. package/dist/services/cross-agent-learning.js.map +1 -1
  255. package/dist/services/crystallization-proposer.js.map +1 -1
  256. package/dist/services/dedupe-policy.js.map +1 -1
  257. package/dist/services/deprecated-cron-commands.js.map +1 -1
  258. package/dist/services/directive-extract.js.map +1 -1
  259. package/dist/services/document-chunker.js.map +1 -1
  260. package/dist/services/document-grader.js.map +1 -1
  261. package/dist/services/dream-cycle.js.map +1 -1
  262. package/dist/services/embedding-migration.js.map +1 -1
  263. package/dist/services/embedding-registry.js.map +1 -1
  264. package/dist/services/embeddings/chain-provider.js.map +1 -1
  265. package/dist/services/embeddings/factory.js.map +1 -1
  266. package/dist/services/embeddings/fallback-provider.js.map +1 -1
  267. package/dist/services/embeddings/ollama-provider.js.map +1 -1
  268. package/dist/services/embeddings/onnx-provider.js.map +1 -1
  269. package/dist/services/embeddings/openai-provider.js.map +1 -1
  270. package/dist/services/embeddings/shared.js +3 -3
  271. package/dist/services/embeddings/shared.js.map +1 -1
  272. package/dist/services/embeddings/types.js.map +1 -1
  273. package/dist/services/entity-enrichment-adaptive.js +128 -0
  274. package/dist/services/entity-enrichment-adaptive.js.map +1 -0
  275. package/dist/services/entity-enrichment-cli.js +389 -42
  276. package/dist/services/entity-enrichment-cli.js.map +1 -1
  277. package/dist/services/entity-enrichment.js +31 -5
  278. package/dist/services/entity-enrichment.js.map +1 -1
  279. package/dist/services/error-reporter/noisy-errors.js.map +1 -1
  280. package/dist/services/error-reporter/sanitize.js.map +1 -1
  281. package/dist/services/error-reporter.js.map +1 -1
  282. package/dist/services/event-hub-repair.js.map +1 -1
  283. package/dist/services/export-memory.js.map +1 -1
  284. package/dist/services/fact-extraction.js.map +1 -1
  285. package/dist/services/feedback-effectiveness.js.map +1 -1
  286. package/dist/services/find-duplicates.js.map +1 -1
  287. package/dist/services/frustration-detector.js.map +1 -1
  288. package/dist/services/fts-search.js.map +1 -1
  289. package/dist/services/gap-detector.js.map +1 -1
  290. package/dist/services/generated-skill-lifecycle.js.map +1 -1
  291. package/dist/services/generated-skill-validation.js.map +1 -1
  292. package/dist/services/goal-active-task-mirror.js.map +1 -1
  293. package/dist/services/goal-circuit-breaker.js.map +1 -1
  294. package/dist/services/goal-health.js.map +1 -1
  295. package/dist/services/goal-registry.js.map +1 -1
  296. package/dist/services/goal-stewardship-heartbeat.js.map +1 -1
  297. package/dist/services/goal-stewardship-llm-triage.js.map +1 -1
  298. package/dist/services/goal-stewardship-verify-cron.js.map +1 -1
  299. package/dist/services/goal-stewardship.js.map +1 -1
  300. package/dist/services/goal-subagent.js.map +1 -1
  301. package/dist/services/graph-retrieval.js.map +1 -1
  302. package/dist/services/humanizer-score.js.map +1 -1
  303. package/dist/services/hybrid-mem-cron-default-job-steps.js.map +1 -1
  304. package/dist/services/hyde-helper.js.map +1 -1
  305. package/dist/services/identity-reflection.js.map +1 -1
  306. package/dist/services/implicit-feedback-extract.js.map +1 -1
  307. package/dist/services/index.js.map +1 -1
  308. package/dist/services/ingest-utils.js.map +1 -1
  309. package/dist/services/intent-template.js.map +1 -1
  310. package/dist/services/json-array-parser.js.map +1 -1
  311. package/dist/services/knowledge-gaps.js.map +1 -1
  312. package/dist/services/language-keywords-build.js.map +1 -1
  313. package/dist/services/lifecycle/github-adapter.js.map +1 -1
  314. package/dist/services/llm-rate-limit-headers.js +1 -2
  315. package/dist/services/llm-rate-limit-headers.js.map +1 -1
  316. package/dist/services/maintenance-auto-fix.js.map +1 -1
  317. package/dist/services/maintenance-log-analyzer.js +7 -1
  318. package/dist/services/maintenance-log-analyzer.js.map +1 -1
  319. package/dist/services/maintenance-timestamp.js.map +1 -1
  320. package/dist/services/memory-diagnostics.js.map +1 -1
  321. package/dist/services/memory-index.js.map +1 -1
  322. package/dist/services/merge-results.js.map +1 -1
  323. package/dist/services/model-capabilities.js.map +1 -1
  324. package/dist/services/model-pricing.js.map +1 -1
  325. package/dist/services/narrative-recall.js.map +1 -1
  326. package/dist/services/openclaw-session-artifact.js.map +1 -1
  327. package/dist/services/passive-observer.js.map +1 -1
  328. package/dist/services/pattern-detector-hash.js.map +1 -1
  329. package/dist/services/pattern-detector.js.map +1 -1
  330. package/dist/services/pending-autopilot/foundation.js.map +1 -1
  331. package/dist/services/pending-autopilot/redaction.js.map +1 -1
  332. package/dist/services/pending-autopilot/store.js.map +1 -1
  333. package/dist/services/pending-autopilot/types.js.map +1 -1
  334. package/dist/services/pending-digest-autopilot-cron.js.map +1 -1
  335. package/dist/services/pending-digest-autopilot.js.map +1 -1
  336. package/dist/services/pending-review-digest.js.map +1 -1
  337. package/dist/services/persona-proposal-triage.js.map +1 -1
  338. package/dist/services/persona-state-promotion.js.map +1 -1
  339. package/dist/services/post-compaction-recall.js.map +1 -1
  340. package/dist/services/pre-consolidation-flush.js.map +1 -1
  341. package/dist/services/pre-finalization-guard.js.map +1 -1
  342. package/dist/services/procedure-cluster.js.map +1 -1
  343. package/dist/services/procedure-extractor.js.map +1 -1
  344. package/dist/services/procedure-promotion/duplicate-skill-cache.js.map +1 -1
  345. package/dist/services/procedure-promotion-policy.js.map +1 -1
  346. package/dist/services/procedure-selection-metrics.js.map +1 -1
  347. package/dist/services/procedure-skill-eval.js.map +1 -1
  348. package/dist/services/procedure-skill-generator.js.map +1 -1
  349. package/dist/services/procedure-skill-recipe.js.map +1 -1
  350. package/dist/services/procedure-skill-shrink.js.map +1 -1
  351. package/dist/services/procedure-skill-workflow.js.map +1 -1
  352. package/dist/services/provenance.js.map +1 -1
  353. package/dist/services/public-export-bundle.js.map +1 -1
  354. package/dist/services/python-bridge.js.map +1 -1
  355. package/dist/services/query-expander.js.map +1 -1
  356. package/dist/services/query-validator.js.map +1 -1
  357. package/dist/services/recall-pipeline.js.map +1 -1
  358. package/dist/services/recall-timing.js.map +1 -1
  359. package/dist/services/recent-http-attempts.js.map +1 -1
  360. package/dist/services/reflection/shared.js.map +1 -1
  361. package/dist/services/reflection.js.map +1 -1
  362. package/dist/services/reinforcement-extract.js.map +1 -1
  363. package/dist/services/reranker.js.map +1 -1
  364. package/dist/services/responses-adapter.js.map +1 -1
  365. package/dist/services/retrieval-aliases.js.map +1 -1
  366. package/dist/services/retrieval-mode-policy.js.map +1 -1
  367. package/dist/services/retrieval-orchestrator/packing.js.map +1 -1
  368. package/dist/services/retrieval-orchestrator.d.ts +2 -3
  369. package/dist/services/retrieval-orchestrator.js.map +1 -1
  370. package/dist/services/rrf-fusion.js.map +1 -1
  371. package/dist/services/self-correction-extract.js.map +1 -1
  372. package/dist/services/session-observability.js.map +1 -1
  373. package/dist/services/session-pre-filter.js.map +1 -1
  374. package/dist/services/shortest-path.js.map +1 -1
  375. package/dist/services/skill-allowed-tools.js.map +1 -1
  376. package/dist/services/skill-creator-validator.js.map +1 -1
  377. package/dist/services/skill-crystallizer-helpers.js.map +1 -1
  378. package/dist/services/skill-crystallizer.js.map +1 -1
  379. package/dist/services/skill-description-builder.js.map +1 -1
  380. package/dist/services/skill-eval-synthesizer.js.map +1 -1
  381. package/dist/services/skill-examples-builder.js.map +1 -1
  382. package/dist/services/skill-frontmatter.js.map +1 -1
  383. package/dist/services/skill-name-validator.js.map +1 -1
  384. package/dist/services/skill-prompt-injection.js.map +1 -1
  385. package/dist/services/skill-reference-sidecar.js.map +1 -1
  386. package/dist/services/skill-script-bundler.js.map +1 -1
  387. package/dist/services/skill-validator.js.map +1 -1
  388. package/dist/services/startup-memory-attribution.js.map +1 -1
  389. package/dist/services/task-hygiene.js.map +1 -1
  390. package/dist/services/task-ledger/canonical.js.map +1 -1
  391. package/dist/services/task-ledger-facts.js.map +1 -1
  392. package/dist/services/task-queue-leases.js.map +1 -1
  393. package/dist/services/task-queue-watchdog.js.map +1 -1
  394. package/dist/services/tool-effectiveness.js.map +1 -1
  395. package/dist/services/tool-proposer.js.map +1 -1
  396. package/dist/services/tools-md-section.js.map +1 -1
  397. package/dist/services/topic-clusters.js.map +1 -1
  398. package/dist/services/trajectory-tracker.js.map +1 -1
  399. package/dist/services/vector-backend-observability.js.map +1 -1
  400. package/dist/services/vector-lifecycle-audit.js.map +1 -1
  401. package/dist/services/vector-maintenance.js.map +1 -1
  402. package/dist/services/vector-search.js.map +1 -1
  403. package/dist/services/verification-store.js.map +1 -1
  404. package/dist/services/verified-fact-triage.js.map +1 -1
  405. package/dist/services/wal-helpers.js.map +1 -1
  406. package/dist/services/workflow-tracker.js.map +1 -1
  407. package/dist/setup/bootstrap-databases.js.map +1 -1
  408. package/dist/setup/cli-context/cli-services.js.map +1 -1
  409. package/dist/setup/cli-context/help-text.js.map +1 -1
  410. package/dist/setup/cli-context/metadata.js.map +1 -1
  411. package/dist/setup/cli-context/register-cli-with-help.js.map +1 -1
  412. package/dist/setup/cli-context/register-full.js.map +1 -1
  413. package/dist/setup/cli-context/register-help.js.map +1 -1
  414. package/dist/setup/cost-instrumentation.js.map +1 -1
  415. package/dist/setup/hybrid-memory-generation-state.js.map +1 -1
  416. package/dist/setup/hybrid-memory-reload-coordinator.js +13 -13
  417. package/dist/setup/hybrid-memory-reload-coordinator.js.map +1 -1
  418. package/dist/setup/plugin-service.js.map +1 -1
  419. package/dist/setup/provider-router.js.map +1 -1
  420. package/dist/setup/register-context-engine.js.map +1 -1
  421. package/dist/setup/register-hooks.js.map +1 -1
  422. package/dist/setup/register-plugin.js +25 -21
  423. package/dist/setup/register-plugin.js.map +1 -1
  424. package/dist/setup/register-tools.js.map +1 -1
  425. package/dist/setup/reregister-policy.js +2 -2
  426. package/dist/setup/reregister-policy.js.map +1 -1
  427. package/dist/setup/tool-installers.js.map +1 -1
  428. package/dist/setup/workspace-bootstrap.js.map +1 -1
  429. package/dist/src/worker/narratives.js.map +1 -1
  430. package/dist/tools/apitap-tools.js.map +1 -1
  431. package/dist/tools/credential-tools.js.map +1 -1
  432. package/dist/tools/crystallization-tools.js.map +1 -1
  433. package/dist/tools/dashboard-routes.js.map +1 -1
  434. package/dist/tools/document-tools.js.map +1 -1
  435. package/dist/tools/goal-tools.js.map +1 -1
  436. package/dist/tools/graph-tools.js.map +1 -1
  437. package/dist/tools/issue-tools.js.map +1 -1
  438. package/dist/tools/memory/build-runtime.js.map +1 -1
  439. package/dist/tools/memory/helpers.js.map +1 -1
  440. package/dist/tools/memory/register-checkpoint-tools.js.map +1 -1
  441. package/dist/tools/memory/register-directory-tools.js.map +1 -1
  442. package/dist/tools/memory/register-edict-tools.js.map +1 -1
  443. package/dist/tools/memory/register-episode-tools.js.map +1 -1
  444. package/dist/tools/memory/register-recall-tools.js.map +1 -1
  445. package/dist/tools/memory/register-store-tools.js.map +1 -1
  446. package/dist/tools/memory-tools.js.map +1 -1
  447. package/dist/tools/persona-tools.js.map +1 -1
  448. package/dist/tools/provenance-tools.js.map +1 -1
  449. package/dist/tools/public-api-routes.js.map +1 -1
  450. package/dist/tools/safe-register-http-route.js.map +1 -1
  451. package/dist/tools/self-extension-tools.js.map +1 -1
  452. package/dist/tools/task-hygiene-tools.js.map +1 -1
  453. package/dist/tools/utility-tools.js.map +1 -1
  454. package/dist/tools/verification-tools.js.map +1 -1
  455. package/dist/tools/workflow-tools.js.map +1 -1
  456. package/dist/types/issue-types.js.map +1 -1
  457. package/dist/types/learnings-types.js.map +1 -1
  458. package/dist/types/memory.js.map +1 -1
  459. package/dist/utils/apim-gateway-fetch.js.map +1 -1
  460. package/dist/utils/atomic-write.js.map +1 -1
  461. package/dist/utils/auth-failover.js.map +1 -1
  462. package/dist/utils/auth.js.map +1 -1
  463. package/dist/utils/compaction-model-watchdog.js.map +1 -1
  464. package/dist/utils/consolidation-controls.js.map +1 -1
  465. package/dist/utils/constants.js.map +1 -1
  466. package/dist/utils/date-detector.js.map +1 -1
  467. package/dist/utils/dates.js.map +1 -1
  468. package/dist/utils/decay.js.map +1 -1
  469. package/dist/utils/duration.js.map +1 -1
  470. package/dist/utils/embed-call.js.map +1 -1
  471. package/dist/utils/entity-lookup-resolve.js.map +1 -1
  472. package/dist/utils/entity-mention-quality.js.map +1 -1
  473. package/dist/utils/entity-stopwords.js.map +1 -1
  474. package/dist/utils/env-manager.js.map +1 -1
  475. package/dist/utils/error-tracking.js.map +1 -1
  476. package/dist/utils/event-loop-yield.js.map +1 -1
  477. package/dist/utils/extract-last-user-message.js.map +1 -1
  478. package/dist/utils/extraction-from-template.js.map +1 -1
  479. package/dist/utils/fact-embeddings.js.map +1 -1
  480. package/dist/utils/file-snapshot.js.map +1 -1
  481. package/dist/utils/format.js.map +1 -1
  482. package/dist/utils/fs.js.map +1 -1
  483. package/dist/utils/gh-repo-arg.js.map +1 -1
  484. package/dist/utils/hybrid-mem-json-cli.js.map +1 -1
  485. package/dist/utils/language-keywords.js.map +1 -1
  486. package/dist/utils/lifecycle-generation.js.map +1 -1
  487. package/dist/utils/llm-json-array.js.map +1 -1
  488. package/dist/utils/llm-selection.js.map +1 -1
  489. package/dist/utils/logger.js.map +1 -1
  490. package/dist/utils/model-provider-family.js.map +1 -1
  491. package/dist/utils/model-tier.js.map +1 -1
  492. package/dist/utils/openclaw-agent-defaults.js.map +1 -1
  493. package/dist/utils/path.js.map +1 -1
  494. package/dist/utils/plugin-root.js.map +1 -1
  495. package/dist/utils/plugin-update-check.js.map +1 -1
  496. package/dist/utils/procedure-risk.js.map +1 -1
  497. package/dist/utils/progress-indicators.js.map +1 -1
  498. package/dist/utils/prompt-loader.js.map +1 -1
  499. package/dist/utils/provenance.js.map +1 -1
  500. package/dist/utils/provider-detection.js.map +1 -1
  501. package/dist/utils/registration-superseded.js.map +1 -1
  502. package/dist/utils/salience.js.map +1 -1
  503. package/dist/utils/sanitize-messages.js.map +1 -1
  504. package/dist/utils/scope-filter.js.map +1 -1
  505. package/dist/utils/skill-discovery.js.map +1 -1
  506. package/dist/utils/sqlite-file-perms.js.map +1 -1
  507. package/dist/utils/sqlite-outcome-compat.js.map +1 -1
  508. package/dist/utils/sqlite-transaction.js.map +1 -1
  509. package/dist/utils/stable-stringify.js.map +1 -1
  510. package/dist/utils/subagent-ended-utils.js.map +1 -1
  511. package/dist/utils/tags.js.map +1 -1
  512. package/dist/utils/text.js.map +1 -1
  513. package/dist/utils/timeout.js.map +1 -1
  514. package/dist/utils/typebox.js.map +1 -1
  515. package/dist/utils/version-check.js.map +1 -1
  516. package/dist/utils/wal-replay.js.map +1 -1
  517. package/dist/versionInfo.js.map +1 -1
  518. package/index.ts +2 -2
  519. package/lifecycle/hooks.ts +0 -1
  520. package/npm-shrinkwrap.json +487 -186
  521. package/openclaw.plugin.json +1 -1
  522. package/package.json +2 -2
  523. package/services/adaptive-catch-up-pacing.ts +28 -0
  524. package/services/chat.ts +34 -1
  525. package/services/cron-job-bash-harness.ts +52 -5
  526. package/services/embeddings/shared.ts +5 -2
  527. package/services/entity-enrichment-adaptive.ts +245 -0
  528. package/services/entity-enrichment-cli.ts +553 -47
  529. package/services/entity-enrichment.ts +43 -2
  530. package/services/llm-rate-limit-headers.ts +1 -4
  531. package/services/maintenance-log-analyzer.ts +13 -9
  532. package/services/reinforcement-extract.ts +19 -0
  533. package/setup/hybrid-memory-reload-coordinator.ts +26 -0
  534. package/setup/register-plugin.ts +62 -32
  535. package/setup/reregister-policy.ts +10 -5
@@ -1 +1 @@
1
- {"version":3,"file":"capture-utils.js","names":[],"sources":["../../services/capture-utils.ts"],"sourcesContent":["/**\n * Utilities for auto-capture filtering and category detection.\n */\n\nimport type { MemoryCategory } from \"../types/memory.js\";\nimport { CAPTURE_FILTER_PATTERNS } from \"./auto-capture.js\";\n\n/**\n * Returns true if the text is a memory-classifier artifact or LLM reasoning trace.\n * These should never be stored as memory facts.\n * See: https://github.com/markus-lassfolk/openclaw-hybrid-memory/issues/1560\n */\nexport function isPromptArtifactOrReasoningTrace(text: string): boolean {\n const trimmed = text.trim();\n // Chain-of-thought / reasoning trace markers\n if (trimmed.startsWith(\"<think>\")) return true;\n if (/^<thinking>[\\s\\S]*<\\/thinking>$/i.test(trimmed)) return true;\n if (/^think\\s*/i.test(trimmed) && /thinking\\s*process/i.test(trimmed)) return true;\n if (/^thinking\\s*process[:#]/i.test(trimmed)) return true;\n if (/^the\\s+user\\s+is\\s+(asking|requesting)\\s+me\\s+to\\s+(classify|extract)/i.test(trimmed)) return true;\n // Classifier output markers (NOOP |, ADD |, UPDATE |, DELETE |)\n if (/^(ADD|UPDATE|DELETE|NOOP)(?:\\s*[a-f0-9-]+)?\\s*\\|\\s*(?:$|[^|])/i.test(trimmed)) return true;\n // Classifier JSON output: single object or array of objects with an \"action\" field\n // To avoid false positives, require additional classifier-specific fields like \"reason\" or \"targetId\"\n try {\n if (/^[[{]/.test(trimmed) && /\"action\"\\s*:/i.test(trimmed)) {\n const parsed = JSON.parse(trimmed);\n if (Array.isArray(parsed)) {\n if (parsed.length > 0 && parsed[0] && typeof parsed[0] === \"object\" && \"action\" in parsed[0]) {\n // Require classifier-specific fields to avoid matching legitimate action logs\n const first = parsed[0] as Record<string, unknown>;\n if (\"reason\" in first || \"targetId\" in first || \"confidence\" in first) {\n return true;\n }\n }\n } else if (parsed && typeof parsed === \"object\" && \"action\" in parsed) {\n // Require classifier-specific fields to avoid matching legitimate action logs\n if (\"reason\" in parsed || \"targetId\" in parsed || \"confidence\" in parsed) {\n return true;\n }\n }\n }\n } catch {\n // Not JSON\n }\n // Memory-hybrid capability hints (issue #1560)\n if (trimmed.startsWith(\"<!-- \") && trimmed.includes(\"memory-hybrid:\")) return true;\n return false;\n}\n\nexport function shouldCapture(text: string, captureMaxChars: number, memoryTriggers: RegExp[]): boolean {\n if (text.length < 10 || text.length > captureMaxChars) return false;\n if (isPromptArtifactOrReasoningTrace(text)) return false;\n if (text.includes(\"<relevant-memories>\")) return false;\n if (text.startsWith(\"<\") && text.includes(\"</\")) return false;\n if (text.includes(\"**\") && text.includes(\"\\n-\")) return false;\n const emojiCount = (text.match(/[\\u{1F300}-\\u{1F9FF}]/gu) || []).length;\n if (emojiCount > 3) return false;\n if (CAPTURE_FILTER_PATTERNS.some((r) => r.test(text))) return false;\n return memoryTriggers.some((r) => r.test(text));\n}\n\nexport function detectCategory(\n text: string,\n categoryDecisionRegex: RegExp,\n categoryPreferenceRegex: RegExp,\n categoryEntityRegex: RegExp,\n categoryFactRegex: RegExp,\n): MemoryCategory {\n const lower = text.toLowerCase();\n if (categoryDecisionRegex.test(lower)) return \"decision\";\n if (categoryPreferenceRegex.test(lower)) return \"preference\";\n if (/\\+\\d{10,}|@[\\w.-]+\\.\\w+/.test(lower) || categoryEntityRegex.test(lower)) return \"entity\";\n if (categoryFactRegex.test(lower)) return \"fact\";\n return \"other\";\n}\n"],"mappings":";;;;;;;AAYA,SAAgB,iCAAiC,MAAuB;CACtE,MAAM,UAAU,KAAK,MAAM;CAE3B,IAAI,QAAQ,WAAW,UAAU,EAAE,OAAO;CAC1C,IAAI,mCAAmC,KAAK,QAAQ,EAAE,OAAO;CAC7D,IAAI,aAAa,KAAK,QAAQ,IAAI,sBAAsB,KAAK,QAAQ,EAAE,OAAO;CAC9E,IAAI,2BAA2B,KAAK,QAAQ,EAAE,OAAO;CACrD,IAAI,yEAAyE,KAAK,QAAQ,EAAE,OAAO;CAEnG,IAAI,iEAAiE,KAAK,QAAQ,EAAE,OAAO;CAG3F,IAAI;EACF,IAAI,QAAQ,KAAK,QAAQ,IAAI,gBAAgB,KAAK,QAAQ,EAAE;GAC1D,MAAM,SAAS,KAAK,MAAM,QAAQ;GAClC,IAAI,MAAM,QAAQ,OAAO;QACnB,OAAO,SAAS,KAAK,OAAO,MAAM,OAAO,OAAO,OAAO,YAAY,YAAY,OAAO,IAAI;KAE5F,MAAM,QAAQ,OAAO;KACrB,IAAI,YAAY,SAAS,cAAc,SAAS,gBAAgB,OAC9D,OAAO;;UAGN,IAAI,UAAU,OAAO,WAAW,YAAY,YAAY;QAEzD,YAAY,UAAU,cAAc,UAAU,gBAAgB,QAChE,OAAO;;;SAIP;CAIR,IAAI,QAAQ,WAAW,QAAQ,IAAI,QAAQ,SAAS,iBAAiB,EAAE,OAAO;CAC9E,OAAO;;AAGT,SAAgB,cAAc,MAAc,iBAAyB,gBAAmC;CACtG,IAAI,KAAK,SAAS,MAAM,KAAK,SAAS,iBAAiB,OAAO;CAC9D,IAAI,iCAAiC,KAAK,EAAE,OAAO;CACnD,IAAI,KAAK,SAAS,sBAAsB,EAAE,OAAO;CACjD,IAAI,KAAK,WAAW,IAAI,IAAI,KAAK,SAAS,KAAK,EAAE,OAAO;CACxD,IAAI,KAAK,SAAS,KAAK,IAAI,KAAK,SAAS,MAAM,EAAE,OAAO;CAExD,KADoB,KAAK,MAAM,0BAA0B,IAAI,EAAE,EAAE,SAChD,GAAG,OAAO;CAC3B,IAAI,wBAAwB,MAAM,MAAM,EAAE,KAAK,KAAK,CAAC,EAAE,OAAO;CAC9D,OAAO,eAAe,MAAM,MAAM,EAAE,KAAK,KAAK,CAAC;;AAGjD,SAAgB,eACd,MACA,uBACA,yBACA,qBACA,mBACgB;CAChB,MAAM,QAAQ,KAAK,aAAa;CAChC,IAAI,sBAAsB,KAAK,MAAM,EAAE,OAAO;CAC9C,IAAI,wBAAwB,KAAK,MAAM,EAAE,OAAO;CAChD,IAAI,0BAA0B,KAAK,MAAM,IAAI,oBAAoB,KAAK,MAAM,EAAE,OAAO;CACrF,IAAI,kBAAkB,KAAK,MAAM,EAAE,OAAO;CAC1C,OAAO"}
1
+ {"version":3,"file":"capture-utils.js","names":[],"sources":["../../services/capture-utils.ts"],"sourcesContent":["/**\n * Utilities for auto-capture filtering and category detection.\n */\n\nimport type { MemoryCategory } from \"../types/memory.js\";\nimport { CAPTURE_FILTER_PATTERNS } from \"./auto-capture.js\";\n\n/**\n * Returns true if the text is a memory-classifier artifact or LLM reasoning trace.\n * These should never be stored as memory facts.\n * See: https://github.com/markus-lassfolk/openclaw-hybrid-memory/issues/1560\n */\nexport function isPromptArtifactOrReasoningTrace(text: string): boolean {\n const trimmed = text.trim();\n // Chain-of-thought / reasoning trace markers\n if (trimmed.startsWith(\"<think>\")) return true;\n if (/^<thinking>[\\s\\S]*<\\/thinking>$/i.test(trimmed)) return true;\n if (/^think\\s*/i.test(trimmed) && /thinking\\s*process/i.test(trimmed)) return true;\n if (/^thinking\\s*process[:#]/i.test(trimmed)) return true;\n if (/^the\\s+user\\s+is\\s+(asking|requesting)\\s+me\\s+to\\s+(classify|extract)/i.test(trimmed)) return true;\n // Classifier output markers (NOOP |, ADD |, UPDATE |, DELETE |)\n if (/^(ADD|UPDATE|DELETE|NOOP)(?:\\s*[a-f0-9-]+)?\\s*\\|\\s*(?:$|[^|])/i.test(trimmed)) return true;\n // Classifier JSON output: single object or array of objects with an \"action\" field\n // To avoid false positives, require additional classifier-specific fields like \"reason\" or \"targetId\"\n try {\n if (/^[[{]/.test(trimmed) && /\"action\"\\s*:/i.test(trimmed)) {\n const parsed = JSON.parse(trimmed);\n if (Array.isArray(parsed)) {\n if (parsed.length > 0 && parsed[0] && typeof parsed[0] === \"object\" && \"action\" in parsed[0]) {\n // Require classifier-specific fields to avoid matching legitimate action logs\n const first = parsed[0] as Record<string, unknown>;\n if (\"reason\" in first || \"targetId\" in first || \"confidence\" in first) {\n return true;\n }\n }\n } else if (parsed && typeof parsed === \"object\" && \"action\" in parsed) {\n // Require classifier-specific fields to avoid matching legitimate action logs\n if (\"reason\" in parsed || \"targetId\" in parsed || \"confidence\" in parsed) {\n return true;\n }\n }\n }\n } catch {\n // Not JSON\n }\n // Memory-hybrid capability hints (issue #1560)\n if (trimmed.startsWith(\"<!-- \") && trimmed.includes(\"memory-hybrid:\")) return true;\n return false;\n}\n\nexport function shouldCapture(text: string, captureMaxChars: number, memoryTriggers: RegExp[]): boolean {\n if (text.length < 10 || text.length > captureMaxChars) return false;\n if (isPromptArtifactOrReasoningTrace(text)) return false;\n if (text.includes(\"<relevant-memories>\")) return false;\n if (text.startsWith(\"<\") && text.includes(\"</\")) return false;\n if (text.includes(\"**\") && text.includes(\"\\n-\")) return false;\n const emojiCount = (text.match(/[\\u{1F300}-\\u{1F9FF}]/gu) || []).length;\n if (emojiCount > 3) return false;\n if (CAPTURE_FILTER_PATTERNS.some((r) => r.test(text))) return false;\n return memoryTriggers.some((r) => r.test(text));\n}\n\nexport function detectCategory(\n text: string,\n categoryDecisionRegex: RegExp,\n categoryPreferenceRegex: RegExp,\n categoryEntityRegex: RegExp,\n categoryFactRegex: RegExp,\n): MemoryCategory {\n const lower = text.toLowerCase();\n if (categoryDecisionRegex.test(lower)) return \"decision\";\n if (categoryPreferenceRegex.test(lower)) return \"preference\";\n if (/\\+\\d{10,}|@[\\w.-]+\\.\\w+/.test(lower) || categoryEntityRegex.test(lower)) return \"entity\";\n if (categoryFactRegex.test(lower)) return \"fact\";\n return \"other\";\n}\n"],"mappings":";;;;;;;AAYA,SAAgB,iCAAiC,MAAuB;CACtE,MAAM,UAAU,KAAK,KAAK;CAE1B,IAAI,QAAQ,WAAW,SAAS,GAAG,OAAO;CAC1C,IAAI,mCAAmC,KAAK,OAAO,GAAG,OAAO;CAC7D,IAAI,aAAa,KAAK,OAAO,KAAK,sBAAsB,KAAK,OAAO,GAAG,OAAO;CAC9E,IAAI,2BAA2B,KAAK,OAAO,GAAG,OAAO;CACrD,IAAI,yEAAyE,KAAK,OAAO,GAAG,OAAO;CAEnG,IAAI,iEAAiE,KAAK,OAAO,GAAG,OAAO;CAG3F,IAAI;EACF,IAAI,QAAQ,KAAK,OAAO,KAAK,gBAAgB,KAAK,OAAO,GAAG;GAC1D,MAAM,SAAS,KAAK,MAAM,OAAO;GACjC,IAAI,MAAM,QAAQ,MAAM;QAClB,OAAO,SAAS,KAAK,OAAO,MAAM,OAAO,OAAO,OAAO,YAAY,YAAY,OAAO,IAAI;KAE5F,MAAM,QAAQ,OAAO;KACrB,IAAI,YAAY,SAAS,cAAc,SAAS,gBAAgB,OAC9D,OAAO;IAEX;UACK,IAAI,UAAU,OAAO,WAAW,YAAY,YAAY;QAEzD,YAAY,UAAU,cAAc,UAAU,gBAAgB,QAChE,OAAO;GAAA;EAGb;CACF,QAAQ,CAER;CAEA,IAAI,QAAQ,WAAW,OAAO,KAAK,QAAQ,SAAS,gBAAgB,GAAG,OAAO;CAC9E,OAAO;AACT;AAEA,SAAgB,cAAc,MAAc,iBAAyB,gBAAmC;CACtG,IAAI,KAAK,SAAS,MAAM,KAAK,SAAS,iBAAiB,OAAO;CAC9D,IAAI,iCAAiC,IAAI,GAAG,OAAO;CACnD,IAAI,KAAK,SAAS,qBAAqB,GAAG,OAAO;CACjD,IAAI,KAAK,WAAW,GAAG,KAAK,KAAK,SAAS,IAAI,GAAG,OAAO;CACxD,IAAI,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS,KAAK,GAAG,OAAO;CAExD,KADoB,KAAK,MAAM,yBAAyB,KAAK,CAAC,GAAG,SAChD,GAAG,OAAO;CAC3B,IAAI,wBAAwB,MAAM,MAAM,EAAE,KAAK,IAAI,CAAC,GAAG,OAAO;CAC9D,OAAO,eAAe,MAAM,MAAM,EAAE,KAAK,IAAI,CAAC;AAChD;AAEA,SAAgB,eACd,MACA,uBACA,yBACA,qBACA,mBACgB;CAChB,MAAM,QAAQ,KAAK,YAAY;CAC/B,IAAI,sBAAsB,KAAK,KAAK,GAAG,OAAO;CAC9C,IAAI,wBAAwB,KAAK,KAAK,GAAG,OAAO;CAChD,IAAI,0BAA0B,KAAK,KAAK,KAAK,oBAAoB,KAAK,KAAK,GAAG,OAAO;CACrF,IAAI,kBAAkB,KAAK,KAAK,GAAG,OAAO;CAC1C,OAAO;AACT"}
@@ -119,6 +119,12 @@ function is429OrWrapped(err) {
119
119
  if (err instanceof LLMRetryError && is429Like(err.cause)) return true;
120
120
  return false;
121
121
  }
122
+ /** Returns true when the error is a ByteString serialization failure — direct or wrapped in LLMRetryError. */
123
+ function isByteStringOrWrapped(err) {
124
+ if (isByteStringSerializationError(err)) return true;
125
+ if (err instanceof LLMRetryError && isByteStringSerializationError(err.cause)) return true;
126
+ return false;
127
+ }
122
128
  /**
123
129
  * Unified 5xx / internal server error detection helper.
124
130
  * Checks HTTP status code property first, then uses conservative message patterns.
@@ -220,6 +226,16 @@ function is400EmptyBodyGatewayError(err) {
220
226
  if (!(err instanceof Error)) return false;
221
227
  return /\b400\b.*\bno body\b/i.test(err.message);
222
228
  }
229
+ /**
230
+ * True when the error is a Node.js / browser-fetch ByteString serialization failure.
231
+ * Thrown by the HTTP layer when a header value contains a character with code point > 255
232
+ * (e.g. U+2026 HORIZONTAL ELLIPSIS in a copy-pasted API key, model name, or custom header).
233
+ * These errors are permanent — retrying the same request will always fail (#1776).
234
+ */
235
+ function isByteStringSerializationError(err) {
236
+ if (!(err instanceof Error)) return false;
237
+ return /cannot convert argument to a bytestring/i.test(err.message);
238
+ }
223
239
  /** Some Azure/Foundry deployments return 400 when chat params are not supported for the model (#1165). */
224
240
  function is400UnsupportedOperationError(err) {
225
241
  if (!(err instanceof Error)) return false;
@@ -330,7 +346,7 @@ async function chatComplete(opts) {
330
346
  }) : new Error(String(err));
331
347
  const msg = error.message.toLowerCase();
332
348
  const isTransient = msg.includes("request was aborted") || msg.includes("operation was aborted") || msg.includes("request timed out") || msg.includes("timed out") || msg.includes("llm request timeout") || msg.includes("econnrefused") || isAbortOrTransientLlmError(err) || isConnectionErrorLike(err) || is429Like(err) || is403QuotaOrRateLimitLike(err) || /^\d+\s*internal\s*error$/i.test(msg.trim()) || /^5\d{2}\s/.test(msg.trim()) || is500Like(err) || isOllamaOOM(err) || isResponsesReasoningSequenceError(err) || is400EmptyBodyGatewayError(error) || is400EmptyBodyGatewayError(err) || is400UnsupportedOperationError(error) || is400UnsupportedOperationError(err);
333
- const isConfigError = err instanceof UnconfiguredProviderError || is404Like(err) || is403Like(err) || is401Like(err) || isContextLengthError(err);
349
+ const isConfigError = err instanceof UnconfiguredProviderError || is404Like(err) || is403Like(err) || is401Like(err) || isContextLengthError(err) || isByteStringSerializationError(err);
334
350
  if (!isTransient && !isConfigError) capturePluginError(error, {
335
351
  subsystem: "chat",
336
352
  operation: "chatComplete",
@@ -407,6 +423,7 @@ async function withLLMRetry(fn, opts) {
407
423
  pluginLogger.warn("memory-hybrid: Input exceeds model context length — retrying will not help; truncate input before calling");
408
424
  throw enrichLlmErrorMessage(lastError, opts?.llmContext);
409
425
  }
426
+ if (isByteStringOrWrapped(lastError)) throw enrichLlmErrorMessage(lastError, opts?.llmContext);
410
427
  const isReasoningSequenceError = isResponsesReasoningSequenceError(lastError);
411
428
  if (isReasoningSequenceError && attempt >= 1) throw enrichLlmErrorMessage(lastError, opts?.llmContext);
412
429
  if (!isReasoningSequenceError && isNonRetryableClient400(lastError)) throw enrichLlmErrorMessage(lastError, opts?.llmContext);
@@ -546,6 +563,7 @@ async function chatCompleteWithRetryDetailed(opts) {
546
563
  const finalIsOOM = isOllamaOOM(finalError);
547
564
  const finalIs429 = is429OrWrapped(finalError);
548
565
  const finalIsContextLength = isContextLengthError(finalError);
566
+ const finalIsByteString = isByteStringOrWrapped(finalError);
549
567
  const finalIsReasoningSequence = isResponsesReasoningSequenceError(finalError);
550
568
  /** Unwraps LLMRetryError so "Request was aborted" in the cause is detected (#935, #936). */
551
569
  const finalIsTransientLlm = isAbortOrTransientLlmError(finalError);
@@ -555,13 +573,14 @@ async function chatCompleteWithRetryDetailed(opts) {
555
573
  } else if (unconfiguredCount > 0) {
556
574
  pendingWarnings?.add("⚠️ Memory plugin: Some LLM provider keys are missing. Add API keys via: llm.providers.<provider>.apiKey in plugin config, then run: openclaw hybrid-mem verify --test-llm");
557
575
  const finalIsUnconfigured = finalError instanceof UnconfiguredProviderError || finalError instanceof LLMRetryError && finalError.cause instanceof UnconfiguredProviderError;
558
- if (!finalIs500 && !finalIsOOM && !finalIsContextLength && !finalIsUnconfigured && !finalIsTransientLlm && !finalIs403 && !finalIsQuota403 && !finalIs401 && !finalIs429 && !finalIsReasoningSequence) capturePluginError(finalError, {
576
+ if (!finalIs500 && !finalIsOOM && !finalIsContextLength && !finalIsByteString && !finalIsUnconfigured && !finalIsTransientLlm && !finalIs403 && !finalIsQuota403 && !finalIs401 && !finalIs429 && !finalIsReasoningSequence) capturePluginError(finalError, {
559
577
  subsystem: "chat",
560
578
  operation: "chatCompleteWithRetry",
561
579
  phase: "fallback-exhausted"
562
580
  });
563
581
  } else if (finalIsOOM) pendingWarnings?.add("⚠️ Memory plugin: LLM model requires more memory than available (OOM). Consider using a smaller model or configuring a cloud fallback. Run: openclaw hybrid-mem verify --test-llm");
564
582
  else if (finalIsContextLength) pendingWarnings?.add("⚠️ Memory plugin: LLM input exceeds model context window. Consider using a model with a larger context window or reducing input size. Run: openclaw hybrid-mem verify --test-llm");
583
+ else if (finalIsByteString) pendingWarnings?.add("⚠️ Memory plugin: LLM request contains non-ASCII characters in HTTP headers (API key, model name, or custom header). Check your provider configuration for invalid characters (e.g. copy-pasted ellipsis U+2026). Run: openclaw hybrid-mem verify --test-llm");
565
584
  else if (finalIs500) {} else if (finalIs404) pendingWarnings?.add("⚠️ Memory plugin: LLM model not found (404) for all configured models. Check model names in llm.default / llm.heavy / llm.nano config. Run: openclaw hybrid-mem verify --test-llm");
566
585
  else if (finalIs403) pendingWarnings?.add("⚠️ Memory plugin: LLM access denied (403) — your API key may be restricted by country/region, IP block, or billing. Check provider settings. Run: openclaw hybrid-mem verify --test-llm");
567
586
  else if (finalIs401) pendingWarnings?.add("⚠️ Memory plugin: LLM unauthorized (401) — your API key is invalid or expired. Check provider settings. Run: openclaw hybrid-mem verify --test-llm");
@@ -577,6 +596,6 @@ async function chatCompleteWithRetryDetailed(opts) {
577
596
  throw finalError;
578
597
  }
579
598
  //#endregion
580
- export { LLMRetryError, UnconfiguredProviderError, chatComplete, chatCompleteWithRetry, chatCompleteWithRetryDetailed, createPendingLLMWarnings, distillBatchTokenLimit, distillMaxOutputTokens, is401OrWrapped, is403Like, is404Like, is429OrWrapped, is500Like, is500OrWrapped, isAbortOrTransientLlmError, isConnectionErrorLike, isContextLengthError, isOllamaOOM, withLLMRetry };
599
+ export { LLMRetryError, UnconfiguredProviderError, chatComplete, chatCompleteWithRetry, chatCompleteWithRetryDetailed, createPendingLLMWarnings, distillBatchTokenLimit, distillMaxOutputTokens, is401OrWrapped, is403Like, is404Like, is429OrWrapped, is500Like, is500OrWrapped, isAbortOrTransientLlmError, isByteStringSerializationError, isConnectionErrorLike, isContextLengthError, isOllamaOOM, withLLMRetry };
581
600
 
582
601
  //# sourceMappingURL=chat.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"chat.js","names":["getDistillBatchTokenLimitFromCatalog","getDistillMaxOutputTokensFromCatalog"],"sources":["../../services/chat.ts"],"sourcesContent":["/**\n * Unified chat completion for distill and other LLM features.\n * Uses a multi-provider OpenAI-compatible proxy; provider-agnostic model fallback (issue #87).\n */\n\nimport type { OpenAI } from \"openai\";\nimport { DEFAULT_CHAT_TIMEOUT_MS } from \"../utils/constants.js\";\nimport { pluginLogger } from \"../utils/logger.js\";\nimport { withCostFeature } from \"./cost-context.js\";\nimport { capturePluginError } from \"./error-reporter.js\";\nimport {\n formatProviderRateLimitHeaderSummary,\n is403QuotaOrRateLimitLike,\n parseRetryAfterMs,\n} from \"./llm-rate-limit-headers.js\";\nimport {\n type WireApi,\n getDistillBatchTokenLimit as getDistillBatchTokenLimitFromCatalog,\n getDistillMaxOutputTokens as getDistillMaxOutputTokensFromCatalog,\n requiresMaxCompletionTokens,\n shouldOmitSamplingParams,\n resolveWireApi,\n} from \"./model-capabilities.js\";\nimport { callResponsesApi } from \"./responses-adapter.js\";\nimport { recordProviderHttpAttempt, formatRecentHttpAttemptsForRateLimitLog } from \"./recent-http-attempts.js\";\n\nexport { is403QuotaOrRateLimitLike, parseGoDurationToMs, parseRetryAfterMs } from \"./llm-rate-limit-headers.js\";\n\n/**\n * Thrown when a model's provider has no API key or base URL configured in llm.providers.\n * chatCompleteWithRetry catches this and skips immediately to the next fallback model.\n */\nexport class UnconfiguredProviderError extends Error {\n readonly provider: string;\n readonly model: string;\n constructor(provider: string, model: string, detail?: string) {\n const base = `Provider '${provider}' is not configured for model ${model}.`;\n const hint = detail ?? `Set llm.providers.${provider}.apiKey in plugin config.`;\n super(`${base} ${hint}`);\n this.name = \"UnconfiguredProviderError\";\n this.provider = provider;\n this.model = model;\n }\n}\n\n/**\n * Pending user-visible LLM config warnings to inject into the next chat session.\n * Scoped per plugin instance to avoid cross-agent leakage.\n */\nexport type PendingLLMWarnings = {\n add: (message: string) => void;\n drain: () => string[];\n};\n\n/** Create a per-instance pending LLM warning queue. */\nexport function createPendingLLMWarnings(): PendingLLMWarnings {\n const pending = new Set<string>();\n return {\n add(message: string) {\n pending.add(message);\n },\n drain() {\n if (pending.size === 0) return [];\n const msgs = [...pending];\n pending.clear();\n return msgs;\n },\n };\n}\n\n/**\n * Unified 404 detection helper.\n * Checks the HTTP status code property first (reliable), then falls back to\n * targeted message pattern matching. Only matches \"model not found\" scenarios,\n * NOT generic \"file not found\" or \"module not found\" errors.\n *\n * Exported so embeddings.ts can suppress capturePluginError for 404 errors.\n */\nexport function is404Like(err: unknown): boolean {\n if (err && typeof err === \"object\") {\n // OpenAI SDK sets .status directly (number); also tolerate string \"404\" for robustness\n const status = (err as { status?: unknown }).status;\n if (status === 404 || status === \"404\") return true;\n }\n if (err instanceof Error) {\n // Match HTTP 404 patterns specifically — avoid false positives from\n // \"file not found\" or \"module not found\" strings in non-HTTP errors.\n return (\n /\\bHTTP\\s+404\\b|\\b404\\b.*not\\s+found|model.*not\\s+found|not\\s+found.*model/i.test(err.message) ||\n // Also match bare numeric 404 in error messages (e.g. \"404 Not Found\" from HTTP responses)\n /^\\b404\\b/.test(err.message.trim()) ||\n // OpenAI SDK formats: \"404 Model not found\" or \"Error code: 404\"\n /\\bError\\s+code:\\s*404\\b|\\b404\\s+[A-Za-z]/.test(err.message) ||\n // Google Generative Language API: \"models/<name> is not found for API version <v>\"\n // Occurs when a model is unavailable through a specific API version endpoint (e.g. v1beta/openai/).\n /is not found for api version/i.test(err.message)\n );\n }\n return false;\n}\n\n/**\n * 403 Forbidden / access-denied detection helper (geo, IP block, etc.).\n * Excludes {@link is403QuotaOrRateLimitLike} quota-style 403s.\n */\nexport function is403Like(err: unknown): boolean {\n if (err instanceof LLMRetryError) return is403Like(err.cause);\n // Quota / rate-limit style 403 (retry-after, remaining-tokens) is not geo \"forbidden\".\n if (is403QuotaOrRateLimitLike(err)) return false;\n if (err && typeof err === \"object\") {\n const status = (err as { status?: unknown }).status;\n if (status === 403 || status === \"403\") return true;\n }\n if (err instanceof Error) {\n // Match HTTP 403 patterns: \"403 Forbidden\", \"403 Country, region, or territory not supported\", etc.\n if (\n /^\\b403\\b/.test(err.message.trim()) ||\n /\\bHTTP\\s+403\\b|\\bError\\s+code:\\s*403\\b|\\b403\\s+[A-Za-z]/i.test(err.message)\n ) {\n return true;\n }\n // Match provider-specific geo-restriction / permission-denied phrases that arrive WITHOUT\n // a numeric \"403\" prefix when errors pass through a proxy or gateway.\n // #490: Google returns \"Country, region, or territory not supported\" (exact phrase) as\n // the message body when the API key / project is not permitted in the request's region.\n if (/\\bcountry,\\s*region,\\s*or\\s*territory\\s+not\\s+supported\\b/i.test(err.message)) return true;\n // gRPC PERMISSION_DENIED status mapped to HTTP 403 — some gateway implementations\n // embed the gRPC status name instead of (or in addition to) the HTTP code.\n if (/\\bPERMISSION_DENIED\\b/.test(err.message)) return true;\n // Generic \"access denied\" / \"access forbidden\" phrases without a numeric status prefix\n // that certain proxy implementations emit when forwarding a 403 response.\n if (/\\baccess\\s+(denied|forbidden)\\b/i.test(err.message) && !/\\b(file|directory|path|disk)\\b/i.test(err.message))\n return true;\n }\n return false;\n}\n\n/**\n * 401 Unauthorized / invalid api key detection helper.\n * A 401 is a permanent operator config issue (wrong API key) that will never be resolved by retrying.\n * Exported so other modules can treat 401 as a config error and suppress capturePluginError.\n */\nfunction is401Like(err: unknown): boolean {\n if (err && typeof err === \"object\") {\n const status = (err as { status?: unknown }).status;\n if (status === 401 || status === \"401\") return true;\n }\n if (err instanceof Error) {\n if (/\\b401\\b|unauthorized/i.test(err.message)) return true;\n // Also match specific auth failure phrases for robustness — catches errors from providers\n // that don't include \"401\" or \"unauthorized\" in the message and don't set a .status property.\n if (/incorrect api key|invalid api key|authentication failed/i.test(err.message)) return true;\n }\n return false;\n}\n\n/** Returns true when the error is a 401 (auth failure) — either directly or wrapped in LLMRetryError. */\nexport function is401OrWrapped(err: Error): boolean {\n if (is401Like(err)) return true;\n if (err instanceof LLMRetryError && is401Like(err.cause)) return true;\n return false;\n}\n\n/**\n * 429 Too Many Requests / rate-limit detection helper.\n * Checks the HTTP status code property first (reliable), then falls back to\n * message pattern matching. Rate limits are transient — suppress GlitchTip reporting.\n * Exported so embeddings.ts can suppress capturePluginError for 429 errors.\n */\nfunction is429Like(err: unknown): boolean {\n if (err && typeof err === \"object\") {\n const status = (err as { status?: unknown }).status;\n if (status === 429 || status === \"429\") return true;\n }\n if (err instanceof Error) {\n return (\n /^\\b429\\b/.test(err.message.trim()) ||\n /\\bHTTP\\s+429\\b|\\bError\\s+code:\\s*429\\b|\\b429\\s+[A-Za-z]/i.test(err.message) ||\n /\\btoo\\s+many\\s+requests\\b/i.test(err.message)\n );\n }\n return false;\n}\n\n/**\n * Returns true when the error is a 429 (rate limit) — either directly or wrapped in LLMRetryError.\n * Used in chatCompleteWithRetry to detect 429 errors that were retried and wrapped by withLLMRetry.\n * Exported so embeddings.ts can suppress capturePluginError for 429 errors.\n */\nexport function is429OrWrapped(err: Error): boolean {\n if (is429Like(err)) return true;\n if (err instanceof LLMRetryError && is429Like(err.cause)) return true;\n return false;\n}\n\n/**\n * Unified 5xx / internal server error detection helper.\n * Checks HTTP status code property first, then uses conservative message patterns.\n * Avoids false positives from non-HTTP \"internal error\" messages (e.g. JavaScript errors).\n *\n * Exported so other modules (lifecycle/hooks, auto-classifier) can suppress\n * capturePluginError for transient server errors.\n */\nexport function is500Like(err: unknown): boolean {\n if (err && typeof err === \"object\") {\n const status = (err as { status?: unknown }).status;\n if (typeof status === \"number\" && status >= 500 && status < 600) return true;\n }\n if (err instanceof Error) {\n const m = err.message;\n // Only match HTTP 5xx patterns — not generic \"internal error\" from JS\n if (\n /\\bHTTP\\s+5\\d{2}\\b|\\b5\\d{2}\\s+(internal\\s+)?(error|status)|status\\s+5\\d{2}|internal\\s+server\\s+error/i.test(m)\n ) {\n return true;\n }\n // Gateway/proxy phrasing seen in production (#1010, #1013): \"502 error code: 502\"\n // Single pattern: any \"error code: 5xx\" also covers \"5xx error code: 5xx\".\n if (/\\berror\\s+code:\\s*5\\d{2}\\b/i.test(m)) return true;\n }\n return false;\n}\n\n/** Returns true when the error is a 5xx server error — either directly or wrapped in LLMRetryError. */\nexport function is500OrWrapped(err: Error): boolean {\n if (is500Like(err)) return true;\n if (err instanceof LLMRetryError && is500Like(err.cause)) return true;\n return false;\n}\n\n/**\n * Detect transient SDK/network connection failures.\n *\n * OpenAI-compatible SDKs may surface connectivity problems as a bare\n * \"Connection error.\" message (e.g. APIConnectionError) without exposing the\n * underlying socket code on the top-level Error object. These are transient\n * transport failures, not plugin logic bugs, so callers should treat them like\n * timeouts/ECONNREFUSED for retry, fallback, and GlitchTip suppression.\n */\nexport function isConnectionErrorLike(err: unknown): boolean {\n if (err instanceof LLMRetryError) return isConnectionErrorLike(err.cause);\n\n const candidates: unknown[] = [err];\n if (err && typeof err === \"object\" && \"cause\" in err) {\n candidates.push((err as { cause?: unknown }).cause);\n }\n\n for (const candidate of candidates) {\n if (!candidate || typeof candidate !== \"object\") continue;\n\n const code = (candidate as { code?: unknown }).code;\n if (\n typeof code === \"string\" &&\n /ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENOTFOUND|EHOSTUNREACH|UND_ERR_CONNECT_TIMEOUT/i.test(code)\n ) {\n return true;\n }\n\n const name = (candidate as { name?: unknown }).name;\n if (typeof name === \"string\" && /APIConnectionError/i.test(name)) {\n return true;\n }\n }\n\n if (!(err instanceof Error)) return false;\n\n return (\n /\\bconnection error\\b/i.test(err.message) ||\n /\\bnetwork error\\b/i.test(err.message) ||\n /ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENOTFOUND|EHOSTUNREACH|connect\\s+ETIMEDOUT|socket hang up|fetch failed/i.test(\n err.message,\n )\n );\n}\n\n/**\n * Detect 400 errors caused by exceeding the model's context length.\n * These are unrecoverable without truncating the input — retrying is wasteful (#442).\n * Pattern matches OpenAI's error: \"400 Invalid 'input': maximum context length is 8192 tokens.\"\n * Also matches Ollama's error: \"Input length 768 exceeds maximum allowed token size 512\" (#488).\n */\nexport function isContextLengthError(err: unknown): boolean {\n if (err && typeof err === \"object\") {\n const status = (err as { status?: unknown }).status;\n if (typeof status === \"number\" && status === 400) {\n const msg = ((err as { message?: string }).message ?? \"\").toLowerCase();\n if (\n msg.includes(\"context length\") ||\n msg.includes(\"maximum context\") ||\n /max.*token.*(length|limit)|token limit|context.length/i.test(msg) ||\n // #488: Ollama \"Input length 768 exceeds maximum allowed token size 512\"\n /input\\s+length\\s+\\d+\\s+exceeds/i.test(msg)\n ) {\n return true;\n }\n }\n }\n if (err instanceof Error) {\n const msg = err.message.toLowerCase();\n return (\n ((msg.includes(\"400\") || msg.includes(\"bad request\")) &&\n (msg.includes(\"context length\") ||\n msg.includes(\"maximum context\") ||\n /max.*token.*(length|limit)|token limit|context.length/i.test(err.message))) ||\n /\\b400\\b.*maximum context length/i.test(err.message) ||\n // #488: Ollama message-only — no \"400\" prefix or .status property.\n // This pattern intentionally appears three times: some SDKs set .status=400 (first block),\n // others embed \"400\" in the message (second block), others emit message-only (this block).\n /input\\s+length\\s+\\d+\\s+exceeds/i.test(err.message)\n );\n }\n return false;\n}\n\n/**\n * Mutates and returns `err` so GlitchTip/async stacks show which model/phase failed (#1010–#1011).\n */\nfunction enrichLlmErrorMessage(err: Error, ctx?: { model?: string; operation?: string }): Error {\n if (!ctx?.model && !ctx?.operation) return err;\n if (/\\[llm\\s+/i.test(err.message)) return err;\n const parts: string[] = [];\n if (ctx.model) parts.push(`model=${ctx.model}`);\n if (ctx.operation) parts.push(`op=${ctx.operation}`);\n err.message = `${err.message} [llm ${parts.join(\", \")}]`;\n return err;\n}\n\n/**\n * HTTP 400 that is not a context-length violation — retrying will not help (#1011, #1016).\n */\nfunction isNonRetryableClient400(err: unknown): boolean {\n if (isContextLengthError(err)) return false;\n if (err && typeof err === \"object\") {\n const status = (err as { status?: unknown }).status;\n if (status === 400 || status === \"400\") return true;\n }\n if (err instanceof Error) {\n const m = err.message;\n if (/\\b400\\b.*\\b(?:no body|status code)\\b/i.test(m)) return true;\n }\n return false;\n}\n\n/** Gateway/proxy may return HTTP 400 with an empty body — transient, not a plugin bug (#1157). */\nfunction is400EmptyBodyGatewayError(err: unknown): boolean {\n if (!(err instanceof Error)) return false;\n return /\\b400\\b.*\\bno body\\b/i.test(err.message);\n}\n\n/** Some Azure/Foundry deployments return 400 when chat params are not supported for the model (#1165). */\nfunction is400UnsupportedOperationError(err: unknown): boolean {\n if (!(err instanceof Error)) return false;\n return /\\b400\\b.*\\bunsupported\\b/i.test(err.message);\n}\n\n/**\n * Detect malformed Responses API reasoning-output sequencing errors, e.g.:\n * \"Item 'rs_...' of type 'reasoning' was provided without its required following item.\"\n *\n * This is an intermittent provider-side 400 seen with some reasoning deployments (notably some\n * Azure Foundry o3-pro paths). Treat as transient/retryable (limited) instead of permanent bad-request.\n *\n * Unwraps {@link LLMRetryError} and checks `cause` so detection still works when the message is wrapped.\n */\nexport function isResponsesReasoningSequenceError(err: unknown): boolean {\n const msg = collectErrorMessageChain(err);\n return /\\btype\\s+['\"]?reasoning['\"]?\\s+was\\s+provided\\s+without\\s+(?:its\\s+)?required\\s+following\\s+item\\b/i.test(\n msg,\n );\n}\n\nfunction collectErrorMessageChain(err: unknown): string {\n const parts: string[] = [];\n let cur: unknown = err;\n const seen = new Set<unknown>();\n for (let i = 0; i < 8 && cur != null && !seen.has(cur); i++) {\n seen.add(cur);\n if (cur instanceof Error) {\n parts.push(cur.message);\n cur = cur.cause;\n continue;\n }\n if (\n typeof cur === \"object\" &&\n cur !== null &&\n \"message\" in cur &&\n typeof (cur as { message: unknown }).message === \"string\"\n ) {\n parts.push((cur as { message: string }).message);\n break;\n }\n break;\n }\n return parts.join(\" \");\n}\n\n/**\n * Detect Ollama out-of-memory (OOM) errors from the model server.\n * Ollama returns HTTP 500 with a body like:\n * \"model requires more system memory (18.2 GiB) than is available (8.0 GiB)\"\n * These are expected failures when the configured model is too large for the host —\n * not bugs — so callers should skip capturePluginError and warn the user instead.\n *\n * Exported so other modules (lifecycle/hooks, auto-classifier, embeddings) can\n * detect OOM specifically for user-visible warnings and circuit-breaker decisions.\n */\nexport function isOllamaOOM(err: unknown): boolean {\n if (!(err instanceof Error)) return false;\n const msg = err.message.toLowerCase();\n return (\n msg.includes(\"model requires more system memory\") ||\n msg.includes(\"not enough memory to load\") ||\n // \"requires X GiB\" pattern covers variant phrasings from different Ollama versions\n /\\bmodel\\s+requires\\s+[\\d.]+\\s*gib/i.test(err.message) ||\n // Bare OOM signal in error body (e.g. \"oom: model 'qwen3:8b' ...\")\n /\\boom:/i.test(err.message)\n );\n}\n\nexport async function chatComplete(opts: {\n model: string;\n content: string;\n temperature?: number;\n maxTokens?: number;\n openai: OpenAI;\n /** Timeout in ms; after this the promise rejects. Prevents silent hang when gateway/LLM never responds. Default 45s. */\n timeoutMs?: number;\n /** When aborted (e.g. parent timeout), the request is cancelled and no retry is needed. */\n signal?: AbortSignal;\n /** Feature label for cost tracking. When set, the call is wrapped in withCostFeature() so the proxy records the correct label. */\n feature?: string;\n /** Force a specific wire API surface (\"chat\" or \"responses\"). When unset, resolved from the model's provider prefix. */\n wireApi?: WireApi;\n}): Promise<string> {\n const {\n model,\n content,\n temperature = 0.2,\n maxTokens,\n timeoutMs = DEFAULT_CHAT_TIMEOUT_MS,\n signal,\n feature,\n wireApi: wireApiOverride,\n } = opts;\n const effectiveMaxTokens = maxTokens ?? distillMaxOutputTokens(model);\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeoutMs);\n const onAbort = () => controller.abort();\n if (signal) {\n if (signal.aborted) {\n clearTimeout(timeoutId);\n throw new Error(\"Request was aborted\");\n }\n signal.addEventListener(\"abort\", onAbort);\n }\n\n try {\n const wireApi = resolveWireApi(model, wireApiOverride);\n\n if (wireApi === \"responses\") {\n const doCreate = () =>\n callResponsesApi(\n opts.openai,\n { model, content, temperature, maxTokens: effectiveMaxTokens },\n { signal: controller.signal },\n );\n const { text } = await (feature ? withCostFeature(feature, doCreate) : doCreate());\n clearTimeout(timeoutId);\n if (signal) signal.removeEventListener(\"abort\", onAbort);\n return text;\n }\n\n // Standard chat.completions.create path\n const useMaxCompletionTokens = requiresMaxCompletionTokens(model);\n const body: OpenAI.ChatCompletionCreateParamsNonStreaming = {\n model,\n messages: [{ role: \"user\", content }],\n ...(useMaxCompletionTokens ? { max_completion_tokens: effectiveMaxTokens } : { max_tokens: effectiveMaxTokens }),\n };\n if (!shouldOmitSamplingParams(model)) {\n body.temperature = temperature;\n }\n const doCreate = () =>\n opts.openai.chat.completions.create(body as unknown as Parameters<OpenAI[\"chat\"][\"completions\"][\"create\"]>[0], {\n signal: controller.signal,\n });\n // If feature is provided, wrap in withCostFeature so the proxy attributes the call correctly.\n // Cost recording itself is done by the OpenAI proxy in setup/init-databases.ts.\n const resp = (await (feature ? withCostFeature(feature, doCreate) : doCreate())) as OpenAI.Chat.ChatCompletion;\n clearTimeout(timeoutId);\n if (signal) signal.removeEventListener(\"abort\", onAbort);\n const msg = resp.choices?.[0]?.message;\n const msgContent = msg?.content?.trim();\n if (msgContent) return msgContent;\n // Qwen3 thinking mode (Ollama OpenAI-compat endpoint) puts the response in\n // message.reasoning_content (current standard, May 2025+) or message.reasoning (legacy).\n // Fall back to these fields when enable_thinking=true so agents don't see an empty reply (#314).\n const msgRecord = msg as unknown as Record<string, unknown> | undefined;\n const reasoningContent = msgRecord?.reasoning_content;\n if (typeof reasoningContent === \"string\" && reasoningContent.trim()) return reasoningContent.trim();\n const reasoning = msgRecord?.reasoning;\n if (typeof reasoning === \"string\" && reasoning.trim()) return reasoning.trim();\n return msgContent ?? \"\";\n } catch (err) {\n clearTimeout(timeoutId);\n if (signal) signal.removeEventListener(\"abort\", onAbort);\n const isAbort = err instanceof Error && err.name === \"AbortError\";\n const error = isAbort\n ? new Error(`LLM request timeout after ${timeoutMs}ms (model: ${model})`)\n : err instanceof Error\n ? enrichLlmErrorMessage(err, { model, operation: \"chatComplete\" })\n : new Error(String(err));\n // Skip reporting known transient gateway/LLM errors (aborted, timeout, 5xx, OOM, 429) and config errors (missing provider keys) to avoid GlitchTip noise\n const msg = error.message.toLowerCase();\n const isTransient =\n msg.includes(\"request was aborted\") ||\n msg.includes(\"operation was aborted\") ||\n msg.includes(\"request timed out\") ||\n msg.includes(\"timed out\") ||\n msg.includes(\"llm request timeout\") || // #339: our own timeout message uses \"timeout\" not \"timed out\"\n msg.includes(\"econnrefused\") ||\n isAbortOrTransientLlmError(err) || // #1694: recursive cause-chain check catches nested AbortError (e.g. MiniMax wraps abort in outer error)\n isConnectionErrorLike(err) || // #703: OpenAI SDK APIConnectionError / \"Connection error.\" is transient\n is429Like(err) || // #397: rate limit is transient\n is403QuotaOrRateLimitLike(err) || // Azure/APIM quota as 403 + headers\n /^\\d+\\s*internal\\s*error$/i.test(msg.trim()) ||\n /^5\\d{2}\\s/.test(msg.trim()) ||\n is500Like(err) || // #302: OpenAI SDK InternalServerError has no numeric prefix\n isOllamaOOM(err) || // #387: Ollama OOM — model too large for available RAM, not a bug\n isResponsesReasoningSequenceError(err) || // #1034: malformed reasoning item sequence — retryable\n is400EmptyBodyGatewayError(error) ||\n is400EmptyBodyGatewayError(err) || // #1157: 400 (no body) from gateway — do not GlitchTip\n is400UnsupportedOperationError(error) ||\n is400UnsupportedOperationError(err); // #1165: model/endpoint mismatch\n const isConfigError =\n err instanceof UnconfiguredProviderError ||\n is404Like(err) || // #303: model not found = wrong model name in config, not a bug\n is403Like(err) || // #394: country/region restriction = operator config issue, not a bug\n is401Like(err) || // #475: invalid API key = operator config issue, not a bug\n isContextLengthError(err); // #488: input too long for model context window = wrong model choice, not a bug\n if (!isTransient && !isConfigError) {\n capturePluginError(error, {\n subsystem: \"chat\",\n operation: \"chatComplete\",\n phase: \"gateway\",\n });\n }\n throw error;\n }\n}\n\n/** Max input tokens for one distill batch request. From model-capabilities catalog (docs/MODEL-REFERENCE.md). */\nexport function distillBatchTokenLimit(model: string): number {\n return getDistillBatchTokenLimitFromCatalog(model);\n}\n\n/** Max output tokens for distill/ingest LLM calls. From model-capabilities catalog (docs/MODEL-REFERENCE.md). */\nexport function distillMaxOutputTokens(model: string): number {\n return getDistillMaxOutputTokensFromCatalog(model);\n}\n\n/**\n * Custom error class that includes retry attempt information.\n */\nexport class LLMRetryError extends Error {\n constructor(\n message: string,\n public readonly cause: Error,\n public readonly attemptNumber: number,\n ) {\n super(message);\n this.name = \"LLMRetryError\";\n }\n}\n\n/**\n * True when an LLM call failed for abort, gateway loss, or transport — not plugin logic.\n * Used by session narrative and similar paths to avoid noisy warns when the gateway stops.\n */\nexport function isAbortOrTransientLlmError(err: unknown): boolean {\n if (err instanceof LLMRetryError) {\n return isAbortOrTransientLlmError(err.cause);\n }\n if (err && typeof err === \"object\" && \"cause\" in err) {\n const c = (err as { cause?: unknown }).cause;\n if (c !== undefined && c !== null && isAbortOrTransientLlmError(c)) return true;\n }\n if (!(err instanceof Error)) {\n return isConnectionErrorLike(err);\n }\n if (err.name === \"AbortError\") return true;\n const msg = err.message;\n if (/request was aborted|Request was aborted|The operation was aborted|operation was aborted/i.test(msg)) return true;\n if (/gateway client stopped|gateway not reachable|not reachable\\.|is it running/i.test(msg)) return true;\n return isConnectionErrorLike(err);\n}\n\n/**\n * Retry wrapper for LLM calls with exponential backoff.\n * Retries on failure with increasing delays: 1s, 3s, 9s.\n * On final failure, throws LLMRetryError with attempt number.\n * When signal is provided and aborted, does not retry (fails immediately).\n */\nexport async function withLLMRetry<T>(\n fn: () => Promise<T>,\n opts?: {\n maxRetries?: number;\n signal?: AbortSignal;\n /** Tags errors for triage when they surface at async boundaries (#1010, #1011). */\n llmContext?: { model?: string; operation?: string };\n },\n): Promise<T> {\n const maxRetries = opts?.maxRetries ?? 3;\n let lastError: Error | undefined;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n recordProviderHttpAttempt({\n operation: opts?.llmContext?.operation,\n model: opts?.llmContext?.model,\n });\n try {\n return await fn();\n } catch (err) {\n lastError = err instanceof Error ? err : new Error(String(err));\n // Don't retry UnconfiguredProviderError — missing API keys won't be fixed by retrying\n if (lastError instanceof UnconfiguredProviderError) {\n throw lastError;\n }\n // Don't retry 401 — wrong key won't be fixed by retrying\n if (is401Like(lastError)) {\n throw lastError;\n }\n // Don't retry 403 — access forbidden (country/region restriction, IP block, billing) won't be fixed by retrying (#394)\n if (is403Like(lastError)) {\n throw lastError;\n }\n // Don't retry 404 — model doesn't exist, let chatCompleteWithRetry try next model\n if (is404Like(lastError)) {\n const modelHint = lastError.message.match(/model[:\\s]+(\\S+)/i)?.[1];\n pluginLogger.warn(\n `memory-hybrid: Model not found (404)${modelHint ? ` for ${modelHint}` : \"\"} — check model name or provider availability`,\n );\n throw lastError;\n }\n // Don't retry 400 context-length errors — input was too long; retrying won't fix it (#442)\n if (isContextLengthError(lastError)) {\n pluginLogger.warn(\n \"memory-hybrid: Input exceeds model context length — retrying will not help; truncate input before calling\",\n );\n throw enrichLlmErrorMessage(lastError, opts?.llmContext);\n }\n const isReasoningSequenceError = isResponsesReasoningSequenceError(lastError);\n // Azure/OpenAI Responses API intermittent malformed reasoning item sequence:\n // allow one retry, then fail fast so fallback model can run.\n if (isReasoningSequenceError && attempt >= 1) {\n throw enrichLlmErrorMessage(lastError, opts?.llmContext);\n }\n // Other 400s (e.g. empty body from gateway) — not fixed by retry (#1011, #1016)\n if (!isReasoningSequenceError && isNonRetryableClient400(lastError)) {\n throw enrichLlmErrorMessage(lastError, opts?.llmContext);\n }\n const is429 = is429Like(lastError);\n const isQuota403 = is403QuotaOrRateLimitLike(lastError);\n // Timeouts: only retry once (attempt 0 → attempt 1), then throw so chatCompleteWithRetry can try next model.\n // (attempt is 0-based: attempt >= 1 means we've already retried once.)\n const isTimeout = /timed out|llm request timeout|request was aborted|Request was aborted/i.test(\n lastError.message,\n ); // #339: include our own \"LLM request timeout\" pattern\n const isConnectionError = isConnectionErrorLike(lastError);\n if ((isTimeout || isConnectionError) && attempt >= 1) {\n throw lastError;\n }\n // Ollama OOM: never retry — model requires more memory than available, won't be fixed by retrying.\n // chatCompleteWithRetry will try the next fallback model (e.g. Gemini, OpenAI).\n if (isOllamaOOM(lastError)) {\n pluginLogger.warn(\n \"memory-hybrid: Ollama model OOM — model requires more memory than is available. Skipping retries; will try next fallback model.\",\n );\n throw lastError;\n }\n // 5xx / internal server error: only retry once\n const isServerError = is500Like(lastError);\n if (isServerError && attempt >= 1) {\n throw lastError;\n }\n if (attempt === maxRetries || opts?.signal?.aborted) {\n // Capture causeMsg before enrichment to preserve end-anchored regex matching\n const causeMsg = lastError.message.toLowerCase();\n enrichLlmErrorMessage(lastError, opts?.llmContext);\n const retryError = new LLMRetryError(\n `Failed after ${attempt + 1} attempts: ${lastError.message}`,\n lastError,\n attempt + 1,\n );\n // Skip reporting when the underlying cause is a transient gateway error (aborted, timeout, 5xx, 429).\n // Note: 404 and 403 errors should never reach this branch (they exit early above),\n // but we include them as defensive safety nets in case they slip past due to future refactors.\n const fullMsg = retryError.message.toLowerCase();\n const isTransient =\n is429 ||\n is403QuotaOrRateLimitLike(lastError) ||\n isServerError || // #302: 5xx server errors are transient\n is404Like(lastError) || // #329: defensive safety net — 404 = model not found, config issue, not a bug\n is403Like(lastError) || // #394: defensive safety net — 403 = country/region restriction, config issue, not a bug\n is401Like(lastError) || // #475: defensive safety net — 401 = invalid API key, config issue, not a bug\n causeMsg.includes(\"request was aborted\") ||\n fullMsg.includes(\"request was aborted\") ||\n causeMsg.includes(\"request timed out\") ||\n fullMsg.includes(\"request timed out\") ||\n causeMsg.includes(\"timed out\") ||\n fullMsg.includes(\"timed out\") ||\n causeMsg.includes(\"llm request timeout\") || // #339: our own timeout message uses \"timeout\" not \"timed out\"\n fullMsg.includes(\"llm request timeout\") ||\n isAbortOrTransientLlmError(lastError) || // #1694: recursive cause-chain check catches nested AbortError (e.g. MiniMax wraps abort in outer error)\n isConnectionErrorLike(lastError) ||\n /^\\d+\\s*internal\\s*error$/i.test(causeMsg.trim()) ||\n /^5\\d{2}\\s/.test(causeMsg.trim()) ||\n /\\b405\\s+method\\s+not\\s+allowed/i.test(causeMsg) ||\n /\\b405\\s+method\\s+not\\s+allowed/i.test(fullMsg) ||\n isResponsesReasoningSequenceError(lastError) || // #1034\n is400EmptyBodyGatewayError(lastError) || // #1157\n is400UnsupportedOperationError(lastError); // #1165\n if (!isTransient) {\n capturePluginError(retryError, {\n subsystem: \"chat\",\n operation: \"withLLMRetry\",\n retryAttempt: attempt + 1,\n ...(opts?.llmContext?.model ? { configShape: { model: opts.llmContext.model } } : {}),\n });\n }\n throw retryError;\n }\n\n // 429 / quota-style 403: respect Retry-After header if present; otherwise use exponential backoff (2s → 4s → 8s)\n let delay: number;\n if (is429 || isQuota403) {\n const retryAfterMs = parseRetryAfterMs(err);\n delay = retryAfterMs ?? 2 ** (attempt + 1) * 1000;\n const op = opts?.llmContext?.operation;\n const modelId = opts?.llmContext?.model;\n const ctxTag = op || modelId ? ` [${[op, modelId].filter(Boolean).join(\" · \")}]` : \"\";\n const hdr = formatProviderRateLimitHeaderSummary(err);\n const recent = formatRecentHttpAttemptsForRateLimitLog();\n const attemptTotal = Math.max(1, maxRetries);\n pluginLogger.warn(\n `memory-hybrid: ${isQuota403 ? \"Quota/rate limit (403)\" : \"Rate limited by provider\"}${ctxTag} — retry ${attempt + 1}/${attemptTotal}, backing off ${delay}ms | ${hdr} | ${recent} | rolling counts = outbound HTTP attempts in this process (not token usage); use tokRem/reqRem when headers exist.`,\n );\n } else {\n delay = 3 ** attempt * 1000; // 1s, 3s, 9s\n }\n // Abort-aware backoff sleep: if the signal fires while we are waiting, reject immediately\n // instead of sleeping through the full delay. The listener is removed on normal resolve to\n // prevent leaks; the { once: true } option is not relied on alone for cleanup.\n await new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timeout);\n const reason = opts?.signal?.reason;\n const msg = reason instanceof Error ? reason.message : reason != null ? String(reason) : \"Aborted\";\n const abortError = new Error(msg);\n abortError.name = \"AbortError\";\n reject(abortError);\n };\n const timeout = setTimeout(() => {\n opts?.signal?.removeEventListener(\"abort\", onAbort);\n resolve();\n }, delay);\n if (opts?.signal) {\n if (opts.signal.aborted) {\n onAbort();\n } else {\n opts.signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n }\n });\n }\n }\n throw new Error(\"unreachable\");\n}\n\n/**\n * Wrapper for chatComplete with retry and fallback model support.\n * Tries primary model with retries, then falls back to each fallback model in order.\n * All calls go through the gateway (openai client).\n */\nexport async function chatCompleteWithRetry(opts: {\n model: string;\n content: string;\n temperature?: number;\n maxTokens?: number;\n openai: OpenAI;\n fallbackModels?: string[];\n label?: string;\n /** Timeout per model attempt (passed to chatComplete). Default 45s. */\n timeoutMs?: number;\n /** When aborted (e.g. parent step timeout), the request is cancelled and no fallback models are tried. */\n signal?: AbortSignal;\n /** Optional per-instance warning queue for missing provider keys. */\n pendingWarnings?: PendingLLMWarnings;\n /** Feature label for cost tracking. Passed to chatComplete which wraps the call in withCostFeature(). */\n feature?: string;\n}): Promise<string> {\n return (await chatCompleteWithRetryDetailed(opts)).content;\n}\n\nexport type ChatCompleteWithRetryDetails = {\n content: string;\n modelUsed: string;\n /**\n * Effective model attempt order (primary then fallbacks). This is the full chain that was eligible\n * for attempts; some models may still have been skipped due to AbortSignal pre-checks.\n */\n attemptChain: string[];\n};\n\n/**\n * Like {@link chatCompleteWithRetry} but returns which model actually produced the response.\n * Used by maintenance commands to log the effective model selected after fallbacks.\n */\nexport async function chatCompleteWithRetryDetailed(opts: {\n model: string;\n content: string;\n temperature?: number;\n maxTokens?: number;\n openai: OpenAI;\n fallbackModels?: string[];\n label?: string;\n /** Timeout per model attempt (passed to chatComplete). Default 45s. */\n timeoutMs?: number;\n /** When aborted (e.g. parent step timeout), the request is cancelled and no fallback models are tried. */\n signal?: AbortSignal;\n /** Optional per-instance warning queue for missing provider keys. */\n pendingWarnings?: PendingLLMWarnings;\n /** Feature label for cost tracking. Passed to chatComplete which wraps the call in withCostFeature(). */\n feature?: string;\n}): Promise<ChatCompleteWithRetryDetails> {\n const {\n fallbackModels = [],\n label: rawLabel,\n maxTokens,\n timeoutMs,\n signal,\n pendingWarnings,\n feature,\n ...chatOpts\n } = opts;\n const label = rawLabel ?? \"LLM call\";\n const modelsToTry = [opts.model, ...fallbackModels];\n\n let lastError: Error | undefined;\n let unconfiguredCount = 0;\n let lastAttemptedModel = opts.model;\n\n for (let i = 0; i < modelsToTry.length; i++) {\n if (signal?.aborted) {\n const reason = (signal as AbortSignal).reason;\n const msg = reason instanceof Error ? reason.message : reason != null ? String(reason) : \"Aborted\";\n const abortError = new Error(msg);\n abortError.name = \"AbortError\";\n throw abortError;\n }\n const currentModel = modelsToTry[i];\n lastAttemptedModel = currentModel;\n const _isFallback = i > 0;\n // Use per-model max_tokens so fallbacks (e.g. gpt-4o) don't receive primary model's limit (e.g. 65k for Gemini)\n const effectiveMaxTokens = maxTokens ?? distillMaxOutputTokens(currentModel);\n\n try {\n const content = await withLLMRetry(\n () =>\n chatComplete({\n ...chatOpts,\n model: currentModel,\n maxTokens: effectiveMaxTokens,\n ...(timeoutMs != null && { timeoutMs }),\n signal,\n ...(feature != null && { feature }),\n }),\n {\n maxRetries: 3,\n signal,\n llmContext: { model: currentModel, operation: label },\n },\n );\n return { content, modelUsed: currentModel, attemptChain: modelsToTry };\n } catch (err) {\n lastError = err instanceof Error ? err : new Error(String(err));\n // Check both direct UnconfiguredProviderError and wrapped in LLMRetryError\n const isUnconfigured =\n lastError instanceof UnconfiguredProviderError ||\n (lastError instanceof LLMRetryError && lastError.cause instanceof UnconfiguredProviderError);\n const is429 = is429OrWrapped(lastError);\n const isTimeout = /timed out|llm request timeout|request was aborted|Request was aborted/i.test(\n lastError.message,\n ); // #339: include our own \"LLM request timeout\" pattern\n const isConnectionError = isConnectionErrorLike(lastError);\n const is404 = is404Like(lastError);\n const is403 = is403Like(lastError);\n const isQuota403 = is403QuotaOrRateLimitLike(lastError);\n const is401 = is401Like(lastError);\n const is500 = is500Like(lastError); // #302\n const isContextLength = isContextLengthError(lastError); // #488\n const isReasoningSequence = isResponsesReasoningSequenceError(lastError); // #1034\n if (isUnconfigured) unconfiguredCount++;\n if (i < modelsToTry.length - 1 && !signal?.aborted) {\n if (!isUnconfigured) {\n const reason = is429\n ? \"rate limited (429)\"\n : isTimeout\n ? \"timed out\"\n : isConnectionError\n ? \"connection failed\"\n : is404\n ? \"model not found (404)\"\n : isQuota403\n ? \"quota / rate limit (403)\"\n : is403\n ? \"access denied (403)\"\n : is401\n ? \"unauthorized (401)\"\n : is500\n ? \"server error (500)\" // #302\n : isContextLength\n ? \"input too long\" // #488\n : isReasoningSequence\n ? \"responses reasoning sequence error (400)\"\n : \"failed after retries\";\n pluginLogger.warn(\n `${label}: model ${currentModel} ${reason}, trying fallback model ${modelsToTry[i + 1]}...`,\n );\n }\n }\n }\n }\n\n const finalError = lastError ?? new Error(\"All models failed\");\n const finalIs500 = is500Like(finalError);\n const finalIs404 = is404Like(finalError);\n const finalIs403 = is403Like(finalError); // #394: country/region restriction = operator config issue\n const finalIsQuota403 = is403QuotaOrRateLimitLike(finalError);\n const finalIs401 = is401OrWrapped(finalError); // #475: invalid API key = operator config issue\n const finalIsOOM = isOllamaOOM(finalError); // #387: OOM is expected when model too large for RAM\n const finalIs429 = is429OrWrapped(finalError); // #397\n const finalIsContextLength = isContextLengthError(finalError); // #488: input too long for model context window\n const finalIsReasoningSequence = isResponsesReasoningSequenceError(finalError); // #1034\n /** Unwraps LLMRetryError so \"Request was aborted\" in the cause is detected (#935, #936). */\n const finalIsTransientLlm = isAbortOrTransientLlmError(finalError);\n\n // When every model failed because provider keys are missing, queue a user-visible chat warning\n // and skip Sentry (this is a config issue, not a bug).\n if (unconfiguredCount > 0 && unconfiguredCount === modelsToTry.length) {\n const unconfiguredProviders = [...new Set(modelsToTry.map((m) => (m.includes(\"/\") ? m.split(\"/\")[0] : \"openai\")))];\n pendingWarnings?.add(\n `⚠️ Memory plugin: No LLM provider keys are configured for ${unconfiguredProviders.join(\", \")}. Memory features (HyDE search, classification, distillation) are degraded. Add API keys via: llm.providers.<provider>.apiKey in plugin config, then run: openclaw hybrid-mem verify --test-llm`,\n );\n } else if (unconfiguredCount > 0) {\n // Some models were unconfigured — warn user even if final error was 500/404\n pendingWarnings?.add(\n \"⚠️ Memory plugin: Some LLM provider keys are missing. \" +\n \"Add API keys via: llm.providers.<provider>.apiKey in plugin config, then run: openclaw hybrid-mem verify --test-llm\",\n );\n // Don't report UnconfiguredProviderError to GlitchTip — it's a config issue, not a code bug.\n // This can happen when the final model in the fallback chain is also unconfigured but an\n // earlier model failed for a different reason (e.g. rate limit), so unconfiguredCount < total.\n const finalIsUnconfigured =\n finalError instanceof UnconfiguredProviderError ||\n (finalError instanceof LLMRetryError && finalError.cause instanceof UnconfiguredProviderError);\n if (\n !finalIs500 &&\n !finalIsOOM &&\n !finalIsContextLength && // #488: context window exceeded = config issue, not a bug\n !finalIsUnconfigured &&\n !finalIsTransientLlm &&\n !finalIs403 &&\n !finalIsQuota403 &&\n !finalIs401 &&\n !finalIs429 &&\n !finalIsReasoningSequence\n ) {\n capturePluginError(finalError, {\n subsystem: \"chat\",\n operation: \"chatCompleteWithRetry\",\n phase: \"fallback-exhausted\",\n });\n }\n } else if (finalIsOOM) {\n // #387: OOM is a persistent condition (model too large for RAM), not transient — warn user to use smaller model\n pendingWarnings?.add(\n \"⚠️ Memory plugin: LLM model requires more memory than available (OOM). \" +\n \"Consider using a smaller model or configuring a cloud fallback. \" +\n \"Run: openclaw hybrid-mem verify --test-llm\",\n );\n } else if (finalIsContextLength) {\n // #488: input too long for model's context window — config issue (model too small), not a code bug\n pendingWarnings?.add(\n \"⚠️ Memory plugin: LLM input exceeds model context window. \" +\n \"Consider using a model with a larger context window or reducing input size. \" +\n \"Run: openclaw hybrid-mem verify --test-llm\",\n );\n } else if (finalIs500) {\n // #302: 500 server errors are transient — don't report to GlitchTip; request will be retried naturally\n } else if (finalIs404) {\n // #303: model not found across all fallbacks = misconfigured model name — surface to user, skip Sentry\n pendingWarnings?.add(\n \"⚠️ Memory plugin: LLM model not found (404) for all configured models. \" +\n \"Check model names in llm.default / llm.heavy / llm.nano config. \" +\n \"Run: openclaw hybrid-mem verify --test-llm\",\n );\n } else if (finalIs403) {\n // #394: country/region restriction / IP block = operator config issue, not a bug — skip GlitchTip\n pendingWarnings?.add(\n \"⚠️ Memory plugin: LLM access denied (403) — your API key may be restricted by country/region, \" +\n \"IP block, or billing. Check provider settings. \" +\n \"Run: openclaw hybrid-mem verify --test-llm\",\n );\n } else if (finalIs401) {\n // #475: invalid API key = operator config issue, not a bug — skip GlitchTip\n pendingWarnings?.add(\n \"⚠️ Memory plugin: LLM unauthorized (401) — your API key is invalid or expired. Check provider settings. \" +\n \"Run: openclaw hybrid-mem verify --test-llm\",\n );\n } else if (finalIsTransientLlm) {\n // #339, #703, #935, #936: abort/timeout/connection (including LLMRetryError-wrapped causes) — don't report\n } else if (finalIsQuota403) {\n pendingWarnings?.add(\n \"⚠️ Memory plugin: Provider quota or rate limit (403 with Retry-After / remaining-tokens). \" +\n \"Try again after the indicated window or raise quota.\",\n );\n } else if (finalIs429) {\n // #397: rate limit / usage limit — transient provider error, don't report to GlitchTip\n pendingWarnings?.add(\n \"⚠️ Memory plugin: LLM provider rate limited (429 Too Many Requests). \" +\n \"Memory features may be degraded. Try again later or upgrade your provider plan.\",\n );\n } else if (finalIsReasoningSequence) {\n // #1034: intermittent Responses API reasoning output sequencing failure — usually transient/provider-side\n pendingWarnings?.add(\n \"⚠️ Memory plugin: LLM provider returned malformed reasoning output (400). \" +\n \"This can be transient on reasoning models; a retry or fallback model is recommended.\",\n );\n } else {\n // Only report unexpected failures to Sentry — not pure config/key issues\n capturePluginError(finalError, {\n subsystem: \"chat\",\n operation: \"chatCompleteWithRetry\",\n phase: \"fallback-exhausted\",\n });\n }\n\n // Let callers (e.g. adaptive distill) attribute limits to the model that actually failed last.\n Object.assign(finalError, { lastAttemptedModel });\n\n throw finalError;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"chat.js","names":["getDistillBatchTokenLimitFromCatalog","getDistillMaxOutputTokensFromCatalog"],"sources":["../../services/chat.ts"],"sourcesContent":["/**\n * Unified chat completion for distill and other LLM features.\n * Uses a multi-provider OpenAI-compatible proxy; provider-agnostic model fallback (issue #87).\n */\n\nimport type { OpenAI } from \"openai\";\nimport { DEFAULT_CHAT_TIMEOUT_MS } from \"../utils/constants.js\";\nimport { pluginLogger } from \"../utils/logger.js\";\nimport { withCostFeature } from \"./cost-context.js\";\nimport { capturePluginError } from \"./error-reporter.js\";\nimport {\n formatProviderRateLimitHeaderSummary,\n is403QuotaOrRateLimitLike,\n parseRetryAfterMs,\n} from \"./llm-rate-limit-headers.js\";\nimport {\n type WireApi,\n getDistillBatchTokenLimit as getDistillBatchTokenLimitFromCatalog,\n getDistillMaxOutputTokens as getDistillMaxOutputTokensFromCatalog,\n requiresMaxCompletionTokens,\n shouldOmitSamplingParams,\n resolveWireApi,\n} from \"./model-capabilities.js\";\nimport { callResponsesApi } from \"./responses-adapter.js\";\nimport { recordProviderHttpAttempt, formatRecentHttpAttemptsForRateLimitLog } from \"./recent-http-attempts.js\";\n\nexport { is403QuotaOrRateLimitLike, parseGoDurationToMs, parseRetryAfterMs } from \"./llm-rate-limit-headers.js\";\n\n/**\n * Thrown when a model's provider has no API key or base URL configured in llm.providers.\n * chatCompleteWithRetry catches this and skips immediately to the next fallback model.\n */\nexport class UnconfiguredProviderError extends Error {\n readonly provider: string;\n readonly model: string;\n constructor(provider: string, model: string, detail?: string) {\n const base = `Provider '${provider}' is not configured for model ${model}.`;\n const hint = detail ?? `Set llm.providers.${provider}.apiKey in plugin config.`;\n super(`${base} ${hint}`);\n this.name = \"UnconfiguredProviderError\";\n this.provider = provider;\n this.model = model;\n }\n}\n\n/**\n * Pending user-visible LLM config warnings to inject into the next chat session.\n * Scoped per plugin instance to avoid cross-agent leakage.\n */\nexport type PendingLLMWarnings = {\n add: (message: string) => void;\n drain: () => string[];\n};\n\n/** Create a per-instance pending LLM warning queue. */\nexport function createPendingLLMWarnings(): PendingLLMWarnings {\n const pending = new Set<string>();\n return {\n add(message: string) {\n pending.add(message);\n },\n drain() {\n if (pending.size === 0) return [];\n const msgs = [...pending];\n pending.clear();\n return msgs;\n },\n };\n}\n\n/**\n * Unified 404 detection helper.\n * Checks the HTTP status code property first (reliable), then falls back to\n * targeted message pattern matching. Only matches \"model not found\" scenarios,\n * NOT generic \"file not found\" or \"module not found\" errors.\n *\n * Exported so embeddings.ts can suppress capturePluginError for 404 errors.\n */\nexport function is404Like(err: unknown): boolean {\n if (err && typeof err === \"object\") {\n // OpenAI SDK sets .status directly (number); also tolerate string \"404\" for robustness\n const status = (err as { status?: unknown }).status;\n if (status === 404 || status === \"404\") return true;\n }\n if (err instanceof Error) {\n // Match HTTP 404 patterns specifically — avoid false positives from\n // \"file not found\" or \"module not found\" strings in non-HTTP errors.\n return (\n /\\bHTTP\\s+404\\b|\\b404\\b.*not\\s+found|model.*not\\s+found|not\\s+found.*model/i.test(err.message) ||\n // Also match bare numeric 404 in error messages (e.g. \"404 Not Found\" from HTTP responses)\n /^\\b404\\b/.test(err.message.trim()) ||\n // OpenAI SDK formats: \"404 Model not found\" or \"Error code: 404\"\n /\\bError\\s+code:\\s*404\\b|\\b404\\s+[A-Za-z]/.test(err.message) ||\n // Google Generative Language API: \"models/<name> is not found for API version <v>\"\n // Occurs when a model is unavailable through a specific API version endpoint (e.g. v1beta/openai/).\n /is not found for api version/i.test(err.message)\n );\n }\n return false;\n}\n\n/**\n * 403 Forbidden / access-denied detection helper (geo, IP block, etc.).\n * Excludes {@link is403QuotaOrRateLimitLike} quota-style 403s.\n */\nexport function is403Like(err: unknown): boolean {\n if (err instanceof LLMRetryError) return is403Like(err.cause);\n // Quota / rate-limit style 403 (retry-after, remaining-tokens) is not geo \"forbidden\".\n if (is403QuotaOrRateLimitLike(err)) return false;\n if (err && typeof err === \"object\") {\n const status = (err as { status?: unknown }).status;\n if (status === 403 || status === \"403\") return true;\n }\n if (err instanceof Error) {\n // Match HTTP 403 patterns: \"403 Forbidden\", \"403 Country, region, or territory not supported\", etc.\n if (\n /^\\b403\\b/.test(err.message.trim()) ||\n /\\bHTTP\\s+403\\b|\\bError\\s+code:\\s*403\\b|\\b403\\s+[A-Za-z]/i.test(err.message)\n ) {\n return true;\n }\n // Match provider-specific geo-restriction / permission-denied phrases that arrive WITHOUT\n // a numeric \"403\" prefix when errors pass through a proxy or gateway.\n // #490: Google returns \"Country, region, or territory not supported\" (exact phrase) as\n // the message body when the API key / project is not permitted in the request's region.\n if (/\\bcountry,\\s*region,\\s*or\\s*territory\\s+not\\s+supported\\b/i.test(err.message)) return true;\n // gRPC PERMISSION_DENIED status mapped to HTTP 403 — some gateway implementations\n // embed the gRPC status name instead of (or in addition to) the HTTP code.\n if (/\\bPERMISSION_DENIED\\b/.test(err.message)) return true;\n // Generic \"access denied\" / \"access forbidden\" phrases without a numeric status prefix\n // that certain proxy implementations emit when forwarding a 403 response.\n if (/\\baccess\\s+(denied|forbidden)\\b/i.test(err.message) && !/\\b(file|directory|path|disk)\\b/i.test(err.message))\n return true;\n }\n return false;\n}\n\n/**\n * 401 Unauthorized / invalid api key detection helper.\n * A 401 is a permanent operator config issue (wrong API key) that will never be resolved by retrying.\n * Exported so other modules can treat 401 as a config error and suppress capturePluginError.\n */\nfunction is401Like(err: unknown): boolean {\n if (err && typeof err === \"object\") {\n const status = (err as { status?: unknown }).status;\n if (status === 401 || status === \"401\") return true;\n }\n if (err instanceof Error) {\n if (/\\b401\\b|unauthorized/i.test(err.message)) return true;\n // Also match specific auth failure phrases for robustness — catches errors from providers\n // that don't include \"401\" or \"unauthorized\" in the message and don't set a .status property.\n if (/incorrect api key|invalid api key|authentication failed/i.test(err.message)) return true;\n }\n return false;\n}\n\n/** Returns true when the error is a 401 (auth failure) — either directly or wrapped in LLMRetryError. */\nexport function is401OrWrapped(err: Error): boolean {\n if (is401Like(err)) return true;\n if (err instanceof LLMRetryError && is401Like(err.cause)) return true;\n return false;\n}\n\n/**\n * 429 Too Many Requests / rate-limit detection helper.\n * Checks the HTTP status code property first (reliable), then falls back to\n * message pattern matching. Rate limits are transient — suppress GlitchTip reporting.\n * Exported so embeddings.ts can suppress capturePluginError for 429 errors.\n */\nfunction is429Like(err: unknown): boolean {\n if (err && typeof err === \"object\") {\n const status = (err as { status?: unknown }).status;\n if (status === 429 || status === \"429\") return true;\n }\n if (err instanceof Error) {\n return (\n /^\\b429\\b/.test(err.message.trim()) ||\n /\\bHTTP\\s+429\\b|\\bError\\s+code:\\s*429\\b|\\b429\\s+[A-Za-z]/i.test(err.message) ||\n /\\btoo\\s+many\\s+requests\\b/i.test(err.message)\n );\n }\n return false;\n}\n\n/**\n * Returns true when the error is a 429 (rate limit) — either directly or wrapped in LLMRetryError.\n * Used in chatCompleteWithRetry to detect 429 errors that were retried and wrapped by withLLMRetry.\n * Exported so embeddings.ts can suppress capturePluginError for 429 errors.\n */\nexport function is429OrWrapped(err: Error): boolean {\n if (is429Like(err)) return true;\n if (err instanceof LLMRetryError && is429Like(err.cause)) return true;\n return false;\n}\n\n/** Returns true when the error is a ByteString serialization failure — direct or wrapped in LLMRetryError. */\nexport function isByteStringOrWrapped(err: Error): boolean {\n if (isByteStringSerializationError(err)) return true;\n if (err instanceof LLMRetryError && isByteStringSerializationError(err.cause)) return true;\n return false;\n}\n\n/**\n * Unified 5xx / internal server error detection helper.\n * Checks HTTP status code property first, then uses conservative message patterns.\n * Avoids false positives from non-HTTP \"internal error\" messages (e.g. JavaScript errors).\n *\n * Exported so other modules (lifecycle/hooks, auto-classifier) can suppress\n * capturePluginError for transient server errors.\n */\nexport function is500Like(err: unknown): boolean {\n if (err && typeof err === \"object\") {\n const status = (err as { status?: unknown }).status;\n if (typeof status === \"number\" && status >= 500 && status < 600) return true;\n }\n if (err instanceof Error) {\n const m = err.message;\n // Only match HTTP 5xx patterns — not generic \"internal error\" from JS\n if (\n /\\bHTTP\\s+5\\d{2}\\b|\\b5\\d{2}\\s+(internal\\s+)?(error|status)|status\\s+5\\d{2}|internal\\s+server\\s+error/i.test(m)\n ) {\n return true;\n }\n // Gateway/proxy phrasing seen in production (#1010, #1013): \"502 error code: 502\"\n // Single pattern: any \"error code: 5xx\" also covers \"5xx error code: 5xx\".\n if (/\\berror\\s+code:\\s*5\\d{2}\\b/i.test(m)) return true;\n }\n return false;\n}\n\n/** Returns true when the error is a 5xx server error — either directly or wrapped in LLMRetryError. */\nexport function is500OrWrapped(err: Error): boolean {\n if (is500Like(err)) return true;\n if (err instanceof LLMRetryError && is500Like(err.cause)) return true;\n return false;\n}\n\n/**\n * Detect transient SDK/network connection failures.\n *\n * OpenAI-compatible SDKs may surface connectivity problems as a bare\n * \"Connection error.\" message (e.g. APIConnectionError) without exposing the\n * underlying socket code on the top-level Error object. These are transient\n * transport failures, not plugin logic bugs, so callers should treat them like\n * timeouts/ECONNREFUSED for retry, fallback, and GlitchTip suppression.\n */\nexport function isConnectionErrorLike(err: unknown): boolean {\n if (err instanceof LLMRetryError) return isConnectionErrorLike(err.cause);\n\n const candidates: unknown[] = [err];\n if (err && typeof err === \"object\" && \"cause\" in err) {\n candidates.push((err as { cause?: unknown }).cause);\n }\n\n for (const candidate of candidates) {\n if (!candidate || typeof candidate !== \"object\") continue;\n\n const code = (candidate as { code?: unknown }).code;\n if (\n typeof code === \"string\" &&\n /ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENOTFOUND|EHOSTUNREACH|UND_ERR_CONNECT_TIMEOUT/i.test(code)\n ) {\n return true;\n }\n\n const name = (candidate as { name?: unknown }).name;\n if (typeof name === \"string\" && /APIConnectionError/i.test(name)) {\n return true;\n }\n }\n\n if (!(err instanceof Error)) return false;\n\n return (\n /\\bconnection error\\b/i.test(err.message) ||\n /\\bnetwork error\\b/i.test(err.message) ||\n /ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENOTFOUND|EHOSTUNREACH|connect\\s+ETIMEDOUT|socket hang up|fetch failed/i.test(\n err.message,\n )\n );\n}\n\n/**\n * Detect 400 errors caused by exceeding the model's context length.\n * These are unrecoverable without truncating the input — retrying is wasteful (#442).\n * Pattern matches OpenAI's error: \"400 Invalid 'input': maximum context length is 8192 tokens.\"\n * Also matches Ollama's error: \"Input length 768 exceeds maximum allowed token size 512\" (#488).\n */\nexport function isContextLengthError(err: unknown): boolean {\n if (err && typeof err === \"object\") {\n const status = (err as { status?: unknown }).status;\n if (typeof status === \"number\" && status === 400) {\n const msg = ((err as { message?: string }).message ?? \"\").toLowerCase();\n if (\n msg.includes(\"context length\") ||\n msg.includes(\"maximum context\") ||\n /max.*token.*(length|limit)|token limit|context.length/i.test(msg) ||\n // #488: Ollama \"Input length 768 exceeds maximum allowed token size 512\"\n /input\\s+length\\s+\\d+\\s+exceeds/i.test(msg)\n ) {\n return true;\n }\n }\n }\n if (err instanceof Error) {\n const msg = err.message.toLowerCase();\n return (\n ((msg.includes(\"400\") || msg.includes(\"bad request\")) &&\n (msg.includes(\"context length\") ||\n msg.includes(\"maximum context\") ||\n /max.*token.*(length|limit)|token limit|context.length/i.test(err.message))) ||\n /\\b400\\b.*maximum context length/i.test(err.message) ||\n // #488: Ollama message-only — no \"400\" prefix or .status property.\n // This pattern intentionally appears three times: some SDKs set .status=400 (first block),\n // others embed \"400\" in the message (second block), others emit message-only (this block).\n /input\\s+length\\s+\\d+\\s+exceeds/i.test(err.message)\n );\n }\n return false;\n}\n\n/**\n * Mutates and returns `err` so GlitchTip/async stacks show which model/phase failed (#1010–#1011).\n */\nfunction enrichLlmErrorMessage(err: Error, ctx?: { model?: string; operation?: string }): Error {\n if (!ctx?.model && !ctx?.operation) return err;\n if (/\\[llm\\s+/i.test(err.message)) return err;\n const parts: string[] = [];\n if (ctx.model) parts.push(`model=${ctx.model}`);\n if (ctx.operation) parts.push(`op=${ctx.operation}`);\n err.message = `${err.message} [llm ${parts.join(\", \")}]`;\n return err;\n}\n\n/**\n * HTTP 400 that is not a context-length violation — retrying will not help (#1011, #1016).\n */\nfunction isNonRetryableClient400(err: unknown): boolean {\n if (isContextLengthError(err)) return false;\n if (err && typeof err === \"object\") {\n const status = (err as { status?: unknown }).status;\n if (status === 400 || status === \"400\") return true;\n }\n if (err instanceof Error) {\n const m = err.message;\n if (/\\b400\\b.*\\b(?:no body|status code)\\b/i.test(m)) return true;\n }\n return false;\n}\n\n/** Gateway/proxy may return HTTP 400 with an empty body — transient, not a plugin bug (#1157). */\nfunction is400EmptyBodyGatewayError(err: unknown): boolean {\n if (!(err instanceof Error)) return false;\n return /\\b400\\b.*\\bno body\\b/i.test(err.message);\n}\n\n/**\n * True when the error is a Node.js / browser-fetch ByteString serialization failure.\n * Thrown by the HTTP layer when a header value contains a character with code point > 255\n * (e.g. U+2026 HORIZONTAL ELLIPSIS in a copy-pasted API key, model name, or custom header).\n * These errors are permanent — retrying the same request will always fail (#1776).\n */\nexport function isByteStringSerializationError(err: unknown): boolean {\n if (!(err instanceof Error)) return false;\n return /cannot convert argument to a bytestring/i.test(err.message);\n}\n\n/** Some Azure/Foundry deployments return 400 when chat params are not supported for the model (#1165). */\nfunction is400UnsupportedOperationError(err: unknown): boolean {\n if (!(err instanceof Error)) return false;\n return /\\b400\\b.*\\bunsupported\\b/i.test(err.message);\n}\n\n/**\n * Detect malformed Responses API reasoning-output sequencing errors, e.g.:\n * \"Item 'rs_...' of type 'reasoning' was provided without its required following item.\"\n *\n * This is an intermittent provider-side 400 seen with some reasoning deployments (notably some\n * Azure Foundry o3-pro paths). Treat as transient/retryable (limited) instead of permanent bad-request.\n *\n * Unwraps {@link LLMRetryError} and checks `cause` so detection still works when the message is wrapped.\n */\nexport function isResponsesReasoningSequenceError(err: unknown): boolean {\n const msg = collectErrorMessageChain(err);\n return /\\btype\\s+['\"]?reasoning['\"]?\\s+was\\s+provided\\s+without\\s+(?:its\\s+)?required\\s+following\\s+item\\b/i.test(\n msg,\n );\n}\n\nfunction collectErrorMessageChain(err: unknown): string {\n const parts: string[] = [];\n let cur: unknown = err;\n const seen = new Set<unknown>();\n for (let i = 0; i < 8 && cur != null && !seen.has(cur); i++) {\n seen.add(cur);\n if (cur instanceof Error) {\n parts.push(cur.message);\n cur = cur.cause;\n continue;\n }\n if (\n typeof cur === \"object\" &&\n cur !== null &&\n \"message\" in cur &&\n typeof (cur as { message: unknown }).message === \"string\"\n ) {\n parts.push((cur as { message: string }).message);\n break;\n }\n break;\n }\n return parts.join(\" \");\n}\n\n/**\n * Detect Ollama out-of-memory (OOM) errors from the model server.\n * Ollama returns HTTP 500 with a body like:\n * \"model requires more system memory (18.2 GiB) than is available (8.0 GiB)\"\n * These are expected failures when the configured model is too large for the host —\n * not bugs — so callers should skip capturePluginError and warn the user instead.\n *\n * Exported so other modules (lifecycle/hooks, auto-classifier, embeddings) can\n * detect OOM specifically for user-visible warnings and circuit-breaker decisions.\n */\nexport function isOllamaOOM(err: unknown): boolean {\n if (!(err instanceof Error)) return false;\n const msg = err.message.toLowerCase();\n return (\n msg.includes(\"model requires more system memory\") ||\n msg.includes(\"not enough memory to load\") ||\n // \"requires X GiB\" pattern covers variant phrasings from different Ollama versions\n /\\bmodel\\s+requires\\s+[\\d.]+\\s*gib/i.test(err.message) ||\n // Bare OOM signal in error body (e.g. \"oom: model 'qwen3:8b' ...\")\n /\\boom:/i.test(err.message)\n );\n}\n\nexport async function chatComplete(opts: {\n model: string;\n content: string;\n temperature?: number;\n maxTokens?: number;\n openai: OpenAI;\n /** Timeout in ms; after this the promise rejects. Prevents silent hang when gateway/LLM never responds. Default 45s. */\n timeoutMs?: number;\n /** When aborted (e.g. parent timeout), the request is cancelled and no retry is needed. */\n signal?: AbortSignal;\n /** Feature label for cost tracking. When set, the call is wrapped in withCostFeature() so the proxy records the correct label. */\n feature?: string;\n /** Force a specific wire API surface (\"chat\" or \"responses\"). When unset, resolved from the model's provider prefix. */\n wireApi?: WireApi;\n}): Promise<string> {\n const {\n model,\n content,\n temperature = 0.2,\n maxTokens,\n timeoutMs = DEFAULT_CHAT_TIMEOUT_MS,\n signal,\n feature,\n wireApi: wireApiOverride,\n } = opts;\n const effectiveMaxTokens = maxTokens ?? distillMaxOutputTokens(model);\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeoutMs);\n const onAbort = () => controller.abort();\n if (signal) {\n if (signal.aborted) {\n clearTimeout(timeoutId);\n throw new Error(\"Request was aborted\");\n }\n signal.addEventListener(\"abort\", onAbort);\n }\n\n try {\n const wireApi = resolveWireApi(model, wireApiOverride);\n\n if (wireApi === \"responses\") {\n const doCreate = () =>\n callResponsesApi(\n opts.openai,\n { model, content, temperature, maxTokens: effectiveMaxTokens },\n { signal: controller.signal },\n );\n const { text } = await (feature ? withCostFeature(feature, doCreate) : doCreate());\n clearTimeout(timeoutId);\n if (signal) signal.removeEventListener(\"abort\", onAbort);\n return text;\n }\n\n // Standard chat.completions.create path\n const useMaxCompletionTokens = requiresMaxCompletionTokens(model);\n const body: OpenAI.ChatCompletionCreateParamsNonStreaming = {\n model,\n messages: [{ role: \"user\", content }],\n ...(useMaxCompletionTokens ? { max_completion_tokens: effectiveMaxTokens } : { max_tokens: effectiveMaxTokens }),\n };\n if (!shouldOmitSamplingParams(model)) {\n body.temperature = temperature;\n }\n const doCreate = () =>\n opts.openai.chat.completions.create(body as unknown as Parameters<OpenAI[\"chat\"][\"completions\"][\"create\"]>[0], {\n signal: controller.signal,\n });\n // If feature is provided, wrap in withCostFeature so the proxy attributes the call correctly.\n // Cost recording itself is done by the OpenAI proxy in setup/init-databases.ts.\n const resp = (await (feature ? withCostFeature(feature, doCreate) : doCreate())) as OpenAI.Chat.ChatCompletion;\n clearTimeout(timeoutId);\n if (signal) signal.removeEventListener(\"abort\", onAbort);\n const msg = resp.choices?.[0]?.message;\n const msgContent = msg?.content?.trim();\n if (msgContent) return msgContent;\n // Qwen3 thinking mode (Ollama OpenAI-compat endpoint) puts the response in\n // message.reasoning_content (current standard, May 2025+) or message.reasoning (legacy).\n // Fall back to these fields when enable_thinking=true so agents don't see an empty reply (#314).\n const msgRecord = msg as unknown as Record<string, unknown> | undefined;\n const reasoningContent = msgRecord?.reasoning_content;\n if (typeof reasoningContent === \"string\" && reasoningContent.trim()) return reasoningContent.trim();\n const reasoning = msgRecord?.reasoning;\n if (typeof reasoning === \"string\" && reasoning.trim()) return reasoning.trim();\n return msgContent ?? \"\";\n } catch (err) {\n clearTimeout(timeoutId);\n if (signal) signal.removeEventListener(\"abort\", onAbort);\n const isAbort = err instanceof Error && err.name === \"AbortError\";\n const error = isAbort\n ? new Error(`LLM request timeout after ${timeoutMs}ms (model: ${model})`)\n : err instanceof Error\n ? enrichLlmErrorMessage(err, { model, operation: \"chatComplete\" })\n : new Error(String(err));\n // Skip reporting known transient gateway/LLM errors (aborted, timeout, 5xx, OOM, 429) and config errors (missing provider keys) to avoid GlitchTip noise\n const msg = error.message.toLowerCase();\n const isTransient =\n msg.includes(\"request was aborted\") ||\n msg.includes(\"operation was aborted\") ||\n msg.includes(\"request timed out\") ||\n msg.includes(\"timed out\") ||\n msg.includes(\"llm request timeout\") || // #339: our own timeout message uses \"timeout\" not \"timed out\"\n msg.includes(\"econnrefused\") ||\n isAbortOrTransientLlmError(err) || // #1694: recursive cause-chain check catches nested AbortError (e.g. MiniMax wraps abort in outer error)\n isConnectionErrorLike(err) || // #703: OpenAI SDK APIConnectionError / \"Connection error.\" is transient\n is429Like(err) || // #397: rate limit is transient\n is403QuotaOrRateLimitLike(err) || // Azure/APIM quota as 403 + headers\n /^\\d+\\s*internal\\s*error$/i.test(msg.trim()) ||\n /^5\\d{2}\\s/.test(msg.trim()) ||\n is500Like(err) || // #302: OpenAI SDK InternalServerError has no numeric prefix\n isOllamaOOM(err) || // #387: Ollama OOM — model too large for available RAM, not a bug\n isResponsesReasoningSequenceError(err) || // #1034: malformed reasoning item sequence — retryable\n is400EmptyBodyGatewayError(error) ||\n is400EmptyBodyGatewayError(err) || // #1157: 400 (no body) from gateway — do not GlitchTip\n is400UnsupportedOperationError(error) ||\n is400UnsupportedOperationError(err); // #1165: model/endpoint mismatch\n const isConfigError =\n err instanceof UnconfiguredProviderError ||\n is404Like(err) || // #303: model not found = wrong model name in config, not a bug\n is403Like(err) || // #394: country/region restriction = operator config issue, not a bug\n is401Like(err) || // #475: invalid API key = operator config issue, not a bug\n isContextLengthError(err) || // #488: input too long for model context window = wrong model choice, not a bug\n isByteStringSerializationError(err); // #1776: non-ASCII in HTTP header = config issue, not a bug\n if (!isTransient && !isConfigError) {\n capturePluginError(error, {\n subsystem: \"chat\",\n operation: \"chatComplete\",\n phase: \"gateway\",\n });\n }\n throw error;\n }\n}\n\n/** Max input tokens for one distill batch request. From model-capabilities catalog (docs/MODEL-REFERENCE.md). */\nexport function distillBatchTokenLimit(model: string): number {\n return getDistillBatchTokenLimitFromCatalog(model);\n}\n\n/** Max output tokens for distill/ingest LLM calls. From model-capabilities catalog (docs/MODEL-REFERENCE.md). */\nexport function distillMaxOutputTokens(model: string): number {\n return getDistillMaxOutputTokensFromCatalog(model);\n}\n\n/**\n * Custom error class that includes retry attempt information.\n */\nexport class LLMRetryError extends Error {\n constructor(\n message: string,\n public readonly cause: Error,\n public readonly attemptNumber: number,\n ) {\n super(message);\n this.name = \"LLMRetryError\";\n }\n}\n\n/**\n * True when an LLM call failed for abort, gateway loss, or transport — not plugin logic.\n * Used by session narrative and similar paths to avoid noisy warns when the gateway stops.\n */\nexport function isAbortOrTransientLlmError(err: unknown): boolean {\n if (err instanceof LLMRetryError) {\n return isAbortOrTransientLlmError(err.cause);\n }\n if (err && typeof err === \"object\" && \"cause\" in err) {\n const c = (err as { cause?: unknown }).cause;\n if (c !== undefined && c !== null && isAbortOrTransientLlmError(c)) return true;\n }\n if (!(err instanceof Error)) {\n return isConnectionErrorLike(err);\n }\n if (err.name === \"AbortError\") return true;\n const msg = err.message;\n if (/request was aborted|Request was aborted|The operation was aborted|operation was aborted/i.test(msg)) return true;\n if (/gateway client stopped|gateway not reachable|not reachable\\.|is it running/i.test(msg)) return true;\n return isConnectionErrorLike(err);\n}\n\n/**\n * Retry wrapper for LLM calls with exponential backoff.\n * Retries on failure with increasing delays: 1s, 3s, 9s.\n * On final failure, throws LLMRetryError with attempt number.\n * When signal is provided and aborted, does not retry (fails immediately).\n */\nexport async function withLLMRetry<T>(\n fn: () => Promise<T>,\n opts?: {\n maxRetries?: number;\n signal?: AbortSignal;\n /** Tags errors for triage when they surface at async boundaries (#1010, #1011). */\n llmContext?: { model?: string; operation?: string };\n },\n): Promise<T> {\n const maxRetries = opts?.maxRetries ?? 3;\n let lastError: Error | undefined;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n recordProviderHttpAttempt({\n operation: opts?.llmContext?.operation,\n model: opts?.llmContext?.model,\n });\n try {\n return await fn();\n } catch (err) {\n lastError = err instanceof Error ? err : new Error(String(err));\n // Don't retry UnconfiguredProviderError — missing API keys won't be fixed by retrying\n if (lastError instanceof UnconfiguredProviderError) {\n throw lastError;\n }\n // Don't retry 401 — wrong key won't be fixed by retrying\n if (is401Like(lastError)) {\n throw lastError;\n }\n // Don't retry 403 — access forbidden (country/region restriction, IP block, billing) won't be fixed by retrying (#394)\n if (is403Like(lastError)) {\n throw lastError;\n }\n // Don't retry 404 — model doesn't exist, let chatCompleteWithRetry try next model\n if (is404Like(lastError)) {\n const modelHint = lastError.message.match(/model[:\\s]+(\\S+)/i)?.[1];\n pluginLogger.warn(\n `memory-hybrid: Model not found (404)${modelHint ? ` for ${modelHint}` : \"\"} — check model name or provider availability`,\n );\n throw lastError;\n }\n // Don't retry 400 context-length errors — input was too long; retrying won't fix it (#442)\n if (isContextLengthError(lastError)) {\n pluginLogger.warn(\n \"memory-hybrid: Input exceeds model context length — retrying will not help; truncate input before calling\",\n );\n throw enrichLlmErrorMessage(lastError, opts?.llmContext);\n }\n // Don't retry ByteString serialization errors — a non-ASCII character (e.g. U+2026 ELLIPSIS)\n // in an HTTP header value (API key, model name, custom header) cannot be fixed by retrying (#1776).\n if (isByteStringOrWrapped(lastError)) {\n throw enrichLlmErrorMessage(lastError, opts?.llmContext);\n }\n const isReasoningSequenceError = isResponsesReasoningSequenceError(lastError);\n // Azure/OpenAI Responses API intermittent malformed reasoning item sequence:\n // allow one retry, then fail fast so fallback model can run.\n if (isReasoningSequenceError && attempt >= 1) {\n throw enrichLlmErrorMessage(lastError, opts?.llmContext);\n }\n // Other 400s (e.g. empty body from gateway) — not fixed by retry (#1011, #1016)\n if (!isReasoningSequenceError && isNonRetryableClient400(lastError)) {\n throw enrichLlmErrorMessage(lastError, opts?.llmContext);\n }\n const is429 = is429Like(lastError);\n const isQuota403 = is403QuotaOrRateLimitLike(lastError);\n // Timeouts: only retry once (attempt 0 → attempt 1), then throw so chatCompleteWithRetry can try next model.\n // (attempt is 0-based: attempt >= 1 means we've already retried once.)\n const isTimeout = /timed out|llm request timeout|request was aborted|Request was aborted/i.test(\n lastError.message,\n ); // #339: include our own \"LLM request timeout\" pattern\n const isConnectionError = isConnectionErrorLike(lastError);\n if ((isTimeout || isConnectionError) && attempt >= 1) {\n throw lastError;\n }\n // Ollama OOM: never retry — model requires more memory than available, won't be fixed by retrying.\n // chatCompleteWithRetry will try the next fallback model (e.g. Gemini, OpenAI).\n if (isOllamaOOM(lastError)) {\n pluginLogger.warn(\n \"memory-hybrid: Ollama model OOM — model requires more memory than is available. Skipping retries; will try next fallback model.\",\n );\n throw lastError;\n }\n // 5xx / internal server error: only retry once\n const isServerError = is500Like(lastError);\n if (isServerError && attempt >= 1) {\n throw lastError;\n }\n if (attempt === maxRetries || opts?.signal?.aborted) {\n // Capture causeMsg before enrichment to preserve end-anchored regex matching\n const causeMsg = lastError.message.toLowerCase();\n enrichLlmErrorMessage(lastError, opts?.llmContext);\n const retryError = new LLMRetryError(\n `Failed after ${attempt + 1} attempts: ${lastError.message}`,\n lastError,\n attempt + 1,\n );\n // Skip reporting when the underlying cause is a transient gateway error (aborted, timeout, 5xx, 429).\n // Note: 404 and 403 errors should never reach this branch (they exit early above),\n // but we include them as defensive safety nets in case they slip past due to future refactors.\n const fullMsg = retryError.message.toLowerCase();\n const isTransient =\n is429 ||\n is403QuotaOrRateLimitLike(lastError) ||\n isServerError || // #302: 5xx server errors are transient\n is404Like(lastError) || // #329: defensive safety net — 404 = model not found, config issue, not a bug\n is403Like(lastError) || // #394: defensive safety net — 403 = country/region restriction, config issue, not a bug\n is401Like(lastError) || // #475: defensive safety net — 401 = invalid API key, config issue, not a bug\n causeMsg.includes(\"request was aborted\") ||\n fullMsg.includes(\"request was aborted\") ||\n causeMsg.includes(\"request timed out\") ||\n fullMsg.includes(\"request timed out\") ||\n causeMsg.includes(\"timed out\") ||\n fullMsg.includes(\"timed out\") ||\n causeMsg.includes(\"llm request timeout\") || // #339: our own timeout message uses \"timeout\" not \"timed out\"\n fullMsg.includes(\"llm request timeout\") ||\n isAbortOrTransientLlmError(lastError) || // #1694: recursive cause-chain check catches nested AbortError (e.g. MiniMax wraps abort in outer error)\n isConnectionErrorLike(lastError) ||\n /^\\d+\\s*internal\\s*error$/i.test(causeMsg.trim()) ||\n /^5\\d{2}\\s/.test(causeMsg.trim()) ||\n /\\b405\\s+method\\s+not\\s+allowed/i.test(causeMsg) ||\n /\\b405\\s+method\\s+not\\s+allowed/i.test(fullMsg) ||\n isResponsesReasoningSequenceError(lastError) || // #1034\n is400EmptyBodyGatewayError(lastError) || // #1157\n is400UnsupportedOperationError(lastError); // #1165\n if (!isTransient) {\n capturePluginError(retryError, {\n subsystem: \"chat\",\n operation: \"withLLMRetry\",\n retryAttempt: attempt + 1,\n ...(opts?.llmContext?.model ? { configShape: { model: opts.llmContext.model } } : {}),\n });\n }\n throw retryError;\n }\n\n // 429 / quota-style 403: respect Retry-After header if present; otherwise use exponential backoff (2s → 4s → 8s)\n let delay: number;\n if (is429 || isQuota403) {\n const retryAfterMs = parseRetryAfterMs(err);\n delay = retryAfterMs ?? 2 ** (attempt + 1) * 1000;\n const op = opts?.llmContext?.operation;\n const modelId = opts?.llmContext?.model;\n const ctxTag = op || modelId ? ` [${[op, modelId].filter(Boolean).join(\" · \")}]` : \"\";\n const hdr = formatProviderRateLimitHeaderSummary(err);\n const recent = formatRecentHttpAttemptsForRateLimitLog();\n const attemptTotal = Math.max(1, maxRetries);\n pluginLogger.warn(\n `memory-hybrid: ${isQuota403 ? \"Quota/rate limit (403)\" : \"Rate limited by provider\"}${ctxTag} — retry ${attempt + 1}/${attemptTotal}, backing off ${delay}ms | ${hdr} | ${recent} | rolling counts = outbound HTTP attempts in this process (not token usage); use tokRem/reqRem when headers exist.`,\n );\n } else {\n delay = 3 ** attempt * 1000; // 1s, 3s, 9s\n }\n // Abort-aware backoff sleep: if the signal fires while we are waiting, reject immediately\n // instead of sleeping through the full delay. The listener is removed on normal resolve to\n // prevent leaks; the { once: true } option is not relied on alone for cleanup.\n await new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timeout);\n const reason = opts?.signal?.reason;\n const msg = reason instanceof Error ? reason.message : reason != null ? String(reason) : \"Aborted\";\n const abortError = new Error(msg);\n abortError.name = \"AbortError\";\n reject(abortError);\n };\n const timeout = setTimeout(() => {\n opts?.signal?.removeEventListener(\"abort\", onAbort);\n resolve();\n }, delay);\n if (opts?.signal) {\n if (opts.signal.aborted) {\n onAbort();\n } else {\n opts.signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n }\n });\n }\n }\n throw new Error(\"unreachable\");\n}\n\n/**\n * Wrapper for chatComplete with retry and fallback model support.\n * Tries primary model with retries, then falls back to each fallback model in order.\n * All calls go through the gateway (openai client).\n */\nexport async function chatCompleteWithRetry(opts: {\n model: string;\n content: string;\n temperature?: number;\n maxTokens?: number;\n openai: OpenAI;\n fallbackModels?: string[];\n label?: string;\n /** Timeout per model attempt (passed to chatComplete). Default 45s. */\n timeoutMs?: number;\n /** When aborted (e.g. parent step timeout), the request is cancelled and no fallback models are tried. */\n signal?: AbortSignal;\n /** Optional per-instance warning queue for missing provider keys. */\n pendingWarnings?: PendingLLMWarnings;\n /** Feature label for cost tracking. Passed to chatComplete which wraps the call in withCostFeature(). */\n feature?: string;\n}): Promise<string> {\n return (await chatCompleteWithRetryDetailed(opts)).content;\n}\n\nexport type ChatCompleteWithRetryDetails = {\n content: string;\n modelUsed: string;\n /**\n * Effective model attempt order (primary then fallbacks). This is the full chain that was eligible\n * for attempts; some models may still have been skipped due to AbortSignal pre-checks.\n */\n attemptChain: string[];\n};\n\n/**\n * Like {@link chatCompleteWithRetry} but returns which model actually produced the response.\n * Used by maintenance commands to log the effective model selected after fallbacks.\n */\nexport async function chatCompleteWithRetryDetailed(opts: {\n model: string;\n content: string;\n temperature?: number;\n maxTokens?: number;\n openai: OpenAI;\n fallbackModels?: string[];\n label?: string;\n /** Timeout per model attempt (passed to chatComplete). Default 45s. */\n timeoutMs?: number;\n /** When aborted (e.g. parent step timeout), the request is cancelled and no fallback models are tried. */\n signal?: AbortSignal;\n /** Optional per-instance warning queue for missing provider keys. */\n pendingWarnings?: PendingLLMWarnings;\n /** Feature label for cost tracking. Passed to chatComplete which wraps the call in withCostFeature(). */\n feature?: string;\n}): Promise<ChatCompleteWithRetryDetails> {\n const {\n fallbackModels = [],\n label: rawLabel,\n maxTokens,\n timeoutMs,\n signal,\n pendingWarnings,\n feature,\n ...chatOpts\n } = opts;\n const label = rawLabel ?? \"LLM call\";\n const modelsToTry = [opts.model, ...fallbackModels];\n\n let lastError: Error | undefined;\n let unconfiguredCount = 0;\n let lastAttemptedModel = opts.model;\n\n for (let i = 0; i < modelsToTry.length; i++) {\n if (signal?.aborted) {\n const reason = (signal as AbortSignal).reason;\n const msg = reason instanceof Error ? reason.message : reason != null ? String(reason) : \"Aborted\";\n const abortError = new Error(msg);\n abortError.name = \"AbortError\";\n throw abortError;\n }\n const currentModel = modelsToTry[i];\n lastAttemptedModel = currentModel;\n const _isFallback = i > 0;\n // Use per-model max_tokens so fallbacks (e.g. gpt-4o) don't receive primary model's limit (e.g. 65k for Gemini)\n const effectiveMaxTokens = maxTokens ?? distillMaxOutputTokens(currentModel);\n\n try {\n const content = await withLLMRetry(\n () =>\n chatComplete({\n ...chatOpts,\n model: currentModel,\n maxTokens: effectiveMaxTokens,\n ...(timeoutMs != null && { timeoutMs }),\n signal,\n ...(feature != null && { feature }),\n }),\n {\n maxRetries: 3,\n signal,\n llmContext: { model: currentModel, operation: label },\n },\n );\n return { content, modelUsed: currentModel, attemptChain: modelsToTry };\n } catch (err) {\n lastError = err instanceof Error ? err : new Error(String(err));\n // Check both direct UnconfiguredProviderError and wrapped in LLMRetryError\n const isUnconfigured =\n lastError instanceof UnconfiguredProviderError ||\n (lastError instanceof LLMRetryError && lastError.cause instanceof UnconfiguredProviderError);\n const is429 = is429OrWrapped(lastError);\n const isTimeout = /timed out|llm request timeout|request was aborted|Request was aborted/i.test(\n lastError.message,\n ); // #339: include our own \"LLM request timeout\" pattern\n const isConnectionError = isConnectionErrorLike(lastError);\n const is404 = is404Like(lastError);\n const is403 = is403Like(lastError);\n const isQuota403 = is403QuotaOrRateLimitLike(lastError);\n const is401 = is401Like(lastError);\n const is500 = is500Like(lastError); // #302\n const isContextLength = isContextLengthError(lastError); // #488\n const isReasoningSequence = isResponsesReasoningSequenceError(lastError); // #1034\n if (isUnconfigured) unconfiguredCount++;\n if (i < modelsToTry.length - 1 && !signal?.aborted) {\n if (!isUnconfigured) {\n const reason = is429\n ? \"rate limited (429)\"\n : isTimeout\n ? \"timed out\"\n : isConnectionError\n ? \"connection failed\"\n : is404\n ? \"model not found (404)\"\n : isQuota403\n ? \"quota / rate limit (403)\"\n : is403\n ? \"access denied (403)\"\n : is401\n ? \"unauthorized (401)\"\n : is500\n ? \"server error (500)\" // #302\n : isContextLength\n ? \"input too long\" // #488\n : isReasoningSequence\n ? \"responses reasoning sequence error (400)\"\n : \"failed after retries\";\n pluginLogger.warn(\n `${label}: model ${currentModel} ${reason}, trying fallback model ${modelsToTry[i + 1]}...`,\n );\n }\n }\n }\n }\n\n const finalError = lastError ?? new Error(\"All models failed\");\n const finalIs500 = is500Like(finalError);\n const finalIs404 = is404Like(finalError);\n const finalIs403 = is403Like(finalError); // #394: country/region restriction = operator config issue\n const finalIsQuota403 = is403QuotaOrRateLimitLike(finalError);\n const finalIs401 = is401OrWrapped(finalError); // #475: invalid API key = operator config issue\n const finalIsOOM = isOllamaOOM(finalError); // #387: OOM is expected when model too large for RAM\n const finalIs429 = is429OrWrapped(finalError); // #397\n const finalIsContextLength = isContextLengthError(finalError); // #488: input too long for model context window\n const finalIsByteString = isByteStringOrWrapped(finalError); // #1776: non-ASCII in HTTP header = config issue\n const finalIsReasoningSequence = isResponsesReasoningSequenceError(finalError); // #1034\n /** Unwraps LLMRetryError so \"Request was aborted\" in the cause is detected (#935, #936). */\n const finalIsTransientLlm = isAbortOrTransientLlmError(finalError);\n\n // When every model failed because provider keys are missing, queue a user-visible chat warning\n // and skip Sentry (this is a config issue, not a bug).\n if (unconfiguredCount > 0 && unconfiguredCount === modelsToTry.length) {\n const unconfiguredProviders = [...new Set(modelsToTry.map((m) => (m.includes(\"/\") ? m.split(\"/\")[0] : \"openai\")))];\n pendingWarnings?.add(\n `⚠️ Memory plugin: No LLM provider keys are configured for ${unconfiguredProviders.join(\", \")}. Memory features (HyDE search, classification, distillation) are degraded. Add API keys via: llm.providers.<provider>.apiKey in plugin config, then run: openclaw hybrid-mem verify --test-llm`,\n );\n } else if (unconfiguredCount > 0) {\n // Some models were unconfigured — warn user even if final error was 500/404\n pendingWarnings?.add(\n \"⚠️ Memory plugin: Some LLM provider keys are missing. \" +\n \"Add API keys via: llm.providers.<provider>.apiKey in plugin config, then run: openclaw hybrid-mem verify --test-llm\",\n );\n // Don't report UnconfiguredProviderError to GlitchTip — it's a config issue, not a code bug.\n // This can happen when the final model in the fallback chain is also unconfigured but an\n // earlier model failed for a different reason (e.g. rate limit), so unconfiguredCount < total.\n const finalIsUnconfigured =\n finalError instanceof UnconfiguredProviderError ||\n (finalError instanceof LLMRetryError && finalError.cause instanceof UnconfiguredProviderError);\n if (\n !finalIs500 &&\n !finalIsOOM &&\n !finalIsContextLength && // #488: context window exceeded = config issue, not a bug\n !finalIsByteString && // #1776: non-ASCII in HTTP header = config issue, not a bug\n !finalIsUnconfigured &&\n !finalIsTransientLlm &&\n !finalIs403 &&\n !finalIsQuota403 &&\n !finalIs401 &&\n !finalIs429 &&\n !finalIsReasoningSequence\n ) {\n capturePluginError(finalError, {\n subsystem: \"chat\",\n operation: \"chatCompleteWithRetry\",\n phase: \"fallback-exhausted\",\n });\n }\n } else if (finalIsOOM) {\n // #387: OOM is a persistent condition (model too large for RAM), not transient — warn user to use smaller model\n pendingWarnings?.add(\n \"⚠️ Memory plugin: LLM model requires more memory than available (OOM). \" +\n \"Consider using a smaller model or configuring a cloud fallback. \" +\n \"Run: openclaw hybrid-mem verify --test-llm\",\n );\n } else if (finalIsContextLength) {\n // #488: input too long for model's context window — config issue (model too small), not a code bug\n pendingWarnings?.add(\n \"⚠️ Memory plugin: LLM input exceeds model context window. \" +\n \"Consider using a model with a larger context window or reducing input size. \" +\n \"Run: openclaw hybrid-mem verify --test-llm\",\n );\n } else if (finalIsByteString) {\n // #1776: ByteString serialization error (non-ASCII in HTTP header) — config issue, not a code bug\n pendingWarnings?.add(\n \"⚠️ Memory plugin: LLM request contains non-ASCII characters in HTTP headers (API key, model name, or custom header). \" +\n \"Check your provider configuration for invalid characters (e.g. copy-pasted ellipsis U+2026). \" +\n \"Run: openclaw hybrid-mem verify --test-llm\",\n );\n } else if (finalIs500) {\n // #302: 500 server errors are transient — don't report to GlitchTip; request will be retried naturally\n } else if (finalIs404) {\n // #303: model not found across all fallbacks = misconfigured model name — surface to user, skip Sentry\n pendingWarnings?.add(\n \"⚠️ Memory plugin: LLM model not found (404) for all configured models. \" +\n \"Check model names in llm.default / llm.heavy / llm.nano config. \" +\n \"Run: openclaw hybrid-mem verify --test-llm\",\n );\n } else if (finalIs403) {\n // #394: country/region restriction / IP block = operator config issue, not a bug — skip GlitchTip\n pendingWarnings?.add(\n \"⚠️ Memory plugin: LLM access denied (403) — your API key may be restricted by country/region, \" +\n \"IP block, or billing. Check provider settings. \" +\n \"Run: openclaw hybrid-mem verify --test-llm\",\n );\n } else if (finalIs401) {\n // #475: invalid API key = operator config issue, not a bug — skip GlitchTip\n pendingWarnings?.add(\n \"⚠️ Memory plugin: LLM unauthorized (401) — your API key is invalid or expired. Check provider settings. \" +\n \"Run: openclaw hybrid-mem verify --test-llm\",\n );\n } else if (finalIsTransientLlm) {\n // #339, #703, #935, #936: abort/timeout/connection (including LLMRetryError-wrapped causes) — don't report\n } else if (finalIsQuota403) {\n pendingWarnings?.add(\n \"⚠️ Memory plugin: Provider quota or rate limit (403 with Retry-After / remaining-tokens). \" +\n \"Try again after the indicated window or raise quota.\",\n );\n } else if (finalIs429) {\n // #397: rate limit / usage limit — transient provider error, don't report to GlitchTip\n pendingWarnings?.add(\n \"⚠️ Memory plugin: LLM provider rate limited (429 Too Many Requests). \" +\n \"Memory features may be degraded. Try again later or upgrade your provider plan.\",\n );\n } else if (finalIsReasoningSequence) {\n // #1034: intermittent Responses API reasoning output sequencing failure — usually transient/provider-side\n pendingWarnings?.add(\n \"⚠️ Memory plugin: LLM provider returned malformed reasoning output (400). \" +\n \"This can be transient on reasoning models; a retry or fallback model is recommended.\",\n );\n } else {\n // Only report unexpected failures to Sentry — not pure config/key issues\n capturePluginError(finalError, {\n subsystem: \"chat\",\n operation: \"chatCompleteWithRetry\",\n phase: \"fallback-exhausted\",\n });\n }\n\n // Let callers (e.g. adaptive distill) attribute limits to the model that actually failed last.\n Object.assign(finalError, { lastAttemptedModel });\n\n throw finalError;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"classification-scope.js","names":[],"sources":["../../services/classification-scope.ts"],"sourcesContent":["import type { MemoryEntry, MemoryScope } from \"../types/memory.js\";\n\n/**\n * Exact scope matcher used by classify-before-write mutations.\n * - `global` only matches `global` entries.\n * - non-global scopes must match both `scope` and `scopeTarget` exactly.\n */\nexport function matchesExactScope(\n entry: Pick<MemoryEntry, \"scope\" | \"scopeTarget\">,\n scope: MemoryScope,\n scopeTarget: string | null,\n): boolean {\n const entryScope = entry.scope ?? \"global\";\n const entryScopeTarget = entry.scopeTarget ?? null;\n if (scope === \"global\") return entryScope === \"global\";\n return entryScope === scope && entryScopeTarget === scopeTarget;\n}\n\n/**\n * Validate a destructive classification target before DELETE/UPDATE.\n * Target must exist, be present in the candidate set given to the classifier,\n * and match the expected scope boundary exactly.\n */\nexport function validateScopedClassificationTarget(params: {\n targetId: string;\n candidates: ReadonlyArray<{ id: string }>;\n getById: (id: string) => MemoryEntry | null;\n scope: MemoryScope;\n scopeTarget: string | null;\n warn?: (message: string) => void;\n warnMessage: string;\n}): MemoryEntry | null {\n const target = params.getById(params.targetId);\n const allowed =\n !!target &&\n params.candidates.some((candidate) => candidate.id === params.targetId) &&\n matchesExactScope(target, params.scope, params.scopeTarget);\n if (!allowed) {\n params.warn?.(params.warnMessage);\n return null;\n }\n return target;\n}\n"],"mappings":";;;;;;AAOA,SAAgB,kBACd,OACA,OACA,aACS;CACT,MAAM,aAAa,MAAM,SAAS;CAClC,MAAM,mBAAmB,MAAM,eAAe;CAC9C,IAAI,UAAU,UAAU,OAAO,eAAe;CAC9C,OAAO,eAAe,SAAS,qBAAqB;;;;;;;AAQtD,SAAgB,mCAAmC,QAQ5B;CACrB,MAAM,SAAS,OAAO,QAAQ,OAAO,SAAS;CAK9C,IAAI,EAHF,CAAC,CAAC,UACF,OAAO,WAAW,MAAM,cAAc,UAAU,OAAO,OAAO,SAAS,IACvE,kBAAkB,QAAQ,OAAO,OAAO,OAAO,YAAY,GAC/C;EACZ,OAAO,OAAO,OAAO,YAAY;EACjC,OAAO;;CAET,OAAO"}
1
+ {"version":3,"file":"classification-scope.js","names":[],"sources":["../../services/classification-scope.ts"],"sourcesContent":["import type { MemoryEntry, MemoryScope } from \"../types/memory.js\";\n\n/**\n * Exact scope matcher used by classify-before-write mutations.\n * - `global` only matches `global` entries.\n * - non-global scopes must match both `scope` and `scopeTarget` exactly.\n */\nexport function matchesExactScope(\n entry: Pick<MemoryEntry, \"scope\" | \"scopeTarget\">,\n scope: MemoryScope,\n scopeTarget: string | null,\n): boolean {\n const entryScope = entry.scope ?? \"global\";\n const entryScopeTarget = entry.scopeTarget ?? null;\n if (scope === \"global\") return entryScope === \"global\";\n return entryScope === scope && entryScopeTarget === scopeTarget;\n}\n\n/**\n * Validate a destructive classification target before DELETE/UPDATE.\n * Target must exist, be present in the candidate set given to the classifier,\n * and match the expected scope boundary exactly.\n */\nexport function validateScopedClassificationTarget(params: {\n targetId: string;\n candidates: ReadonlyArray<{ id: string }>;\n getById: (id: string) => MemoryEntry | null;\n scope: MemoryScope;\n scopeTarget: string | null;\n warn?: (message: string) => void;\n warnMessage: string;\n}): MemoryEntry | null {\n const target = params.getById(params.targetId);\n const allowed =\n !!target &&\n params.candidates.some((candidate) => candidate.id === params.targetId) &&\n matchesExactScope(target, params.scope, params.scopeTarget);\n if (!allowed) {\n params.warn?.(params.warnMessage);\n return null;\n }\n return target;\n}\n"],"mappings":";;;;;;AAOA,SAAgB,kBACd,OACA,OACA,aACS;CACT,MAAM,aAAa,MAAM,SAAS;CAClC,MAAM,mBAAmB,MAAM,eAAe;CAC9C,IAAI,UAAU,UAAU,OAAO,eAAe;CAC9C,OAAO,eAAe,SAAS,qBAAqB;AACtD;;;;;;AAOA,SAAgB,mCAAmC,QAQ5B;CACrB,MAAM,SAAS,OAAO,QAAQ,OAAO,QAAQ;CAK7C,IAAI,EAHF,CAAC,CAAC,UACF,OAAO,WAAW,MAAM,cAAc,UAAU,OAAO,OAAO,QAAQ,KACtE,kBAAkB,QAAQ,OAAO,OAAO,OAAO,WAAW,IAC9C;EACZ,OAAO,OAAO,OAAO,WAAW;EAChC,OAAO;CACT;CACA,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"classification.js","names":[],"sources":["../../services/classification.ts"],"sourcesContent":["/**\n * Memory Operation Classification (ADD/UPDATE/DELETE/NOOP)\n *\n * Extracted from index.ts - pure function for classifying memory operations\n */\n\nimport type OpenAI from \"openai\";\nimport type { MemoryEntry } from \"../types/memory.js\";\nimport { fillPrompt, loadPrompt } from \"../utils/prompt-loader.js\";\nimport { capturePluginError } from \"./error-reporter.js\";\nimport { requiresMaxCompletionTokens, shouldOmitSamplingParams } from \"./model-capabilities.js\";\nimport { extractBalancedArraySlice } from \"../utils/llm-json-array.js\";\n\n/** Chat body for classify completions — mirrors `chatComplete` in `chat.ts` (#1008). */\nfunction buildClassifyChatBody(\n model: string,\n userContent: string,\n maxOutputTokens: number,\n): OpenAI.ChatCompletionCreateParamsNonStreaming {\n const useMaxCompletionTokens = requiresMaxCompletionTokens(model);\n const body: OpenAI.ChatCompletionCreateParamsNonStreaming = {\n model,\n messages: [{ role: \"user\", content: userContent }],\n ...(useMaxCompletionTokens ? { max_completion_tokens: maxOutputTokens } : { max_tokens: maxOutputTokens }),\n };\n if (!shouldOmitSamplingParams(model)) {\n body.temperature = 0;\n }\n return body;\n}\n\nexport type MemoryClassification = {\n action: \"ADD\" | \"UPDATE\" | \"DELETE\" | \"NOOP\";\n targetId?: string;\n reason: string;\n /** For UPDATE: the updated text to store (only if LLM suggests a merge) */\n updatedText?: string;\n};\n\n/**\n * Parse LLM classification response into MemoryClassification.\n * Format: \"ACTION [id] | reason\". Exported for tests.\n */\nexport function parseClassificationResponse(content: string, existingFacts: MemoryEntry[]): MemoryClassification {\n const match = content.match(/^(ADD|UPDATE|DELETE|NOOP)\\s*([a-f0-9-]*)\\s*\\|\\s*(.+)$/i);\n if (!match) {\n return { action: \"ADD\", reason: `unparseable LLM response: ${content.slice(0, 80)}` };\n }\n\n const action = match[1].toUpperCase() as MemoryClassification[\"action\"];\n const targetId = match[2]?.trim() || undefined;\n const reason = match[3].trim();\n\n if (action === \"UPDATE\" || action === \"DELETE\") {\n if (!targetId) {\n return { action: \"ADD\", reason: `missing targetId for ${action}; treating as ADD` };\n }\n const validTarget = existingFacts.find((f) => f.id === targetId);\n if (!validTarget) {\n return { action: \"ADD\", reason: `LLM referenced unknown id ${targetId}; treating as ADD` };\n }\n }\n\n return { action, targetId, reason };\n}\n\nfunction formatExistingFactsLines(existingFacts: MemoryEntry[]): string {\n return existingFacts\n .slice(0, 5)\n .map(\n (f, i) =>\n `${i + 1}. [id=${f.id}] ${f.category}${f.entity ? ` | entity: ${f.entity}` : \"\"}${f.key ? ` | key: ${f.key}` : \"\"}: ${f.text.slice(0, 300)}`,\n )\n .join(\"\\n\");\n}\n\n/** Same rules as prompts/memory-classify.txt — stated once in batch prompts, not repeated per candidate. */\nconst CLASSIFY_RULES_LINES = `Classify as one of:\n- ADD: The new fact is genuinely new information not covered by any existing fact.\n- UPDATE <id>: The new fact supersedes or updates an existing fact (e.g., a preference changed, a value was corrected). Specify which existing fact id it replaces.\n- DELETE <id>: The new fact explicitly retracts or negates an existing fact (e.g., \"I no longer use X\"). Specify which fact to invalidate.\n- NOOP: The new fact is already adequately captured by existing facts. No action needed.`;\n\n/**\n * Classify an incoming fact against existing similar facts.\n * Uses a cheap LLM call to determine ADD/UPDATE/DELETE/NOOP.\n * Falls back to ADD on error.\n */\nfunction buildClassifyPromptParts(\n candidateText: string,\n candidateEntity: string | null,\n candidateKey: string | null,\n existingFacts: MemoryEntry[],\n): { prompt: string } {\n const existingLines = formatExistingFactsLines(existingFacts);\n\n const template = loadPrompt(\"memory-classify\");\n const prompt = fillPrompt(template, {\n NEW_FACT: candidateText.slice(0, 500),\n ENTITY_LINE: candidateEntity ? `\\nEntity: ${candidateEntity}` : \"\",\n KEY_LINE: candidateKey ? `\\nKey: ${candidateKey}` : \"\",\n EXISTING_FACTS: existingLines,\n });\n return { prompt };\n}\n\n/** Per-candidate facts only; rules and JSON schema live in the batch message header. */\nfunction buildBatchCandidateSection(\n candidateText: string,\n candidateEntity: string | null,\n candidateKey: string | null,\n existingFacts: MemoryEntry[],\n): string {\n const existingLines = formatExistingFactsLines(existingFacts);\n const template = loadPrompt(\"memory-classify-batch-candidate\");\n return fillPrompt(template, {\n NEW_FACT: candidateText.slice(0, 500),\n ENTITY_LINE: candidateEntity ? `\\nEntity: ${candidateEntity}` : \"\",\n KEY_LINE: candidateKey ? `\\nKey: ${candidateKey}` : \"\",\n EXISTING_FACTS: existingLines,\n });\n}\n\nexport async function classifyMemoryOperation(\n candidateText: string,\n candidateEntity: string | null,\n candidateKey: string | null,\n existingFacts: MemoryEntry[],\n openai: OpenAI,\n model: string,\n logger: { warn: (msg: string) => void },\n): Promise<MemoryClassification> {\n if (existingFacts.length === 0) {\n return { action: \"ADD\", reason: \"no similar facts found\" };\n }\n\n const { prompt } = buildClassifyPromptParts(candidateText, candidateEntity, candidateKey, existingFacts);\n\n try {\n const { withLLMRetry } = await import(\"./chat.js\");\n const resp = (await withLLMRetry(\n () =>\n openai.chat.completions.create(\n buildClassifyChatBody(model, prompt, 100) as unknown as Parameters<\n OpenAI[\"chat\"][\"completions\"][\"create\"]\n >[0],\n ),\n { maxRetries: 2 },\n )) as OpenAI.Chat.ChatCompletion;\n const content = (resp.choices?.[0]?.message?.content ?? \"\").trim();\n return parseClassificationResponse(content, existingFacts);\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"classify-memory-operation\",\n subsystem: \"openai\",\n model,\n });\n logger.warn(`memory-hybrid: classify operation failed: ${err}`);\n return { action: \"ADD\", reason: \"classification failed; defaulting to ADD\" };\n }\n}\n\n/** One candidate for {@link classifyMemoryOperationsBatch} (#862). */\nexport type ClassifyMemoryOperationInput = {\n candidateText: string;\n candidateEntity: string | null;\n candidateKey: string | null;\n existingFacts: MemoryEntry[];\n};\n\n/**\n * Remove common model \"thinking\" wrappers that appear before JSON (#1007).\n */\nfunction stripThinkingWrapperBlocks(s: string): string {\n return s\n .replace(/<redacted_thinking>[\\s\\S]*?<\\/redacted_thinking>/gi, \"\")\n .replace(/<thinking>[\\s\\S]*?<\\/thinking>/gi, \"\")\n .replace(/<reasoning>[\\s\\S]*?<\\/reasoning>/gi, \"\")\n .trim();\n}\n\n/**\n * Prefer JSON-looking content inside ``` / ```json fences when the model wraps output (#1007).\n */\nfunction preferMarkdownJsonFenceContent(s: string): string {\n const re = /```(?:json)?\\s*\\n?([\\s\\S]*?)```/gi;\n const chunks: string[] = [];\n for (;;) {\n const m = re.exec(s);\n if (m === null) break;\n chunks.push(m[1].trim());\n }\n if (chunks.length === 0) return s;\n const arrayChunk = chunks.find((c) => c.startsWith(\"[\"));\n if (arrayChunk) return arrayChunk;\n const objectChunk = chunks.find((c) => c.startsWith(\"{\"));\n if (objectChunk) return objectChunk;\n return chunks[chunks.length - 1] ?? s;\n}\n\nconst BATCH_OBJECT_ARRAY_KEYS = [\n \"classifications\",\n \"results\",\n \"items\",\n \"data\",\n \"answers\",\n \"operations\",\n \"classifyResults\",\n] as const;\n\nfunction tryParseBatchClassifyAsObjectWithArray(s: string): unknown[] | null {\n const t = s.trim();\n if (!t.startsWith(\"{\")) return null;\n try {\n const obj = JSON.parse(t) as unknown;\n if (!obj || typeof obj !== \"object\" || Array.isArray(obj)) return null;\n const rec = obj as Record<string, unknown>;\n for (const k of BATCH_OBJECT_ARRAY_KEYS) {\n const v = rec[k];\n if (!Array.isArray(v)) continue;\n if (isBatchClassifyResultArray(v)) return v;\n if (isLenientBatchClassifyResultArray(v)) return v;\n }\n } catch {\n return null;\n }\n return null;\n}\n\n/** True if `v` looks like our batch classify payload (objects with `action`), not e.g. `[1]` or `[note]`. */\nfunction isBatchClassifyResultArray(v: unknown): v is unknown[] {\n if (!Array.isArray(v)) return false;\n return v.every(\n (row) =>\n row !== null &&\n typeof row === \"object\" &&\n !Array.isArray(row) &&\n typeof (row as Record<string, unknown>).action === \"string\",\n );\n}\n\n/**\n * Models sometimes omit `action` on a row or use extra keys; we still prefer recovering a JSON\n * array of objects over forcing sequential classify (fewer round-trips).\n */\nfunction isLenientBatchClassifyResultArray(v: unknown): v is unknown[] {\n if (!Array.isArray(v) || v.length === 0) return false;\n const objects = v.every((row) => row !== null && typeof row === \"object\" && !Array.isArray(row));\n if (!objects) return false;\n const withAction = v.filter((row) => typeof (row as Record<string, unknown>).action === \"string\").length;\n return withAction > 0 && withAction >= v.length / 2;\n}\n\n/**\n * Find a JSON array of batch rows; skip bracket preambles that are not JSON or not our shape (#1007, Copilot PR#1006).\n * Returns null when nothing matches (caller falls back to sequential classify; no throw — avoids GlitchTip noise #1155–#1156).\n */\nfunction parseBalancedBatchArrayFromText(text: string): unknown[] | null {\n const t = text.trim();\n if (t.length === 0) return null;\n\n const seen = new Set<string>();\n const tryCandidate = (candidate: string): unknown[] | undefined => {\n const normalized = candidate.trim();\n if (normalized.length === 0 || seen.has(normalized)) return undefined;\n seen.add(normalized);\n try {\n const v = JSON.parse(normalized) as unknown;\n if (isBatchClassifyResultArray(v)) return v;\n if (isLenientBatchClassifyResultArray(v)) return v;\n return undefined;\n } catch {\n return undefined;\n }\n };\n\n let searchFrom = 0;\n while (searchFrom < t.length) {\n const relStart = t.indexOf(\"[\", searchFrom);\n if (relStart < 0) return null;\n const candidate = extractBalancedArraySlice(t, relStart);\n if (!candidate) {\n searchFrom = relStart + 1;\n continue;\n }\n const parsed = tryCandidate(candidate);\n if (parsed !== undefined) return parsed;\n searchFrom = relStart + candidate.length;\n }\n return null;\n}\n\n/**\n * Parse model output for {@link classifyMemoryOperationsBatch}. Exported for tests (#1007).\n * Returns null when no valid batch array can be recovered (expected for some model outputs).\n */\nexport function parseBatchClassifyResponseContent(raw: string): unknown | null {\n let s = raw.trim();\n s = stripThinkingWrapperBlocks(s);\n s = preferMarkdownJsonFenceContent(s);\n s = s.trim();\n\n if (s.startsWith(\"[\")) {\n const arr = parseBalancedBatchArrayFromText(s);\n if (arr) return arr;\n }\n\n const fromObject = tryParseBatchClassifyAsObjectWithArray(s);\n if (fromObject) return fromObject;\n\n const arr = parseBalancedBatchArrayFromText(s);\n if (arr) return arr;\n\n return null;\n}\n\nfunction parseBatchClassificationRow(row: unknown, existingFacts: MemoryEntry[]): MemoryClassification {\n if (!row || typeof row !== \"object\") {\n return { action: \"ADD\", reason: \"invalid batch row\" };\n }\n const r = row as Record<string, unknown>;\n const action = typeof r.action === \"string\" ? r.action.toUpperCase() : \"\";\n const targetId = typeof r.targetId === \"string\" && r.targetId.trim() ? r.targetId.trim() : undefined;\n const reason = typeof r.reason === \"string\" ? r.reason.trim() : \"batch classification\";\n if (action === \"ADD\" || action === \"NOOP\") {\n return { action: action as MemoryClassification[\"action\"], reason };\n }\n if (action === \"UPDATE\" || action === \"DELETE\") {\n if (!targetId) {\n return { action: \"ADD\", reason: `batch: missing targetId for ${action}` };\n }\n const validTarget = existingFacts.find((f) => f.id === targetId);\n if (!validTarget) {\n return { action: \"ADD\", reason: `batch: unknown targetId ${targetId}` };\n }\n return { action: action as MemoryClassification[\"action\"], targetId, reason };\n }\n return { action: \"ADD\", reason: `batch: unknown action ${action}` };\n}\n\n/**\n * Classify multiple store candidates in one LLM call when each has similar facts (#862).\n * Falls back to sequential {@link classifyMemoryOperation} if parsing fails or length mismatches.\n */\nexport async function classifyMemoryOperationsBatch(\n items: ClassifyMemoryOperationInput[],\n openai: OpenAI,\n model: string,\n logger: { warn: (msg: string) => void },\n): Promise<MemoryClassification[]> {\n if (items.length === 0) return [];\n if (items.length === 1) {\n const one = items[0];\n return [\n await classifyMemoryOperation(\n one.candidateText,\n one.candidateEntity,\n one.candidateKey,\n one.existingFacts,\n openai,\n model,\n logger,\n ),\n ];\n }\n\n const blocks = items.map((it, idx) => {\n const section = buildBatchCandidateSection(it.candidateText, it.candidateEntity, it.candidateKey, it.existingFacts);\n return `### Candidate ${idx}\\n${section}`;\n });\n\n const header = `You are a memory classifier. There are ${items.length} independent candidates below. ${CLASSIFY_RULES_LINES}\n\nRespond with ONLY a JSON array of exactly ${items.length} objects in order (index 0 = first candidate). Do not use a one-line \"ACTION | reason\" reply; use JSON only. Each object must be:\n{\"action\":\"ADD\"|\"UPDATE\"|\"DELETE\"|\"NOOP\",\"targetId\":string|null,\"reason\":string}\nFor UPDATE or DELETE, targetId must be one of the existing fact ids listed under that candidate. For ADD or NOOP use null for targetId.\n\n`;\n\n const fullPrompt = `${header}\\n${blocks.join(\"\\n\\n\")}`;\n\n const runSequential = async (): Promise<MemoryClassification[]> => {\n const out: MemoryClassification[] = [];\n for (const it of items) {\n out.push(\n await classifyMemoryOperation(\n it.candidateText,\n it.candidateEntity,\n it.candidateKey,\n it.existingFacts,\n openai,\n model,\n logger,\n ),\n );\n }\n return out;\n };\n\n try {\n const { withLLMRetry } = await import(\"./chat.js\");\n const batchMaxOut = Math.min(800, 80 * items.length);\n const resp = (await withLLMRetry(\n () =>\n openai.chat.completions.create(\n buildClassifyChatBody(model, fullPrompt, batchMaxOut) as unknown as Parameters<\n OpenAI[\"chat\"][\"completions\"][\"create\"]\n >[0],\n ),\n { maxRetries: 2 },\n )) as OpenAI.Chat.ChatCompletion;\n const raw = (resp.choices?.[0]?.message?.content ?? \"\").trim();\n const parsed = parseBatchClassifyResponseContent(raw);\n if (!Array.isArray(parsed) || parsed.length !== items.length) {\n logger.warn(\n `memory-hybrid: batch classify parse/length mismatch (expected ${items.length}, got ${Array.isArray(parsed) ? parsed.length : \"non-array\"}); falling back to sequential calls`,\n );\n return await runSequential();\n }\n return items.map((it, i) => parseBatchClassificationRow(parsed[i], it.existingFacts));\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"classify-memory-operations-batch\",\n subsystem: \"openai\",\n model,\n severity: \"info\",\n });\n logger.warn(`memory-hybrid: batch classify failed (${err}); falling back to sequential calls`);\n return await runSequential();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"classification.js","names":[],"sources":["../../services/classification.ts"],"sourcesContent":["/**\n * Memory Operation Classification (ADD/UPDATE/DELETE/NOOP)\n *\n * Extracted from index.ts - pure function for classifying memory operations\n */\n\nimport type OpenAI from \"openai\";\nimport type { MemoryEntry } from \"../types/memory.js\";\nimport { fillPrompt, loadPrompt } from \"../utils/prompt-loader.js\";\nimport { capturePluginError } from \"./error-reporter.js\";\nimport { requiresMaxCompletionTokens, shouldOmitSamplingParams } from \"./model-capabilities.js\";\nimport { extractBalancedArraySlice } from \"../utils/llm-json-array.js\";\n\n/** Chat body for classify completions — mirrors `chatComplete` in `chat.ts` (#1008). */\nfunction buildClassifyChatBody(\n model: string,\n userContent: string,\n maxOutputTokens: number,\n): OpenAI.ChatCompletionCreateParamsNonStreaming {\n const useMaxCompletionTokens = requiresMaxCompletionTokens(model);\n const body: OpenAI.ChatCompletionCreateParamsNonStreaming = {\n model,\n messages: [{ role: \"user\", content: userContent }],\n ...(useMaxCompletionTokens ? { max_completion_tokens: maxOutputTokens } : { max_tokens: maxOutputTokens }),\n };\n if (!shouldOmitSamplingParams(model)) {\n body.temperature = 0;\n }\n return body;\n}\n\nexport type MemoryClassification = {\n action: \"ADD\" | \"UPDATE\" | \"DELETE\" | \"NOOP\";\n targetId?: string;\n reason: string;\n /** For UPDATE: the updated text to store (only if LLM suggests a merge) */\n updatedText?: string;\n};\n\n/**\n * Parse LLM classification response into MemoryClassification.\n * Format: \"ACTION [id] | reason\". Exported for tests.\n */\nexport function parseClassificationResponse(content: string, existingFacts: MemoryEntry[]): MemoryClassification {\n const match = content.match(/^(ADD|UPDATE|DELETE|NOOP)\\s*([a-f0-9-]*)\\s*\\|\\s*(.+)$/i);\n if (!match) {\n return { action: \"ADD\", reason: `unparseable LLM response: ${content.slice(0, 80)}` };\n }\n\n const action = match[1].toUpperCase() as MemoryClassification[\"action\"];\n const targetId = match[2]?.trim() || undefined;\n const reason = match[3].trim();\n\n if (action === \"UPDATE\" || action === \"DELETE\") {\n if (!targetId) {\n return { action: \"ADD\", reason: `missing targetId for ${action}; treating as ADD` };\n }\n const validTarget = existingFacts.find((f) => f.id === targetId);\n if (!validTarget) {\n return { action: \"ADD\", reason: `LLM referenced unknown id ${targetId}; treating as ADD` };\n }\n }\n\n return { action, targetId, reason };\n}\n\nfunction formatExistingFactsLines(existingFacts: MemoryEntry[]): string {\n return existingFacts\n .slice(0, 5)\n .map(\n (f, i) =>\n `${i + 1}. [id=${f.id}] ${f.category}${f.entity ? ` | entity: ${f.entity}` : \"\"}${f.key ? ` | key: ${f.key}` : \"\"}: ${f.text.slice(0, 300)}`,\n )\n .join(\"\\n\");\n}\n\n/** Same rules as prompts/memory-classify.txt — stated once in batch prompts, not repeated per candidate. */\nconst CLASSIFY_RULES_LINES = `Classify as one of:\n- ADD: The new fact is genuinely new information not covered by any existing fact.\n- UPDATE <id>: The new fact supersedes or updates an existing fact (e.g., a preference changed, a value was corrected). Specify which existing fact id it replaces.\n- DELETE <id>: The new fact explicitly retracts or negates an existing fact (e.g., \"I no longer use X\"). Specify which fact to invalidate.\n- NOOP: The new fact is already adequately captured by existing facts. No action needed.`;\n\n/**\n * Classify an incoming fact against existing similar facts.\n * Uses a cheap LLM call to determine ADD/UPDATE/DELETE/NOOP.\n * Falls back to ADD on error.\n */\nfunction buildClassifyPromptParts(\n candidateText: string,\n candidateEntity: string | null,\n candidateKey: string | null,\n existingFacts: MemoryEntry[],\n): { prompt: string } {\n const existingLines = formatExistingFactsLines(existingFacts);\n\n const template = loadPrompt(\"memory-classify\");\n const prompt = fillPrompt(template, {\n NEW_FACT: candidateText.slice(0, 500),\n ENTITY_LINE: candidateEntity ? `\\nEntity: ${candidateEntity}` : \"\",\n KEY_LINE: candidateKey ? `\\nKey: ${candidateKey}` : \"\",\n EXISTING_FACTS: existingLines,\n });\n return { prompt };\n}\n\n/** Per-candidate facts only; rules and JSON schema live in the batch message header. */\nfunction buildBatchCandidateSection(\n candidateText: string,\n candidateEntity: string | null,\n candidateKey: string | null,\n existingFacts: MemoryEntry[],\n): string {\n const existingLines = formatExistingFactsLines(existingFacts);\n const template = loadPrompt(\"memory-classify-batch-candidate\");\n return fillPrompt(template, {\n NEW_FACT: candidateText.slice(0, 500),\n ENTITY_LINE: candidateEntity ? `\\nEntity: ${candidateEntity}` : \"\",\n KEY_LINE: candidateKey ? `\\nKey: ${candidateKey}` : \"\",\n EXISTING_FACTS: existingLines,\n });\n}\n\nexport async function classifyMemoryOperation(\n candidateText: string,\n candidateEntity: string | null,\n candidateKey: string | null,\n existingFacts: MemoryEntry[],\n openai: OpenAI,\n model: string,\n logger: { warn: (msg: string) => void },\n): Promise<MemoryClassification> {\n if (existingFacts.length === 0) {\n return { action: \"ADD\", reason: \"no similar facts found\" };\n }\n\n const { prompt } = buildClassifyPromptParts(candidateText, candidateEntity, candidateKey, existingFacts);\n\n try {\n const { withLLMRetry } = await import(\"./chat.js\");\n const resp = (await withLLMRetry(\n () =>\n openai.chat.completions.create(\n buildClassifyChatBody(model, prompt, 100) as unknown as Parameters<\n OpenAI[\"chat\"][\"completions\"][\"create\"]\n >[0],\n ),\n { maxRetries: 2 },\n )) as OpenAI.Chat.ChatCompletion;\n const content = (resp.choices?.[0]?.message?.content ?? \"\").trim();\n return parseClassificationResponse(content, existingFacts);\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"classify-memory-operation\",\n subsystem: \"openai\",\n model,\n });\n logger.warn(`memory-hybrid: classify operation failed: ${err}`);\n return { action: \"ADD\", reason: \"classification failed; defaulting to ADD\" };\n }\n}\n\n/** One candidate for {@link classifyMemoryOperationsBatch} (#862). */\nexport type ClassifyMemoryOperationInput = {\n candidateText: string;\n candidateEntity: string | null;\n candidateKey: string | null;\n existingFacts: MemoryEntry[];\n};\n\n/**\n * Remove common model \"thinking\" wrappers that appear before JSON (#1007).\n */\nfunction stripThinkingWrapperBlocks(s: string): string {\n return s\n .replace(/<redacted_thinking>[\\s\\S]*?<\\/redacted_thinking>/gi, \"\")\n .replace(/<thinking>[\\s\\S]*?<\\/thinking>/gi, \"\")\n .replace(/<reasoning>[\\s\\S]*?<\\/reasoning>/gi, \"\")\n .trim();\n}\n\n/**\n * Prefer JSON-looking content inside ``` / ```json fences when the model wraps output (#1007).\n */\nfunction preferMarkdownJsonFenceContent(s: string): string {\n const re = /```(?:json)?\\s*\\n?([\\s\\S]*?)```/gi;\n const chunks: string[] = [];\n for (;;) {\n const m = re.exec(s);\n if (m === null) break;\n chunks.push(m[1].trim());\n }\n if (chunks.length === 0) return s;\n const arrayChunk = chunks.find((c) => c.startsWith(\"[\"));\n if (arrayChunk) return arrayChunk;\n const objectChunk = chunks.find((c) => c.startsWith(\"{\"));\n if (objectChunk) return objectChunk;\n return chunks[chunks.length - 1] ?? s;\n}\n\nconst BATCH_OBJECT_ARRAY_KEYS = [\n \"classifications\",\n \"results\",\n \"items\",\n \"data\",\n \"answers\",\n \"operations\",\n \"classifyResults\",\n] as const;\n\nfunction tryParseBatchClassifyAsObjectWithArray(s: string): unknown[] | null {\n const t = s.trim();\n if (!t.startsWith(\"{\")) return null;\n try {\n const obj = JSON.parse(t) as unknown;\n if (!obj || typeof obj !== \"object\" || Array.isArray(obj)) return null;\n const rec = obj as Record<string, unknown>;\n for (const k of BATCH_OBJECT_ARRAY_KEYS) {\n const v = rec[k];\n if (!Array.isArray(v)) continue;\n if (isBatchClassifyResultArray(v)) return v;\n if (isLenientBatchClassifyResultArray(v)) return v;\n }\n } catch {\n return null;\n }\n return null;\n}\n\n/** True if `v` looks like our batch classify payload (objects with `action`), not e.g. `[1]` or `[note]`. */\nfunction isBatchClassifyResultArray(v: unknown): v is unknown[] {\n if (!Array.isArray(v)) return false;\n return v.every(\n (row) =>\n row !== null &&\n typeof row === \"object\" &&\n !Array.isArray(row) &&\n typeof (row as Record<string, unknown>).action === \"string\",\n );\n}\n\n/**\n * Models sometimes omit `action` on a row or use extra keys; we still prefer recovering a JSON\n * array of objects over forcing sequential classify (fewer round-trips).\n */\nfunction isLenientBatchClassifyResultArray(v: unknown): v is unknown[] {\n if (!Array.isArray(v) || v.length === 0) return false;\n const objects = v.every((row) => row !== null && typeof row === \"object\" && !Array.isArray(row));\n if (!objects) return false;\n const withAction = v.filter((row) => typeof (row as Record<string, unknown>).action === \"string\").length;\n return withAction > 0 && withAction >= v.length / 2;\n}\n\n/**\n * Find a JSON array of batch rows; skip bracket preambles that are not JSON or not our shape (#1007, Copilot PR#1006).\n * Returns null when nothing matches (caller falls back to sequential classify; no throw — avoids GlitchTip noise #1155–#1156).\n */\nfunction parseBalancedBatchArrayFromText(text: string): unknown[] | null {\n const t = text.trim();\n if (t.length === 0) return null;\n\n const seen = new Set<string>();\n const tryCandidate = (candidate: string): unknown[] | undefined => {\n const normalized = candidate.trim();\n if (normalized.length === 0 || seen.has(normalized)) return undefined;\n seen.add(normalized);\n try {\n const v = JSON.parse(normalized) as unknown;\n if (isBatchClassifyResultArray(v)) return v;\n if (isLenientBatchClassifyResultArray(v)) return v;\n return undefined;\n } catch {\n return undefined;\n }\n };\n\n let searchFrom = 0;\n while (searchFrom < t.length) {\n const relStart = t.indexOf(\"[\", searchFrom);\n if (relStart < 0) return null;\n const candidate = extractBalancedArraySlice(t, relStart);\n if (!candidate) {\n searchFrom = relStart + 1;\n continue;\n }\n const parsed = tryCandidate(candidate);\n if (parsed !== undefined) return parsed;\n searchFrom = relStart + candidate.length;\n }\n return null;\n}\n\n/**\n * Parse model output for {@link classifyMemoryOperationsBatch}. Exported for tests (#1007).\n * Returns null when no valid batch array can be recovered (expected for some model outputs).\n */\nexport function parseBatchClassifyResponseContent(raw: string): unknown | null {\n let s = raw.trim();\n s = stripThinkingWrapperBlocks(s);\n s = preferMarkdownJsonFenceContent(s);\n s = s.trim();\n\n if (s.startsWith(\"[\")) {\n const arr = parseBalancedBatchArrayFromText(s);\n if (arr) return arr;\n }\n\n const fromObject = tryParseBatchClassifyAsObjectWithArray(s);\n if (fromObject) return fromObject;\n\n const arr = parseBalancedBatchArrayFromText(s);\n if (arr) return arr;\n\n return null;\n}\n\nfunction parseBatchClassificationRow(row: unknown, existingFacts: MemoryEntry[]): MemoryClassification {\n if (!row || typeof row !== \"object\") {\n return { action: \"ADD\", reason: \"invalid batch row\" };\n }\n const r = row as Record<string, unknown>;\n const action = typeof r.action === \"string\" ? r.action.toUpperCase() : \"\";\n const targetId = typeof r.targetId === \"string\" && r.targetId.trim() ? r.targetId.trim() : undefined;\n const reason = typeof r.reason === \"string\" ? r.reason.trim() : \"batch classification\";\n if (action === \"ADD\" || action === \"NOOP\") {\n return { action: action as MemoryClassification[\"action\"], reason };\n }\n if (action === \"UPDATE\" || action === \"DELETE\") {\n if (!targetId) {\n return { action: \"ADD\", reason: `batch: missing targetId for ${action}` };\n }\n const validTarget = existingFacts.find((f) => f.id === targetId);\n if (!validTarget) {\n return { action: \"ADD\", reason: `batch: unknown targetId ${targetId}` };\n }\n return { action: action as MemoryClassification[\"action\"], targetId, reason };\n }\n return { action: \"ADD\", reason: `batch: unknown action ${action}` };\n}\n\n/**\n * Classify multiple store candidates in one LLM call when each has similar facts (#862).\n * Falls back to sequential {@link classifyMemoryOperation} if parsing fails or length mismatches.\n */\nexport async function classifyMemoryOperationsBatch(\n items: ClassifyMemoryOperationInput[],\n openai: OpenAI,\n model: string,\n logger: { warn: (msg: string) => void },\n): Promise<MemoryClassification[]> {\n if (items.length === 0) return [];\n if (items.length === 1) {\n const one = items[0];\n return [\n await classifyMemoryOperation(\n one.candidateText,\n one.candidateEntity,\n one.candidateKey,\n one.existingFacts,\n openai,\n model,\n logger,\n ),\n ];\n }\n\n const blocks = items.map((it, idx) => {\n const section = buildBatchCandidateSection(it.candidateText, it.candidateEntity, it.candidateKey, it.existingFacts);\n return `### Candidate ${idx}\\n${section}`;\n });\n\n const header = `You are a memory classifier. There are ${items.length} independent candidates below. ${CLASSIFY_RULES_LINES}\n\nRespond with ONLY a JSON array of exactly ${items.length} objects in order (index 0 = first candidate). Do not use a one-line \"ACTION | reason\" reply; use JSON only. Each object must be:\n{\"action\":\"ADD\"|\"UPDATE\"|\"DELETE\"|\"NOOP\",\"targetId\":string|null,\"reason\":string}\nFor UPDATE or DELETE, targetId must be one of the existing fact ids listed under that candidate. For ADD or NOOP use null for targetId.\n\n`;\n\n const fullPrompt = `${header}\\n${blocks.join(\"\\n\\n\")}`;\n\n const runSequential = async (): Promise<MemoryClassification[]> => {\n const out: MemoryClassification[] = [];\n for (const it of items) {\n out.push(\n await classifyMemoryOperation(\n it.candidateText,\n it.candidateEntity,\n it.candidateKey,\n it.existingFacts,\n openai,\n model,\n logger,\n ),\n );\n }\n return out;\n };\n\n try {\n const { withLLMRetry } = await import(\"./chat.js\");\n const batchMaxOut = Math.min(800, 80 * items.length);\n const resp = (await withLLMRetry(\n () =>\n openai.chat.completions.create(\n buildClassifyChatBody(model, fullPrompt, batchMaxOut) as unknown as Parameters<\n OpenAI[\"chat\"][\"completions\"][\"create\"]\n >[0],\n ),\n { maxRetries: 2 },\n )) as OpenAI.Chat.ChatCompletion;\n const raw = (resp.choices?.[0]?.message?.content ?? \"\").trim();\n const parsed = parseBatchClassifyResponseContent(raw);\n if (!Array.isArray(parsed) || parsed.length !== items.length) {\n logger.warn(\n `memory-hybrid: batch classify parse/length mismatch (expected ${items.length}, got ${Array.isArray(parsed) ? parsed.length : \"non-array\"}); falling back to sequential calls`,\n );\n return await runSequential();\n }\n return items.map((it, i) => parseBatchClassificationRow(parsed[i], it.existingFacts));\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"classify-memory-operations-batch\",\n subsystem: \"openai\",\n model,\n severity: \"info\",\n });\n logger.warn(`memory-hybrid: batch classify failed (${err}); falling back to sequential calls`);\n return await runSequential();\n }\n}\n"],"mappings":"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"}