titan-agent 5.0.2 → 5.0.3

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 (176) hide show
  1. package/dist/agent/agent.js +48 -3
  2. package/dist/agent/agent.js.map +1 -1
  3. package/dist/agent/agentLoop.js +83 -5
  4. package/dist/agent/agentLoop.js.map +1 -1
  5. package/dist/agent/commandPost.js +1 -1
  6. package/dist/agent/commandPost.js.map +1 -1
  7. package/dist/agent/goalProposer.js +2 -2
  8. package/dist/agent/goalProposer.js.map +1 -1
  9. package/dist/agent/missionDriver.js +1 -1
  10. package/dist/agent/missionDriver.js.map +1 -1
  11. package/dist/agent/promptBudget.js +85 -0
  12. package/dist/agent/promptBudget.js.map +1 -0
  13. package/dist/agent/structuredSpawn.js +1 -1
  14. package/dist/agent/structuredSpawn.js.map +1 -1
  15. package/dist/agent/subtaskTaxonomy.js +1 -1
  16. package/dist/agent/subtaskTaxonomy.js.map +1 -1
  17. package/dist/agent/systemPromptParts.js +10 -1
  18. package/dist/agent/systemPromptParts.js.map +1 -1
  19. package/dist/agent/toolRunner.js +16 -0
  20. package/dist/agent/toolRunner.js.map +1 -1
  21. package/dist/agent/toolSearch.js +4 -1
  22. package/dist/agent/toolSearch.js.map +1 -1
  23. package/dist/analytics/bugReports.js +1 -1
  24. package/dist/analytics/bugReports.js.map +1 -1
  25. package/dist/channels/messenger.js +1 -1
  26. package/dist/channels/messenger.js.map +1 -1
  27. package/dist/eval/harness.js +141 -0
  28. package/dist/eval/harness.js.map +1 -0
  29. package/dist/gateway/server.js +374 -74
  30. package/dist/gateway/server.js.map +1 -1
  31. package/dist/hooks/shellHooks.js +1 -1
  32. package/dist/hooks/shellHooks.js.map +1 -1
  33. package/dist/lib/auto-heal/repair-strategies.js.map +1 -1
  34. package/dist/memory/promptIncludes.js +58 -0
  35. package/dist/memory/promptIncludes.js.map +1 -0
  36. package/dist/organism/alertsStore.js +70 -0
  37. package/dist/organism/alertsStore.js.map +1 -0
  38. package/dist/plugins/memoryRetrieval.js.map +1 -1
  39. package/dist/providers/ollama.js +7 -7
  40. package/dist/providers/ollama.js.map +1 -1
  41. package/dist/safety/invariants.js +60 -0
  42. package/dist/safety/invariants.js.map +1 -0
  43. package/dist/safety/opusReview.js +1 -1
  44. package/dist/safety/opusReview.js.map +1 -1
  45. package/dist/security/commandScanner.js +2 -2
  46. package/dist/security/commandScanner.js.map +1 -1
  47. package/dist/security/secretGuard.js +4 -4
  48. package/dist/security/secretGuard.js.map +1 -1
  49. package/dist/skills/builtin/widget_gallery.js +28 -1
  50. package/dist/skills/builtin/widget_gallery.js.map +1 -1
  51. package/dist/skills/frontmatterLoader.js +119 -0
  52. package/dist/skills/frontmatterLoader.js.map +1 -0
  53. package/dist/skills/registry.js +20 -0
  54. package/dist/skills/registry.js.map +1 -1
  55. package/dist/testing/testHealthMonitor.js +1 -2
  56. package/dist/testing/testHealthMonitor.js.map +1 -1
  57. package/dist/utils/constants.js +2 -2
  58. package/dist/utils/constants.js.map +1 -1
  59. package/dist/utils/replyQuality.js +1 -1
  60. package/dist/utils/replyQuality.js.map +1 -1
  61. package/dist/utils/tokens.js +1 -1
  62. package/dist/utils/tokens.js.map +1 -1
  63. package/docs/bleeding-edge-agents-2026.md +450 -0
  64. package/docs/langchain-analysis.md +598 -0
  65. package/docs/langchain-code-analysis.md +363 -0
  66. package/docs/space-agent-analysis.md +300 -0
  67. package/package.json +1 -1
  68. package/ui/dist/assets/{AuditPanel-G7YA1HzV.js → AuditPanel-B84Mp16G.js} +2 -2
  69. package/ui/dist/assets/AutonomyPanel-DOtiTFxV.js +11 -0
  70. package/ui/dist/assets/{AutopilotPanel-CHRjxdh0.js → AutopilotPanel-nTb1Dnru.js} +1 -1
  71. package/ui/dist/assets/AutoresearchPanel-D46mX8VF.js +6 -0
  72. package/ui/dist/assets/BackupPanel-DGM1XXbG.js +1 -0
  73. package/ui/dist/assets/BrowserPanel-Cn1tTN3y.js +6 -0
  74. package/ui/dist/assets/{CPAgents-D5533PhK.js → CPAgents-CEraUkME.js} +1 -1
  75. package/ui/dist/assets/{CPDashboard-C-GgqDsI.js → CPDashboard-B_yidGAe.js} +2 -2
  76. package/ui/dist/assets/CPFiles-BBS8jtYH.js +1 -0
  77. package/ui/dist/assets/CPGoals-DL5v21TZ.js +1 -0
  78. package/ui/dist/assets/CPInbox-CyLQJBYF.js +11 -0
  79. package/ui/dist/assets/{CPSocial-mUQsrSh5.js → CPSocial-BkEtQ1Um.js} +3 -3
  80. package/ui/dist/assets/ChannelsPanel-CD2kHhA5.js +1 -0
  81. package/ui/dist/assets/CheckpointsPanel-BrUTFPu_.js +1 -0
  82. package/ui/dist/assets/CommandPostHub-BPPaUv1B.js +29 -0
  83. package/ui/dist/assets/CronPanel-CsfQctFp.js +1 -0
  84. package/ui/dist/assets/DaemonPanel-CNUggBbL.js +1 -0
  85. package/ui/dist/assets/DataTable-DuAEp_QJ.js +1 -0
  86. package/ui/dist/assets/{EmptyState-D60-wQrz.js → EmptyState-DFrAEZDm.js} +1 -1
  87. package/ui/dist/assets/EvalPanel-DEX0a5-b.js +1 -0
  88. package/ui/dist/assets/{FilesPanel-BNN3h_HW.js → FilesPanel-DATsiAqG.js} +1 -1
  89. package/ui/dist/assets/FleetPanel-QYQKqx4W.js +1 -0
  90. package/ui/dist/assets/{HomelabPanel-1mfhRBh6.js → HomelabPanel-DhuXd3ZD.js} +2 -2
  91. package/ui/dist/assets/{InfraView-Df6SFI7b.js → InfraView-eS7cpESw.js} +2 -2
  92. package/ui/dist/assets/InlineEditableField-zIAnW4AR.js +1 -0
  93. package/ui/dist/assets/{Input-DYukme8A.js → Input-bFsLI0fq.js} +1 -1
  94. package/ui/dist/assets/IntegrationsPanel-C_FswSRN.js +1 -0
  95. package/ui/dist/assets/IntelligenceView-smQ6aBwx.js +2 -0
  96. package/ui/dist/assets/{LearningPanel-BPx05bBu.js → LearningPanel-BEgF_iND.js} +1 -1
  97. package/ui/dist/assets/{LogsPanel-D3Qfp2SE.js → LogsPanel-Br1P8ST6.js} +1 -1
  98. package/ui/dist/assets/McpPanel-ByvQ12J_.js +1 -0
  99. package/ui/dist/assets/{MemoryGraphPanel-BFovwaSG.js → MemoryGraphPanel-BGOeSaET.js} +1 -1
  100. package/ui/dist/assets/MemoryWikiPanel-CR8btd66.js +11 -0
  101. package/ui/dist/assets/MeshPanel-BjkcSOMz.js +11 -0
  102. package/ui/dist/assets/NvidiaPanel-NYt42w7L.js +1 -0
  103. package/ui/dist/assets/OrganismPanel-PHvISvVn.js +1 -0
  104. package/ui/dist/assets/OverviewPanel-q35zdMr6.js +6 -0
  105. package/ui/dist/assets/{PageHeader-BdvxKoad.js → PageHeader-Cwn3OALc.js} +1 -1
  106. package/ui/dist/assets/PaperclipPanel-BDpQki0d.js +1 -0
  107. package/ui/dist/assets/{PersonasPanel-BpI6Npxv.js → PersonasPanel-DxrGW5C4.js} +1 -1
  108. package/ui/dist/assets/RecipesPanel-CYRdBx5u.js +1 -0
  109. package/ui/dist/assets/{SecurityPanel-CBDsEAFz.js → SecurityPanel-i1QMctV0.js} +1 -1
  110. package/ui/dist/assets/SelfImprovePanel-DbybAZWp.js +1 -0
  111. package/ui/dist/assets/SelfProposalsPanel-DtcTUDDd.js +2 -0
  112. package/ui/dist/assets/SessionsPanel-B7QmOizR.js +1 -0
  113. package/ui/dist/assets/SessionsTab-BdJj_vsI.js +1 -0
  114. package/ui/dist/assets/{SettingsPanel-BiWHsOAJ.js → SettingsPanel-DnEvJUFe.js} +1 -1
  115. package/ui/dist/assets/SettingsView-C39dk_yr.js +2 -0
  116. package/ui/dist/assets/{SkeletonLoader-CGtpZJ-7.js → SkeletonLoader-CsiR8ED9.js} +1 -1
  117. package/ui/dist/assets/{SkillsPanel-Z_9jA6dU.js → SkillsPanel-DM4qBFDS.js} +1 -1
  118. package/ui/dist/assets/{SomaView-AP3BXqf-.js → SomaView-CWnPKEQI.js} +1 -1
  119. package/ui/dist/assets/{StatCard-CrnvXPg5.js → StatCard-CY8lgeWm.js} +1 -1
  120. package/ui/dist/assets/{StatusBadge-B6r5EWBA.js → StatusBadge-CGvKbP7R.js} +1 -1
  121. package/ui/dist/assets/TeamsPanel-Bf6GaUni.js +1 -0
  122. package/ui/dist/assets/{TelemetryPanel-D6o14H-i.js → TelemetryPanel-JZ90gJXC.js} +1 -1
  123. package/ui/dist/assets/TitanCanvas-Hk49NFcA.js +1092 -0
  124. package/ui/dist/assets/ToolsView-Cq7Fuq3i.js +2 -0
  125. package/ui/dist/assets/{Tooltip-DNsYGHC9.js → Tooltip-CcoZrKsl.js} +1 -1
  126. package/ui/dist/assets/{TraceViewer-TOpdmqLF.js → TraceViewer-ojGf0drx.js} +1 -1
  127. package/ui/dist/assets/TrainingPanel-CWnP4H2l.js +1 -0
  128. package/ui/dist/assets/{VoiceOverlay-XIyCbAP7.js → VoiceOverlay-Dn6iaYgd.js} +1 -1
  129. package/ui/dist/assets/VramPanel-CLd9Ggck.js +1 -0
  130. package/ui/dist/assets/WatchView-CQBemwsm.js +13 -0
  131. package/ui/dist/assets/WorkTab-BOfTN-Bd.js +1 -0
  132. package/ui/dist/assets/WorkflowsPanel-qzNS0p0u.js +11 -0
  133. package/ui/dist/assets/{arrow-left-CQF-yBIU.js → arrow-left-c-8OFZUV.js} +1 -1
  134. package/ui/dist/assets/{chart-column-1smg0GbX.js → chart-column-x6L66Qw7.js} +1 -1
  135. package/ui/dist/assets/{circle-check-big-BiMDFx6C.js → circle-check-big-WaW3U3Xl.js} +1 -1
  136. package/ui/dist/assets/{dollar-sign-DMYH4Q_a.js → dollar-sign-D2Oce4Ru.js} +1 -1
  137. package/ui/dist/assets/{download-BYFd-yl6.js → download-YvPDLlFJ.js} +1 -1
  138. package/ui/dist/assets/eye-off-DIMcxsdQ.js +6 -0
  139. package/ui/dist/assets/{funnel-pWBglhfw.js → funnel-DqD9srZu.js} +1 -1
  140. package/ui/dist/assets/{git-branch-Cgqic2Us.js → git-branch-0FamUEbU.js} +1 -1
  141. package/ui/dist/assets/index-D932CbpQ.css +1 -0
  142. package/ui/dist/assets/index-NatBSFxj.js +227 -0
  143. package/ui/dist/assets/{legacy-BHbi-Nm_.js → legacy-DOO7F5cq.js} +1 -1
  144. package/ui/dist/assets/{lightbulb-D_y0Mtyq.js → lightbulb-Bk6KlR6q.js} +1 -1
  145. package/ui/dist/assets/pause-DDC_zUiJ.js +6 -0
  146. package/ui/dist/assets/{play-2xR4_zUG.js → play-BPXbHToG.js} +1 -1
  147. package/ui/dist/assets/{plug-DhvhYYy_.js → plug-Dxp-sWVF.js} +1 -1
  148. package/ui/dist/assets/proxy-vU7v4NVM.js +9 -0
  149. package/ui/dist/assets/square-Bn_0tYME.js +6 -0
  150. package/ui/dist/assets/target-BrtxUtzl.js +6 -0
  151. package/ui/dist/assets/toggle-right-CYphlpN5.js +11 -0
  152. package/ui/dist/assets/{trash-2-DmRaMz9e.js → trash-2-C_Jsp23A.js} +1 -1
  153. package/ui/dist/assets/{trending-up-DsDcs3Jo.js → trending-up-DrtLViSm.js} +1 -1
  154. package/ui/dist/assets/trophy-DdRzAOfo.js +6 -0
  155. package/ui/dist/index.html +2 -2
  156. package/ui/dist/assets/CPFiles-G7veSjMg.js +0 -6
  157. package/ui/dist/assets/CPGoals-C3DlKJrJ.js +0 -1
  158. package/ui/dist/assets/CPInbox-D10curQs.js +0 -16
  159. package/ui/dist/assets/ChannelsPanel-M3pO2htW.js +0 -1
  160. package/ui/dist/assets/CommandPostHub-CW9OY1A4.js +0 -37
  161. package/ui/dist/assets/InlineEditableField-CH-jR3LC.js +0 -11
  162. package/ui/dist/assets/IntegrationsPanel-EaN999Te.js +0 -1
  163. package/ui/dist/assets/IntelligenceView-Q4DBmJpJ.js +0 -2
  164. package/ui/dist/assets/McpPanel-zC7jTaSx.js +0 -6
  165. package/ui/dist/assets/MeshPanel-CqtYZ74K.js +0 -11
  166. package/ui/dist/assets/NvidiaPanel-BVIZFHet.js +0 -1
  167. package/ui/dist/assets/SelfImprovePanel-PSCYO6sx.js +0 -11
  168. package/ui/dist/assets/SessionsTab-Cn3dGgjX.js +0 -1
  169. package/ui/dist/assets/SettingsView-3BSIzAfW.js +0 -2
  170. package/ui/dist/assets/TitanCanvas-cnb7R1gS.js +0 -1056
  171. package/ui/dist/assets/ToolsView-Dp-xUWJG.js +0 -2
  172. package/ui/dist/assets/WorkTab-Pgq-iLz9.js +0 -1
  173. package/ui/dist/assets/WorkflowsPanel-B91LeW7r.js +0 -21
  174. package/ui/dist/assets/eye-BfW7UcEC.js +0 -11
  175. package/ui/dist/assets/index-BWSnB6Kr.js +0 -227
  176. package/ui/dist/assets/index-Dtw1pbjc.css +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/providers/ollama.ts"],"sourcesContent":["/**\n * TITAN — Ollama Provider (Local LLMs)\n */\nimport {\n LLMProvider,\n type ChatOptions,\n type ChatResponse,\n type ChatStreamChunk,\n type ToolCall,\n} from './base.js';\nimport { loadConfig } from '../config/config.js';\nimport logger from '../utils/logger.js';\nimport { fetchWithRetry } from '../utils/helpers.js';\nimport { v4 as uuid } from 'uuid';\nimport * as fs from 'fs';\n\nconst COMPONENT = 'Ollama';\n\n/**\n * Per-model context window map for Ollama cloud models.\n * Auto-configures num_ctx to each model's actual maximum to prevent truncation.\n * Sources: Ollama Cloud model cards, March 2026.\n */\nconst CLOUD_MODEL_CTX: Record<string, number> = {\n // GLM-5.1 — 198K context (newest agentic flagship, SOTA SWE-Bench Pro)\n 'glm-5.1:cloud': 198656,\n // GLM-5 — 128K context\n 'glm-5:cloud': 131072,\n // Kimi K2.5 — 256K context (native multimodal agentic, agent swarm)\n 'kimi-k2.5:cloud': 262144,\n // Kimi K2.6 — 256K context (next-gen agentic, enhanced reasoning)\n 'kimi-k2.6:cloud': 262144,\n // Qwen3 Coder Next — 262K context (massive)\n 'qwen3-coder-next:cloud': 262144,\n // Qwen3.5 397B Cloud — 256K context (all variants support 256K)\n 'qwen3.5:397b-cloud': 262144,\n // DeepSeek V3.1 — 128K context\n 'deepseek-v3.1:671b-cloud': 131072,\n // DeepSeek V3.2 — 160K context (DSA long-context optimized)\n 'deepseek-v3.2:671b-cloud': 163840,\n 'deepseek-v3.2:cloud': 163840,\n // Devstral 2 — 128K context\n 'devstral-2:cloud': 131072,\n // Devstral Small 2 (local) — 32K\n 'devstral-small-2': 32768,\n 'devstral-small-2:latest': 32768,\n // Nemotron 3 Nano — 1M native, 32K practical for local\n 'nemotron-3-nano': 32768,\n 'nemotron-3-nano:latest': 32768,\n 'nemotron-3-nano:4b': 32768,\n 'nemotron-3-nano:30b': 32768,\n // Nemotron 3 Super — 256K context (MoE 120B/12B active)\n 'nemotron-3-super:cloud': 262144,\n // Gemini 3 Flash — 1M context\n 'gemini-3-flash-preview:latest': 1048576,\n // GPT OSS — 128K\n 'gpt-oss:120b-cloud': 131072,\n // MiniMax M2.7 — 200K context (Agent Teams, dynamic tool search)\n 'minimax-m2.7:cloud': 204800,\n // Gemma 4 — 256K context (native function calling)\n 'gemma4:cloud': 262144,\n // Qwen3.5 35B local — 32K\n 'qwen3.5:35b': 32768,\n};\n\n/**\n * Model capability profiles — controls how TITAN adapts to each model's strengths.\n * Instead of blanket rules for all models, each model gets tuned behavior.\n *\n * selfSelectsTools: Model picks tools well on its own — don't force tool_choice='required'\n * thinkingWithTools: Model benefits from thinking (<think> tags) during tool calling\n * needsSystemMerge: Model ignores standalone system messages — merge into first user msg\n * toolTemperature: Optimal temperature for tool-calling tasks (null = use caller's value or 0.5 default)\n * toolTopP: Optimal top_p for tool calling (null = omit)\n * toolTopK: Optimal top_k for tool calling (null = omit)\n */\ninterface ModelCapabilities {\n selfSelectsTools: boolean;\n thinkingWithTools: boolean;\n needsSystemMerge: boolean;\n toolTemperature: number | null;\n toolTopP: number | null;\n toolTopK: number | null;\n}\n\nconst DEFAULT_CAPABILITIES: ModelCapabilities = {\n selfSelectsTools: false,\n thinkingWithTools: false,\n needsSystemMerge: true, // Conservative default: merge for unknown models\n toolTemperature: 0.5,\n toolTopP: null,\n toolTopK: null,\n};\n\n/** Heuristic: infer capabilities from model name patterns when no hardcoded\n * entry exists. Most modern models (2024+) support native tool calling and\n * handle system prompts correctly. This prevents unknown models from being\n * crippled by overly conservative defaults. */\nfunction inferCapabilitiesFromName(modelName: string): Partial<ModelCapabilities> | undefined {\n const lower = modelName.toLowerCase();\n\n // Cloud-hosted models are almost always modern and capable\n if (lower.includes(':cloud') || lower.includes('-cloud')) {\n return { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 };\n }\n\n // Large local models (30B+) are typically capable\n const sizeMatch = lower.match(/(\\d+)b/);\n if (sizeMatch) {\n const size = parseInt(sizeMatch[1], 10);\n if (size >= 30) {\n return { selfSelectsTools: true, thinkingWithTools: false, needsSystemMerge: false, toolTemperature: 0.5 };\n }\n }\n\n // Known-capable families by name pattern (even if not in hardcoded map)\n const capableFamilies = ['qwen', 'glm', 'deepseek', 'kimi', 'gemma', 'nemotron', 'devstral', 'gemini', 'mistral-large', 'llama3.3', 'llama4', 'phi4', 'command-r-plus'];\n for (const family of capableFamilies) {\n if (lower.includes(family)) {\n return { selfSelectsTools: true, thinkingWithTools: false, needsSystemMerge: false, toolTemperature: 0.5 };\n }\n }\n\n // Truly unknown small local models — stay conservative\n return undefined;\n}\n\nconst MODEL_CAPABILITIES: Record<string, Partial<ModelCapabilities>> = {\n // ── Qwen family — excellent tool calling, uses thinking ──\n 'qwen3.5': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.7 },\n 'qwen3': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.7 },\n 'qwen3-coder-next': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.5 },\n\n // ── DeepSeek family — strong reasoning, good tool use ──\n 'deepseek-v3': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 },\n 'deepseek-v3.1': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 },\n 'deepseek-v3.2': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 },\n\n // ── MiniMax M2.7 — XML tool format, needs special handling ──\n // Hunt Finding #05 (2026-04-14): flipped selfSelectsTools from true→false.\n // Confirmed by reproducing: a \"use shell to run uptime\" prompt returned\n // fabricated uptime text with no tool call. The model hallucinates instead\n // of calling tools when given the choice. Setting false forces the agent\n // loop's forceToolUse to fire `tool_choice: required`, which prevents this\n // class of hallucination at the API level.\n 'minimax-m2.7': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.8, toolTopP: 0.95, toolTopK: 40 },\n 'minimax-m2': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.8, toolTopP: 0.95 },\n\n // ── Gemma family — good tool use, no thinking ──\n 'gemma4': { selfSelectsTools: true, thinkingWithTools: false, needsSystemMerge: false, toolTemperature: 1.0, toolTopP: 0.95, toolTopK: 64 },\n 'gemma-3': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.4 },\n\n // ── GLM family — GLM-5.1 is agentic flagship, SOTA SWE-Bench Pro, 198K ctx ──\n 'glm-5.1': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 },\n 'glm-5': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.4 },\n 'glm-4.7': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.4 },\n\n // ── Nemotron — Super is 256K MoE optimized for collaborative agents ──\n 'nemotron-3-super': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: true, toolTemperature: 0.4 },\n 'nemotron-3-nano': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.3 },\n\n // ── Kimi K2.5 — 256K, native agentic, agent swarm decomposition ──\n 'kimi-k2.5': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 },\n 'kimi-k2.6': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 },\n\n // ── Devstral — code-focused ──\n 'devstral-2': { selfSelectsTools: true, thinkingWithTools: false, needsSystemMerge: false, toolTemperature: 0.4 },\n 'devstral-small-2': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.3 },\n\n // ── Gemini — handles system messages well ──\n 'gemini-3-flash': { selfSelectsTools: true, thinkingWithTools: false, needsSystemMerge: false, toolTemperature: 0.5 },\n\n // ── Llama/Mistral — weaker tool calling ──\n 'llama3.1': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.3 },\n 'llama3.2': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.3 },\n 'mistral': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.3 },\n};\n\n/** Resolve capabilities for a model.\n *\n * Lookup order:\n * 1. Empirical probe result from capabilities registry (~/.titan/model-capabilities.json)\n * — This reflects ACTUAL behavior tested against the live model\n * 2. Hardcoded MODEL_CAPABILITIES map (this file) — matched by longest prefix\n * 3. DEFAULT_CAPABILITIES — conservative fallback for unknown models\n */\nfunction getModelCapabilities(modelName: string): ModelCapabilities {\n // Step 1: Check empirical probe registry (preferred)\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { getProbeResult, isProbeStale } = require('../agent/capabilitiesRegistry.js') as typeof import('../agent/capabilitiesRegistry.js');\n const probe = getProbeResult(modelName) || getProbeResult(`ollama/${modelName}`);\n if (probe && !isProbeStale(probe)) {\n // Convert probe result to capability flags\n return {\n ...DEFAULT_CAPABILITIES,\n selfSelectsTools: probe.nativeToolCalls,\n thinkingWithTools: probe.hasThinkingMode && !probe.needsExplicitThinkFalse,\n needsSystemMerge: !probe.respectsSystemPrompt,\n toolTemperature: probe.nativeToolCalls ? 0.5 : 0.3,\n toolTopP: null,\n toolTopK: null,\n };\n }\n } catch {\n // Registry not available (e.g., during tests) — fall through\n }\n\n/** Track which unknown models we've already triggered background probes for */\nconst probeInFlight = new Set<string>();\n\n/** Trigger a background capability probe for an unknown model.\n * Fire-and-forget: the next request will pick up the result from the registry. */\nfunction triggerBackgroundProbe(modelName: string): void {\n if (probeInFlight.has(modelName)) return;\n probeInFlight.add(modelName);\n // Dynamic import to avoid circular deps at module load time\n import('../agent/modelProbe.js')\n .then(({ probeModel }) => probeModel(`ollama/${modelName}`))\n .then((result) => import('../agent/capabilitiesRegistry.js')\n .then(({ recordProbeResult }) => {\n recordProbeResult(result);\n logger.info(COMPONENT, `Background probe complete for ${modelName}: nativeTools=${result.nativeToolCalls}, respectsSystem=${result.respectsSystemPrompt}`);\n }))\n .catch((err) => logger.warn(COMPONENT, `Background probe failed for ${modelName}: ${(err as Error).message}`))\n .finally(() => probeInFlight.delete(modelName));\n}\n\n // Step 2: Hardcoded map (prefix-matched, longest wins)\n const bare = modelName.includes('/') ? modelName.split('/').slice(1).join('/') : modelName;\n const baseName = bare.replace(/:(cloud|latest|\\d+b(-cloud)?)$/i, '');\n\n let bestMatch: Partial<ModelCapabilities> | undefined;\n let bestLen = 0;\n for (const [pattern, caps] of Object.entries(MODEL_CAPABILITIES)) {\n if (baseName === pattern || baseName.startsWith(pattern)) {\n if (pattern.length > bestLen) {\n bestMatch = caps;\n bestLen = pattern.length;\n }\n }\n }\n\n if (!bestMatch) {\n // Try heuristic inference from model name before falling back to defaults\n const inferred = inferCapabilitiesFromName(modelName);\n if (inferred) {\n logger.info(COMPONENT, `Model \"${modelName}\" not in hardcoded map — using inferred capabilities: ${JSON.stringify(inferred)}`);\n bestMatch = inferred;\n } else {\n logger.info(COMPONENT, `Model \"${modelName}\" not in capabilities database or registry — using conservative defaults. Triggering background probe...`);\n triggerBackgroundProbe(modelName);\n }\n }\n return { ...DEFAULT_CAPABILITIES, ...(bestMatch || {}) };\n}\n\n/** Get the optimal num_ctx for a given model name */\nfunction getModelCtx(modelName: string): number {\n const bare = modelName.includes('/') ? modelName.split('/').slice(1).join('/') : modelName;\n if (CLOUD_MODEL_CTX[bare]) return CLOUD_MODEL_CTX[bare];\n\n // Heuristic: modern cloud models typically have 128K+ context\n if (bare.endsWith(':cloud') || bare.endsWith('-cloud')) return 131072;\n\n // Heuristic: large local models (30B+) often support 32K-64K\n const sizeMatch = bare.match(/(\\d+)b/i);\n if (sizeMatch) {\n const size = parseInt(sizeMatch[1], 10);\n if (size >= 70) return 65536;\n if (size >= 30) return 32768;\n if (size >= 14) return 16384;\n }\n\n // Conservative fallback for tiny unknown local models\n return 8192;\n}\n\n/** Max system prompt length for cloud models with tool calling.\n * Cloud models have 128K+ context — keep this high enough to always include\n * the full descriptions of any tools actively being used in the current task.\n */\nconst CLOUD_MAX_SYSTEM_PROMPT = 8000;\n\n/** Compress a system prompt for cloud models with tool calling.\n * Preserves (in priority order):\n * 1. Tool Execution rules (ReAct loop, MUST/NEVER — highest priority)\n * 2. Active tool descriptions (tools currently in use — must not be stripped)\n * 3. Identity\n * 4. Brief capabilities + behavior reminder\n *\n * @param content The full system prompt to compress\n * @param activeTools Descriptions of tools actively in use — always preserved\n */\nfunction compressSystemPrompt(content: string, activeTools?: Array<{ name: string; description: string }>): string {\n if (content.length <= CLOUD_MAX_SYSTEM_PROMPT) return content;\n\n const sections: string[] = [];\n\n // 1. Tool Execution rules — always first, always preserved\n const toolExecMatch = content.match(/## Tool Execution — HIGHEST PRIORITY[\\s\\S]*?(?=\\n## CRITICAL)/);\n if (toolExecMatch) {\n sections.push(toolExecMatch[0].trim());\n } else {\n sections.push(`## Tool Execution — HIGHEST PRIORITY\nYou are an AI agent. Your PRIMARY function is to execute tasks using tools.\n\nReAct Loop: THINK → ACT (call tool) → OBSERVE (read result) → REPEAT until done.\n\nMUST: call web_search+web_fetch for factual questions, call write_file/edit_file to save files (NEVER output file content as text), call shell for commands, call tool_search if unsure which tool to use.\nNEVER: describe what you could do, output file content inline, generate current facts from memory, tell user to visit a URL.\n\nRight: asked to write a file → call write_file immediately.\nWrong: asked to write a file → output the content as text in your reply.`);\n }\n\n // 2. Identity (shortened)\n const identityMatch = content.match(/## CRITICAL: Your Identity[\\s\\S]*?(?=\\n## )/);\n if (identityMatch) sections.push(identityMatch[0].trim());\n\n // 3. Brief capabilities + behavior\n sections.push('## Tools Available\\nShell, file read/write/edit, web search/fetch, browser, memory, weather, code execution, gmail, gdrive, gcal_personal, gtasks, gcontacts. Use tool_search to discover any tool not listed here.');\n sections.push('## Behavior\\n- Lead with action — call tools immediately, explain briefly after\\n- Never re-plan mid-task after CONFIRM — execute directly\\n- Confirm before destructive operations');\n\n // 4. Active tool descriptions — only inject if budget allows (max 2000 chars for tools).\n // This prevents the model from forgetting available actions mid-task (e.g. after CONFIRM).\n if (activeTools && activeTools.length > 0) {\n const TOOL_BUDGET = 2000;\n const toolLines: string[] = [];\n let toolChars = 0;\n for (const t of activeTools) {\n // Use first 150 chars of description to keep it compact\n const desc = t.description.length > 150 ? t.description.slice(0, 147) + '...' : t.description;\n const line = `- **${t.name}**: ${desc}`;\n if (toolChars + line.length > TOOL_BUDGET) break;\n toolLines.push(line);\n toolChars += line.length;\n }\n if (toolLines.length > 0) {\n sections.push(`## Active Tools\\n${toolLines.join('\\n')}`);\n }\n }\n\n const compressed = sections.join('\\n\\n');\n // Safety: never return something larger than the original\n if (compressed.length >= content.length) {\n logger.info(COMPONENT, `Compressed prompt would be larger (${compressed.length} vs ${content.length}), using truncated original`);\n return content.slice(0, CLOUD_MAX_SYSTEM_PROMPT);\n }\n logger.info(COMPONENT, `Compressed system prompt for cloud model: ${content.length} → ${compressed.length} chars`);\n return compressed;\n}\n\n/**\n * Trim messages for cloud models while preserving tool call/response pairs.\n * Naive slicing can split a tool call from its response, breaking the tool calling contract.\n * This walks backwards keeping assistant+tool pairs together.\n */\nfunction trimPreservingToolPairs(msgs: Array<Record<string, unknown>>, maxTotal: number): Array<Record<string, unknown>> {\n const systemMsgs = msgs.filter(m => m.role === 'system');\n const nonSystem = msgs.filter(m => m.role !== 'system');\n const maxNonSystem = maxTotal - systemMsgs.length;\n\n if (nonSystem.length <= maxNonSystem) return msgs;\n\n // Walk backwards, keeping tool/assistant pairs together\n const kept: Array<Record<string, unknown>> = [];\n let i = nonSystem.length - 1;\n while (i >= 0 && kept.length < maxNonSystem) {\n const msg = nonSystem[i];\n if (msg.role === 'tool') {\n // Keep this tool result and find its assistant parent\n kept.unshift(msg);\n for (let j = i - 1; j >= 0; j--) {\n if (nonSystem[j].role === 'assistant' && (nonSystem[j].tool_calls || nonSystem[j].toolCalls)) {\n kept.unshift(nonSystem[j]);\n i = j - 1;\n break;\n }\n if (nonSystem[j].role === 'tool') {\n // Sibling tool result from same batch\n kept.unshift(nonSystem[j]);\n } else {\n i = j;\n break;\n }\n }\n } else {\n kept.unshift(msg);\n i--;\n }\n }\n\n return [...systemMsgs, ...kept];\n}\n\n/** Simplify tool parameter schemas for cloud models.\n * Strips Zod artifacts ($schema, additionalProperties, etc.) that can\n * confuse cloud model tool-calling.\n */\nfunction simplifySchema(schema: Record<string, unknown> | undefined): Record<string, unknown> {\n if (!schema) return { type: 'object', properties: {} };\n const clean: Record<string, unknown> = { type: schema.type || 'object' };\n if (schema.properties) {\n const props: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(schema.properties as Record<string, Record<string, unknown>>)) {\n // Flatten each property to just type + description\n const prop: Record<string, unknown> = { type: val.type || 'string' };\n if (val.description) prop.description = val.description;\n if (val.enum) prop.enum = val.enum;\n if (val.default !== undefined) prop.default = val.default;\n props[key] = prop;\n }\n clean.properties = props;\n }\n if (schema.required) clean.required = schema.required;\n return clean;\n}\n\nexport class OllamaProvider extends LLMProvider {\n readonly name = 'ollama';\n readonly displayName = 'Ollama (Local)';\n\n private get baseUrl(): string {\n const config = loadConfig();\n return config.providers.ollama.baseUrl || process.env.OLLAMA_BASE_URL || 'http://localhost:11434';\n }\n\n async chat(options: ChatOptions): Promise<ChatResponse> {\n const model = (options.model || 'llama3.1').replace('ollama/', '');\n logger.debug(COMPONENT, `Chat request: model=${model}, messages=${options.messages.length}`);\n\n // Cloud models (Ollama Pro): detect via -cloud suffix or :cloud tag\n const isCloudModel = model.includes('-cloud') || model.includes(':cloud');\n const hasTools = options.tools && options.tools.length > 0;\n const hasToolRoleMessages = options.messages.some(m => m.role === 'tool');\n\n const body: Record<string, unknown> = {\n model,\n messages: options.messages.map((m) => {\n const msg: Record<string, unknown> = { role: m.role };\n // Compress system prompts for cloud models with tools to improve tool-calling compliance.\n // Pass descriptions of complex tools (>200 chars) so compression always preserves them —\n // prevents the model from forgetting available actions mid-task (e.g. after CONFIRM).\n if (m.role === 'system' && isCloudModel && hasTools) {\n const activeToolDescs = (options.tools ?? [])\n .filter(t => (t.function.description?.length ?? 0) > 200)\n .map(t => ({ name: t.function.name, description: t.function.description ?? '' }));\n msg.content = compressSystemPrompt(m.content, activeToolDescs.length > 0 ? activeToolDescs : undefined);\n } else {\n msg.content = m.content;\n }\n if (m.toolCalls && m.toolCalls.length > 0) {\n msg.tool_calls = m.toolCalls.map(tc => {\n let parsedArgs: Record<string, unknown> = {};\n try {\n parsedArgs = JSON.parse(tc.function.arguments || '{}');\n } catch {\n logger.warn(COMPONENT, `Malformed tool arguments for ${tc.function.name}, using empty args`);\n }\n // v4.13: Gemini's Ollama-compat adapter rejects\n // function_call.name === ''. Some models emit empty\n // names for tool_calls when the call is malformed;\n // stamp a placeholder so the whole turn isn't\n // rejected with HTTP 400 \"Name cannot be empty\".\n const fnName = (tc.function.name || '').trim() || 'unknown_tool';\n const out: Record<string, unknown> = {\n id: tc.id,\n type: tc.type || 'function',\n function: {\n name: fnName,\n arguments: parsedArgs,\n },\n };\n // v4.13: relay Gemini thought_signature through the\n // round-trip. Ollama's Gemini proxy needs it on every\n // subsequent functionCall part or rejects with\n // \"Function call is missing a thought_signature\".\n if (tc.thoughtSignature) {\n (out.function as Record<string, unknown>).thought_signature = tc.thoughtSignature;\n out.thought_signature = tc.thoughtSignature;\n }\n return out;\n });\n }\n if (m.toolCallId) msg.tool_call_id = m.toolCallId;\n // Cloud models (Gemini API) require function_response.name to be non-empty.\n // Guarantee a non-empty name on every tool-role message.\n if (m.role === 'tool') {\n const toolName = (m.name || '').trim() || 'tool';\n msg.name = toolName;\n } else if (m.name) {\n msg.name = m.name;\n }\n return msg;\n }),\n stream: false,\n keep_alive: '30m',\n options: {\n // Auto-configure context window per model's known maximum.\n // getModelCtx() returns the correct num_ctx for each cloud/local model.\n // v4.10.0-local (cost cap): capped cloud num_predict to 8K\n // (was 32K). OpenRouter's paid models reject requests whose\n // max_tokens exceeds the remaining credit, even though most\n // responses don't come close to that. 8K is plenty for any\n // single turn and keeps us from getting HTTP 402s when\n // credit runs low.\n num_predict: options.maxTokens || (isCloudModel ? 8192 : 16384),\n num_ctx: getModelCtx(model),\n temperature: options.temperature ?? 0.7,\n },\n };\n\n // Model capabilities — adapts behavior per model family\n const caps = getModelCapabilities(model);\n\n // Thinking mode: explicitly control per model capabilities.\n // Models that don't benefit from thinking (thinkingWithTools=false) get it disabled\n // to prevent content being routed to the thinking field instead of content field.\n // This is critical for models like minimax-m2.7:cloud which put ALL output in\n // the thinking field when think is unset, leaving content empty.\n if (options.thinking === false) {\n body.think = false;\n } else if (options.thinking === true) {\n body.think = true;\n } else if (isCloudModel && !caps.thinkingWithTools) {\n // Disable thinking for cloud models that don't benefit from it —\n // both with and without tools. Prevents empty content field.\n body.think = false;\n }\n // Otherwise: omit body.think — let the model decide\n\n // Per-turn override: when the conversation contains tool-role messages,\n // force think=false regardless of caller intent. The GLM-family\n // tool-call parser on the server (vLLM #39611, confirmed by Z.ai docs\n // for GLM-5.1) silently drops tool results when enable_thinking=true,\n // breaking the multi-turn tool loop. Z.ai's own guidance: disable\n // thinking on tool-call turns. This keeps reasoning available for\n // planning turns while preventing the drop on execution turns.\n if (hasToolRoleMessages && body.think !== false) {\n const priorIntent = body.think === undefined ? 'unset' : String(body.think);\n body.think = false;\n logger.info(COMPONENT, `[ToolTurnThinkOverride] Forcing think=false for ${model} (tool-role message present, caller intent=${priorIntent})`);\n }\n\n if (options.tools && options.tools.length > 0) {\n body.tools = options.tools.map((t) => ({\n type: 'function',\n function: {\n name: t.function.name,\n description: t.function.description,\n parameters: simplifySchema(t.function.parameters),\n },\n }));\n // Per-model optimal sampling for tool calling\n (body.options as Record<string, unknown>).temperature = options.temperature ?? caps.toolTemperature ?? 0.5;\n if (caps.toolTopP) (body.options as Record<string, unknown>).top_p = caps.toolTopP;\n if (caps.toolTopK) (body.options as Record<string, unknown>).top_k = caps.toolTopK;\n\n // Force a tool call on the first round when the task requires it\n // Models that self-select tools well don't need forcing — it hurts them\n if (options.forceToolUse && !caps.selfSelectsTools) {\n body.tool_choice = 'required';\n logger.info(COMPONENT, `[ToolChoiceRequired] Setting tool_choice=required for ${model} (forceToolUse=true, selfSelectsTools=false)`);\n } else if (options.forceToolUse && caps.selfSelectsTools) {\n logger.info(COMPONENT, `[ToolChoiceSkipped] forceToolUse=true but selfSelectsTools=true for ${model} — NOT setting tool_choice`);\n }\n }\n\n // Ollama-native structured outputs — constrain generation to a JSON schema.\n // https://docs.ollama.com/capabilities/structured-outputs.md\n if (options.format !== undefined) {\n body.format = options.format;\n }\n\n // Cloud models: trim conversation history preserving tool call/response pairs.\n // With 131K context window, cloud models can handle much longer histories.\n // E1: Use >= 80 with margin (trim to 75) to prevent off-by-one at exact boundary.\n if (isCloudModel && hasTools) {\n const msgs = body.messages as Array<Record<string, unknown>>;\n if (msgs.length >= 80) {\n const trimmed = trimPreservingToolPairs(msgs, 80);\n logger.info(COMPONENT, `Cloud model context trim: ${msgs.length} → ${trimmed.length} messages`);\n body.messages = trimmed;\n }\n }\n\n // Some models ignore standalone system messages during tool calling.\n // Only merge when the model's capability profile says it needs it.\n if (hasTools && caps.needsSystemMerge) {\n const msgs = body.messages as Array<Record<string, unknown>>;\n const sysIdx = msgs.findIndex(m => m.role === 'system');\n const firstUserIdx = msgs.findIndex(m => m.role === 'user');\n if (sysIdx >= 0 && firstUserIdx >= 0 && msgs[sysIdx].content) {\n const sysContent = msgs[sysIdx].content as string;\n msgs[firstUserIdx].content = `[System Instructions]\\n${sysContent}\\n\\n[User Message]\\n${msgs[firstUserIdx].content}`;\n msgs.splice(sysIdx, 1); // Remove the standalone system message\n logger.info(COMPONENT, `Merged system prompt into first user message for cloud model compatibility`);\n }\n }\n\n const sentMessages = body.messages as Array<{role: string; content: string}>;\n const toolNames = body.tools ? (body.tools as Array<{function: {name: string}}>).map(t => t.function.name) : [];\n logger.info(COMPONENT, `Chat request: model=${model}, cloud=${isCloudModel}, tools=[${toolNames.join(',')}], think=${body.think}, messages=${sentMessages.length}`);\n \n if (process.env.DUMP_OLLAMA_BODY === '1' || model.includes('gemini')) {\n logger.error(COMPONENT, `[DUMP_BODY] Dumping failing request body for ${model} to /tmp/ollama-body-dump.json`);\n try {\n fs.writeFileSync('/tmp/ollama-body-dump.json', JSON.stringify(body, null, 2));\n } catch (e) {\n logger.error(COMPONENT, `Failed to dump body: ${e}`);\n }\n }\n\n // Cloud models routed through Ollama need longer timeouts (they proxy to remote APIs)\n const timeoutMs = isCloudModel ? 300_000 : 120_000; // 5min cloud, 2min local\n let response = await fetchWithRetry(`${this.baseUrl}/api/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n }, { timeoutMs });\n\n if (!response.ok) {\n const errorText = await response.text();\n // Fallback: if model doesn't support native tool calling, retry without tools\n if (response.status === 400 && errorText.includes('does not support tools') && body.tools) {\n logger.warn(COMPONENT, `Model ${model} does not support native tool calling — running in chat-only mode`);\n delete body.tools;\n response = await fetchWithRetry(`${this.baseUrl}/api/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n }, { timeoutMs });\n if (!response.ok) {\n const retryText = await response.text();\n // Hunt Finding #37 (2026-04-14): use createProviderError to\n // attach status + parsed Retry-After so the router actually\n // respects the provider's backoff hint.\n const { createProviderError } = await import('./errorTaxonomy.js');\n throw createProviderError('Ollama', response, retryText, { provider: 'ollama', model });\n }\n } else {\n const { createProviderError } = await import('./errorTaxonomy.js');\n throw createProviderError('Ollama', response, errorText, { provider: 'ollama', model });\n }\n }\n\n // v4.13 ancestor-extraction (Hermes rate_limit_tracker): capture any\n // x-ratelimit-* headers the Ollama proxy exposes. Graceful no-op when\n // the headers aren't present. Provider name is 'ollama' so the router's\n // proactive-backoff logic can consult per-provider state.\n try {\n const { recordHeaders } = await import('./rateLimitTracker.js');\n recordHeaders('ollama', response.headers);\n } catch { /* never fail the chat on tracker issues */ }\n\n const data = await response.json() as Record<string, unknown>;\n const message = data.message as Record<string, unknown>;\n logger.info(COMPONENT, `Response from ${model}: tool_calls=${JSON.stringify(message.tool_calls)}, content_length=${((message.content as string) || '').length}`);\n const toolCalls: ToolCall[] = [];\n\n if (message.tool_calls) {\n for (const tc of message.tool_calls as Array<Record<string, unknown>>) {\n const fn = tc.function as Record<string, unknown>;\n // v4.13: capture Gemini thought_signature if present — needed\n // on the round-trip back or Gemini rejects the next request.\n const thoughtSig = (tc.thought_signature as string | undefined) ??\n (tc.thoughtSignature as string | undefined) ??\n (fn.thought_signature as string | undefined) ??\n (fn.thoughtSignature as string | undefined);\n toolCalls.push({\n id: uuid(),\n type: 'function',\n function: {\n name: fn.name as string,\n arguments: JSON.stringify(fn.arguments),\n },\n ...(thoughtSig ? { thoughtSignature: thoughtSig } : {}),\n });\n }\n }\n\n // A2: Hallucinated tool name detection at provider level (LangGraph pattern)\n if (options.tools && toolCalls.length > 0) {\n const validNames = new Set(options.tools.map(t => t.function.name));\n const invalid = toolCalls.filter(tc => !validNames.has(tc.function.name));\n if (invalid.length > 0) {\n logger.warn(COMPONENT, `[HallucinationGuard] Model hallucinated ${invalid.length} tool name(s): ${invalid.map(tc => tc.function.name).join(', ')}. Will be caught by toolRunner with corrective feedback.`);\n }\n }\n\n // If content is empty but thinking field has content, use it as a fallback.\n // This handles models that route output to thinking field when think is\n // unset or misconfigured. The router's stripThinkingFromResponse() will\n // clean up any reasoning that leaks through, so we can be permissive here.\n let content = (message.content as string) || '';\n if (!content && message.thinking) {\n const thinking = (message.thinking as string) || '';\n if (thinking.length > 0) {\n logger.info(COMPONENT, `[ThinkingFallback] Content empty, using thinking field (${thinking.length} chars)`);\n content = thinking;\n }\n }\n // Strip leaked thinking tags from Qwen/DeepSeek models\n content = content.replace(/^[\\s\\S]*?<\\/think>\\s*/m, '').trim();\n\n return {\n id: uuid(),\n content,\n toolCalls: toolCalls.length > 0 ? toolCalls : undefined,\n usage: {\n promptTokens: (data.prompt_eval_count as number) || 0,\n completionTokens: (data.eval_count as number) || 0,\n totalTokens: ((data.prompt_eval_count as number) || 0) + ((data.eval_count as number) || 0),\n },\n finishReason: toolCalls.length > 0 ? 'tool_calls' : 'stop',\n model: `ollama/${model}`,\n };\n }\n\n async *chatStream(options: ChatOptions): AsyncGenerator<ChatStreamChunk> {\n const model = (options.model || 'llama3.1').replace('ollama/', '');\n\n // Cloud models (Ollama Pro): detect via -cloud suffix or :cloud tag\n const isCloudModel = model.includes('-cloud') || model.includes(':cloud');\n const hasTools = options.tools && options.tools.length > 0;\n const hasToolRoleMessages = options.messages.some(m => m.role === 'tool');\n\n const body: Record<string, unknown> = {\n model,\n messages: options.messages.map((m) => {\n const msg: Record<string, unknown> = { role: m.role };\n // Compress system prompts for cloud models with tools — preserve active tool descriptions\n if (m.role === 'system' && isCloudModel && hasTools) {\n const activeToolDescs = (options.tools ?? [])\n .filter(t => (t.function.description?.length ?? 0) > 200)\n .map(t => ({ name: t.function.name, description: t.function.description ?? '' }));\n msg.content = compressSystemPrompt(m.content, activeToolDescs.length > 0 ? activeToolDescs : undefined);\n } else {\n msg.content = m.content;\n }\n if (m.toolCalls && m.toolCalls.length > 0) {\n msg.tool_calls = m.toolCalls.map(tc => ({\n id: tc.id,\n type: tc.type || 'function',\n function: { name: tc.function.name, arguments: JSON.parse(tc.function.arguments || '{}') }\n }));\n }\n if (m.toolCallId) msg.tool_call_id = m.toolCallId;\n // Cloud models (Gemini API) require function_response.name to be non-empty\n if (m.role === 'tool') {\n msg.name = m.name || 'tool';\n } else if (m.name) {\n msg.name = m.name;\n }\n return msg;\n }),\n stream: true,\n keep_alive: '30m',\n options: {\n // v4.10.0-local (cost cap): 8K cloud cap matches non-stream path\n num_predict: options.maxTokens || (isCloudModel ? 8192 : 16384),\n num_ctx: getModelCtx(model),\n temperature: options.temperature ?? 0.7,\n },\n };\n\n // Model capabilities — adapts behavior per model family\n const caps = getModelCapabilities(model);\n\n // Thinking mode: respect explicit setting, otherwise use model capabilities.\n // Disable for cloud models that don't benefit — prevents empty content field.\n if (options.thinking === false) {\n body.think = false;\n } else if (options.thinking === true) {\n body.think = true;\n } else if (isCloudModel && !caps.thinkingWithTools) {\n body.think = false;\n }\n\n // Per-turn override for tool-role turns (see chat() for rationale: vLLM #39611 / Z.ai docs).\n if (hasToolRoleMessages && body.think !== false) {\n const priorIntent = body.think === undefined ? 'unset' : String(body.think);\n body.think = false;\n logger.info(COMPONENT, `[ToolTurnThinkOverride] (stream) Forcing think=false for ${model} (tool-role message present, caller intent=${priorIntent})`);\n }\n\n if (hasTools) {\n body.tools = options.tools!.map((t) => ({\n type: 'function',\n function: {\n name: t.function.name,\n description: t.function.description,\n parameters: simplifySchema(t.function.parameters),\n },\n }));\n // Per-model optimal sampling for tool calling\n (body.options as Record<string, unknown>).temperature = options.temperature ?? caps.toolTemperature ?? 0.5;\n if (caps.toolTopP) (body.options as Record<string, unknown>).top_p = caps.toolTopP;\n if (caps.toolTopK) (body.options as Record<string, unknown>).top_k = caps.toolTopK;\n\n // Force tool_choice when requested — skip for models that self-select well\n if (options.forceToolUse && !caps.selfSelectsTools) {\n body.tool_choice = 'required';\n }\n }\n\n // Ollama-native structured outputs (stream variant).\n if (options.format !== undefined) {\n body.format = options.format;\n }\n\n // Optimize: trim history preserving tool pairs (cloud models only — local models have smaller contexts)\n if (isCloudModel && hasTools) {\n const msgs = body.messages as Array<Record<string, unknown>>;\n if (msgs.length > 80) {\n const trimmed = trimPreservingToolPairs(msgs, 80);\n logger.info(COMPONENT, `[Stream] Cloud model context trim: ${msgs.length} → ${trimmed.length} messages`);\n body.messages = trimmed;\n }\n }\n // Merge system into first user message only for models that need it\n if (hasTools && caps.needsSystemMerge) {\n const msgs2 = body.messages as Array<Record<string, unknown>>;\n const sysIdx = msgs2.findIndex(m => m.role === 'system');\n const firstUserIdx = msgs2.findIndex(m => m.role === 'user');\n if (sysIdx >= 0 && firstUserIdx >= 0 && msgs2[sysIdx].content) {\n const sysContent = msgs2[sysIdx].content as string;\n msgs2[firstUserIdx].content = `[System Instructions]\\n${sysContent}\\n\\n[User Message]\\n${msgs2[firstUserIdx].content}`;\n msgs2.splice(sysIdx, 1);\n }\n }\n\n try {\n // Cloud models need longer timeouts for streaming too\n const streamTimeoutMs = isCloudModel ? 300_000 : 120_000;\n let response = await fetch(`${this.baseUrl}/api/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(streamTimeoutMs),\n });\n\n if (!response.ok || !response.body) {\n const errorText = await response.text();\n // Fallback: if model doesn't support native tool calling, retry without tools\n if (response.status === 400 && errorText.includes('does not support tools') && body.tools) {\n logger.warn(COMPONENT, `Model ${model} does not support native tool calling — running in chat-only mode`);\n delete body.tools;\n response = await fetch(`${this.baseUrl}/api/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(streamTimeoutMs),\n });\n if (!response.ok || !response.body) {\n const retryText = await response.text();\n yield { type: 'error', error: `Ollama error (${response.status}): ${retryText}` };\n return;\n }\n } else {\n yield { type: 'error', error: `Ollama error (${response.status}): ${errorText}` };\n return;\n }\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let insideThink = false;\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (!line.trim()) continue;\n try {\n const chunk = JSON.parse(line);\n // Handle thinking field for models that put content there\n // Some models (e.g. qwen3.5, nemotron-super:cloud) use the thinking field\n // even when think=false is set — treat thinking as content in that case\n if (!chunk.message?.content && chunk.message?.thinking) {\n if (body.think === false) {\n chunk.message.content = chunk.message.thinking;\n }\n }\n if (chunk.message?.content) {\n let text = chunk.message.content;\n // Strip leaked <think>...</think> blocks from Qwen/DeepSeek\n if (text.includes('<think>')) insideThink = true;\n if (insideThink) {\n if (text.includes('</think>')) {\n text = text.split('</think>').pop()?.trim() || '';\n insideThink = false;\n } else {\n continue; // suppress thinking content\n }\n }\n if (text) yield { type: 'text', content: text };\n }\n if (chunk.message?.tool_calls) {\n for (const tc of chunk.message.tool_calls) {\n const fn = tc.function as Record<string, unknown>;\n yield {\n type: 'tool_call',\n toolCall: { id: uuid(), type: 'function', function: { name: fn.name as string, arguments: JSON.stringify(fn.arguments) } },\n };\n }\n }\n if (chunk.done) break;\n } catch { /* skip malformed NDJSON lines */ }\n }\n }\n yield { type: 'done' };\n } catch (error) {\n yield { type: 'error', error: (error as Error).message };\n }\n }\n\n async listModels(): Promise<string[]> {\n try {\n const response = await fetch(`${this.baseUrl}/api/tags`);\n if (!response.ok) {\n // Hunt Finding #29 (2026-04-14): consume the body even on\n // error paths so the underlying socket can return to the\n // keep-alive pool. Without this, every non-200 response\n // leaks its socket until the GC gets around to it.\n await response.body?.cancel().catch(() => {});\n return [];\n }\n const data = await response.json() as { models?: Array<{ name: string }> };\n return (data.models || []).map((m) => m.name);\n } catch {\n return [];\n }\n }\n\n async healthCheck(): Promise<boolean> {\n try {\n const response = await fetch(`${this.baseUrl}/api/tags`);\n const ok = response.ok;\n // Hunt Finding #29 (2026-04-14): ALWAYS consume or cancel the\n // body. Previously we returned response.ok directly, leaving the\n // body stream dangling and the socket held open.\n await response.body?.cancel().catch(() => {});\n return ok;\n } catch {\n return false;\n }\n }\n}\n"],"mappings":"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","names":["modelName"]}
1
+ {"version":3,"sources":["../../src/providers/ollama.ts"],"sourcesContent":["/**\n * TITAN — Ollama Provider (Local LLMs)\n */\nimport {\n LLMProvider,\n type ChatOptions,\n type ChatResponse,\n type ChatStreamChunk,\n type ToolCall,\n} from './base.js';\nimport { loadConfig } from '../config/config.js';\nimport logger from '../utils/logger.js';\nimport { fetchWithRetry } from '../utils/helpers.js';\nimport { v4 as uuid } from 'uuid';\nimport * as fs from 'fs';\n\nconst COMPONENT = 'Ollama';\n\n/**\n * Per-model context window map for Ollama cloud models.\n * Auto-configures num_ctx to each model's actual maximum to prevent truncation.\n * Sources: Ollama Cloud model cards, March 2026.\n */\nconst CLOUD_MODEL_CTX: Record<string, number> = {\n // GLM-5.1 — 198K context (newest agentic flagship, SOTA SWE-Bench Pro)\n 'glm-5.1:cloud': 198656,\n // GLM-5 — 128K context\n 'glm-5:cloud': 131072,\n // Kimi K2.5 — 256K context (native multimodal agentic, agent swarm)\n 'kimi-k2.5:cloud': 262144,\n // Kimi K2.6 — 256K context (next-gen agentic, enhanced reasoning)\n 'kimi-k2.6:cloud': 262144,\n // Qwen3 Coder Next — 262K context (massive)\n 'qwen3-coder-next:cloud': 262144,\n // Qwen3.5 397B Cloud — 256K context (all variants support 256K)\n 'qwen3.5:397b-cloud': 262144,\n // DeepSeek V3.1 — 128K context\n 'deepseek-v3.1:671b-cloud': 131072,\n // DeepSeek V3.2 — 160K context (DSA long-context optimized)\n 'deepseek-v3.2:671b-cloud': 163840,\n 'deepseek-v3.2:cloud': 163840,\n // Devstral 2 — 128K context\n 'devstral-2:cloud': 131072,\n // Devstral Small 2 (local) — 32K\n 'devstral-small-2': 32768,\n 'devstral-small-2:latest': 32768,\n // Nemotron 3 Nano — 1M native, 32K practical for local\n 'nemotron-3-nano': 32768,\n 'nemotron-3-nano:latest': 32768,\n 'nemotron-3-nano:4b': 32768,\n 'nemotron-3-nano:30b': 32768,\n // Nemotron 3 Super — 256K context (MoE 120B/12B active)\n 'nemotron-3-super:cloud': 262144,\n // Gemini 3 Flash — 1M context\n 'gemini-3-flash-preview:latest': 1048576,\n // GPT OSS — 128K\n 'gpt-oss:120b-cloud': 131072,\n // MiniMax M2.7 — 200K context (Agent Teams, dynamic tool search)\n 'minimax-m2.7:cloud': 204800,\n // Gemma 4 — 256K context (native function calling)\n 'gemma4:cloud': 262144,\n // Qwen3.5 35B local — 32K\n 'qwen3.5:35b': 32768,\n};\n\n/**\n * Model capability profiles — controls how TITAN adapts to each model's strengths.\n * Instead of blanket rules for all models, each model gets tuned behavior.\n *\n * selfSelectsTools: Model picks tools well on its own — don't force tool_choice='required'\n * thinkingWithTools: Model benefits from thinking (<think> tags) during tool calling\n * needsSystemMerge: Model ignores standalone system messages — merge into first user msg\n * toolTemperature: Optimal temperature for tool-calling tasks (null = use caller's value or 0.5 default)\n * toolTopP: Optimal top_p for tool calling (null = omit)\n * toolTopK: Optimal top_k for tool calling (null = omit)\n */\ninterface ModelCapabilities {\n selfSelectsTools: boolean;\n thinkingWithTools: boolean;\n needsSystemMerge: boolean;\n toolTemperature: number | null;\n toolTopP: number | null;\n toolTopK: number | null;\n}\n\nconst DEFAULT_CAPABILITIES: ModelCapabilities = {\n selfSelectsTools: false,\n thinkingWithTools: false,\n needsSystemMerge: true, // Conservative default: merge for unknown models\n toolTemperature: 0.5,\n toolTopP: null,\n toolTopK: null,\n};\n\n/** Heuristic: infer capabilities from model name patterns when no hardcoded\n * entry exists. Most modern models (2024+) support native tool calling and\n * handle system prompts correctly. This prevents unknown models from being\n * crippled by overly conservative defaults. */\nfunction inferCapabilitiesFromName(modelName: string): Partial<ModelCapabilities> | undefined {\n const lower = modelName.toLowerCase();\n\n // Cloud-hosted models are almost always modern and capable\n if (lower.includes(':cloud') || lower.includes('-cloud')) {\n return { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 };\n }\n\n // Large local models (30B+) are typically capable\n const sizeMatch = lower.match(/(\\d+)b/);\n if (sizeMatch) {\n const size = parseInt(sizeMatch[1], 10);\n if (size >= 30) {\n return { selfSelectsTools: true, thinkingWithTools: false, needsSystemMerge: false, toolTemperature: 0.5 };\n }\n }\n\n // Known-capable families by name pattern (even if not in hardcoded map)\n const capableFamilies = ['qwen', 'glm', 'deepseek', 'kimi', 'gemma', 'nemotron', 'devstral', 'gemini', 'mistral-large', 'llama3.3', 'llama4', 'phi4', 'command-r-plus'];\n for (const family of capableFamilies) {\n if (lower.includes(family)) {\n return { selfSelectsTools: true, thinkingWithTools: false, needsSystemMerge: false, toolTemperature: 0.5 };\n }\n }\n\n // Truly unknown small local models — stay conservative\n return undefined;\n}\n\nconst MODEL_CAPABILITIES: Record<string, Partial<ModelCapabilities>> = {\n // ── Qwen family — excellent tool calling, uses thinking ──\n 'qwen3.5': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.7 },\n 'qwen3': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.7 },\n 'qwen3-coder-next': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.5 },\n\n // ── DeepSeek family — strong reasoning, good tool use ──\n 'deepseek-v3': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 },\n 'deepseek-v3.1': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 },\n 'deepseek-v3.2': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 },\n\n // ── MiniMax M2.7 — XML tool format, needs special handling ──\n // Hunt Finding #05 (2026-04-14): flipped selfSelectsTools from true→false.\n // Confirmed by reproducing: a \"use shell to run uptime\" prompt returned\n // fabricated uptime text with no tool call. The model hallucinates instead\n // of calling tools when given the choice. Setting false forces the agent\n // loop's forceToolUse to fire `tool_choice: required`, which prevents this\n // class of hallucination at the API level.\n 'minimax-m2.7': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.8, toolTopP: 0.95, toolTopK: 40 },\n 'minimax-m2': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.8, toolTopP: 0.95 },\n\n // ── Gemma family — good tool use, no thinking ──\n 'gemma4': { selfSelectsTools: true, thinkingWithTools: false, needsSystemMerge: false, toolTemperature: 1.0, toolTopP: 0.95, toolTopK: 64 },\n 'gemma-3': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.4 },\n\n // ── GLM family — GLM-5.1 is agentic flagship, SOTA SWE-Bench Pro, 198K ctx ──\n 'glm-5.1': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 },\n 'glm-5': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.4 },\n 'glm-4.7': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.4 },\n\n // ── Nemotron — Super is 256K MoE optimized for collaborative agents ──\n 'nemotron-3-super': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: true, toolTemperature: 0.4 },\n 'nemotron-3-nano': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.3 },\n\n // ── Kimi K2.5 — 256K, native agentic, agent swarm decomposition ──\n 'kimi-k2.5': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 },\n 'kimi-k2.6': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 },\n\n // ── Devstral — code-focused ──\n 'devstral-2': { selfSelectsTools: true, thinkingWithTools: false, needsSystemMerge: false, toolTemperature: 0.4 },\n 'devstral-small-2': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.3 },\n\n // ── Gemini — handles system messages well ──\n 'gemini-3-flash': { selfSelectsTools: true, thinkingWithTools: false, needsSystemMerge: false, toolTemperature: 0.5 },\n\n // ── Llama/Mistral — weaker tool calling ──\n 'llama3.1': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.3 },\n 'llama3.2': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.3 },\n 'mistral': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.3 },\n};\n\n/** Resolve capabilities for a model.\n *\n * Lookup order:\n * 1. Empirical probe result from capabilities registry (~/.titan/model-capabilities.json)\n * — This reflects ACTUAL behavior tested against the live model\n * 2. Hardcoded MODEL_CAPABILITIES map (this file) — matched by longest prefix\n * 3. DEFAULT_CAPABILITIES — conservative fallback for unknown models\n */\nfunction getModelCapabilities(modelName: string): ModelCapabilities {\n // Step 1: Check empirical probe registry (preferred)\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { getProbeResult, isProbeStale } = require('../agent/capabilitiesRegistry.js') as typeof import('../agent/capabilitiesRegistry.js');\n const probe = getProbeResult(modelName) || getProbeResult(`ollama/${modelName}`);\n if (probe && !isProbeStale(probe)) {\n // Convert probe result to capability flags\n return {\n ...DEFAULT_CAPABILITIES,\n selfSelectsTools: probe.nativeToolCalls,\n thinkingWithTools: probe.hasThinkingMode && !probe.needsExplicitThinkFalse,\n needsSystemMerge: !probe.respectsSystemPrompt,\n toolTemperature: probe.nativeToolCalls ? 0.5 : 0.3,\n toolTopP: null,\n toolTopK: null,\n };\n }\n } catch {\n // Registry not available (e.g., during tests) — fall through\n }\n\n/** Track which unknown models we've already triggered background probes for */\nconst probeInFlight = new Set<string>();\n\n/** Trigger a background capability probe for an unknown model.\n * Fire-and-forget: the next request will pick up the result from the registry. */\nfunction triggerBackgroundProbe(modelName: string): void {\n if (probeInFlight.has(modelName)) return;\n probeInFlight.add(modelName);\n // Dynamic import to avoid circular deps at module load time\n import('../agent/modelProbe.js')\n .then(({ probeModel }) => probeModel(`ollama/${modelName}`))\n .then((result) => import('../agent/capabilitiesRegistry.js')\n .then(({ recordProbeResult }) => {\n recordProbeResult(result);\n logger.info(COMPONENT, `Background probe complete for ${modelName}: nativeTools=${result.nativeToolCalls}, respectsSystem=${result.respectsSystemPrompt}`);\n }))\n .catch((err) => logger.warn(COMPONENT, `Background probe failed for ${modelName}: ${(err as Error).message}`))\n .finally(() => probeInFlight.delete(modelName));\n}\n\n // Step 2: Hardcoded map (prefix-matched, longest wins)\n const bare = modelName.includes('/') ? modelName.split('/').slice(1).join('/') : modelName;\n const baseName = bare.replace(/:(cloud|latest|\\d+b(-cloud)?)$/i, '');\n\n let bestMatch: Partial<ModelCapabilities> | undefined;\n let bestLen = 0;\n for (const [pattern, caps] of Object.entries(MODEL_CAPABILITIES)) {\n if (baseName === pattern || baseName.startsWith(pattern)) {\n if (pattern.length > bestLen) {\n bestMatch = caps;\n bestLen = pattern.length;\n }\n }\n }\n\n if (!bestMatch) {\n // Try heuristic inference from model name before falling back to defaults\n const inferred = inferCapabilitiesFromName(modelName);\n if (inferred) {\n logger.info(COMPONENT, `Model \"${modelName}\" not in hardcoded map — using inferred capabilities: ${JSON.stringify(inferred)}`);\n bestMatch = inferred;\n } else {\n logger.info(COMPONENT, `Model \"${modelName}\" not in capabilities database or registry — using conservative defaults. Triggering background probe...`);\n triggerBackgroundProbe(modelName);\n }\n }\n return { ...DEFAULT_CAPABILITIES, ...(bestMatch || {}) };\n}\n\n/** Get the optimal num_ctx for a given model name */\nfunction getModelCtx(modelName: string): number {\n const bare = modelName.includes('/') ? modelName.split('/').slice(1).join('/') : modelName;\n if (CLOUD_MODEL_CTX[bare]) return CLOUD_MODEL_CTX[bare];\n\n // Heuristic: modern cloud models typically have 128K+ context\n if (bare.endsWith(':cloud') || bare.endsWith('-cloud')) return 131072;\n\n // Heuristic: large local models (30B+) often support 32K-64K\n const sizeMatch = bare.match(/(\\d+)b/i);\n if (sizeMatch) {\n const size = parseInt(sizeMatch[1], 10);\n if (size >= 70) return 65536;\n if (size >= 30) return 32768;\n if (size >= 14) return 16384;\n }\n\n // Conservative fallback for tiny unknown local models\n return 8192;\n}\n\n/** Max system prompt length for cloud models with tool calling.\n * Cloud models have 128K+ context — keep this high enough to always include\n * the full descriptions of any tools actively being used in the current task.\n */\nconst CLOUD_MAX_SYSTEM_PROMPT = 8000;\n\n/** Compress a system prompt for cloud models with tool calling.\n * Preserves (in priority order):\n * 1. Tool Execution rules (ReAct loop, MUST/NEVER — highest priority)\n * 2. Active tool descriptions (tools currently in use — must not be stripped)\n * 3. Identity\n * 4. Brief capabilities + behavior reminder\n *\n * @param content The full system prompt to compress\n * @param activeTools Descriptions of tools actively in use — always preserved\n */\nfunction compressSystemPrompt(content: string, activeTools?: Array<{ name: string; description: string }>): string {\n if (content.length <= CLOUD_MAX_SYSTEM_PROMPT) return content;\n\n const sections: string[] = [];\n\n // 1. Tool Execution rules — always first, always preserved\n const toolExecMatch = content.match(/## Tool Execution — HIGHEST PRIORITY[\\s\\S]*?(?=\\n## CRITICAL)/);\n if (toolExecMatch) {\n sections.push(toolExecMatch[0].trim());\n } else {\n sections.push(`## Tool Execution — HIGHEST PRIORITY\nYou are an AI agent. Your PRIMARY function is to execute tasks using tools.\n\nReAct Loop: THINK → ACT (call tool) → OBSERVE (read result) → REPEAT until done.\n\nMUST: call web_search+web_fetch for factual questions, call write_file/edit_file to save files (NEVER output file content as text), call shell for commands, call tool_search if unsure which tool to use.\nNEVER: describe what you could do, output file content inline, generate current facts from memory, tell user to visit a URL.\n\nRight: asked to write a file → call write_file immediately.\nWrong: asked to write a file → output the content as text in your reply.`);\n }\n\n // 2. Identity (shortened)\n const identityMatch = content.match(/## CRITICAL: Your Identity[\\s\\S]*?(?=\\n## )/);\n if (identityMatch) sections.push(identityMatch[0].trim());\n\n // 3. Brief capabilities + behavior\n sections.push('## Tools Available\\nShell, file read/write/edit, web search/fetch, browser, memory, weather, code execution, gmail, gdrive, gcal_personal, gtasks, gcontacts. Use tool_search to discover any tool not listed here.');\n sections.push('## Behavior\\n- Lead with action — call tools immediately, explain briefly after\\n- Never re-plan mid-task after CONFIRM — execute directly\\n- Confirm before destructive operations');\n\n // 4. Active tool descriptions — only inject if budget allows (max 2000 chars for tools).\n // This prevents the model from forgetting available actions mid-task (e.g. after CONFIRM).\n if (activeTools && activeTools.length > 0) {\n const TOOL_BUDGET = 2000;\n const toolLines: string[] = [];\n let toolChars = 0;\n for (const t of activeTools) {\n // Use first 150 chars of description to keep it compact\n const desc = t.description.length > 150 ? t.description.slice(0, 147) + '...' : t.description;\n const line = `- **${t.name}**: ${desc}`;\n if (toolChars + line.length > TOOL_BUDGET) break;\n toolLines.push(line);\n toolChars += line.length;\n }\n if (toolLines.length > 0) {\n sections.push(`## Active Tools\\n${toolLines.join('\\n')}`);\n }\n }\n\n const compressed = sections.join('\\n\\n');\n // Safety: never return something larger than the original\n if (compressed.length >= content.length) {\n logger.info(COMPONENT, `Compressed prompt would be larger (${compressed.length} vs ${content.length}), using truncated original`);\n return content.slice(0, CLOUD_MAX_SYSTEM_PROMPT);\n }\n logger.info(COMPONENT, `Compressed system prompt for cloud model: ${content.length} → ${compressed.length} chars`);\n return compressed;\n}\n\n/**\n * Trim messages for cloud models while preserving tool call/response pairs.\n * Naive slicing can split a tool call from its response, breaking the tool calling contract.\n * This walks backwards keeping assistant+tool pairs together.\n */\nfunction trimPreservingToolPairs(msgs: Array<Record<string, unknown>>, maxTotal: number): Array<Record<string, unknown>> {\n const systemMsgs = msgs.filter(m => m.role === 'system');\n const nonSystem = msgs.filter(m => m.role !== 'system');\n const maxNonSystem = maxTotal - systemMsgs.length;\n\n if (nonSystem.length <= maxNonSystem) return msgs;\n\n // Walk backwards, keeping tool/assistant pairs together\n const kept: Array<Record<string, unknown>> = [];\n let i = nonSystem.length - 1;\n while (i >= 0 && kept.length < maxNonSystem) {\n const msg = nonSystem[i];\n if (msg.role === 'tool') {\n // Keep this tool result and find its assistant parent\n kept.unshift(msg);\n for (let j = i - 1; j >= 0; j--) {\n if (nonSystem[j].role === 'assistant' && (nonSystem[j].tool_calls || nonSystem[j].toolCalls)) {\n kept.unshift(nonSystem[j]);\n i = j - 1;\n break;\n }\n if (nonSystem[j].role === 'tool') {\n // Sibling tool result from same batch\n kept.unshift(nonSystem[j]);\n } else {\n i = j;\n break;\n }\n }\n } else {\n kept.unshift(msg);\n i--;\n }\n }\n\n return [...systemMsgs, ...kept];\n}\n\n/** Simplify tool parameter schemas for cloud models.\n * Strips Zod artifacts ($schema, additionalProperties, etc.) that can\n * confuse cloud model tool-calling.\n */\nfunction simplifySchema(schema: Record<string, unknown> | undefined): Record<string, unknown> {\n if (!schema) return { type: 'object', properties: {} };\n const clean: Record<string, unknown> = { type: schema.type || 'object' };\n if (schema.properties) {\n const props: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(schema.properties as Record<string, Record<string, unknown>>)) {\n // Flatten each property to just type + description\n const prop: Record<string, unknown> = { type: val.type || 'string' };\n if (val.description) prop.description = val.description;\n if (val.enum) prop.enum = val.enum;\n if (val.default !== undefined) prop.default = val.default;\n props[key] = prop;\n }\n clean.properties = props;\n }\n if (schema.required) clean.required = schema.required;\n return clean;\n}\n\nexport class OllamaProvider extends LLMProvider {\n readonly name = 'ollama';\n readonly displayName = 'Ollama (Local)';\n\n private get baseUrl(): string {\n const config = loadConfig();\n return config.providers.ollama.baseUrl || process.env.OLLAMA_BASE_URL || 'http://localhost:11434';\n }\n\n async chat(options: ChatOptions): Promise<ChatResponse> {\n const model = (options.model || 'llama3.1').replace('ollama/', '');\n logger.debug(COMPONENT, `Chat request: model=${model}, messages=${options.messages.length}`);\n\n // Cloud models (Ollama Pro): detect via -cloud suffix or :cloud tag\n const isCloudModel = model.includes('-cloud') || model.includes(':cloud');\n const hasTools = options.tools && options.tools.length > 0;\n const hasToolRoleMessages = options.messages.some(m => m.role === 'tool');\n\n const body: Record<string, unknown> = {\n model,\n messages: options.messages.map((m) => {\n const msg: Record<string, unknown> = { role: m.role };\n // Compress system prompts for cloud models with tools to improve tool-calling compliance.\n // Pass descriptions of complex tools (>200 chars) so compression always preserves them —\n // prevents the model from forgetting available actions mid-task (e.g. after CONFIRM).\n if (m.role === 'system' && isCloudModel && hasTools) {\n const activeToolDescs = (options.tools ?? [])\n .filter(t => (t.function.description?.length ?? 0) > 200)\n .map(t => ({ name: t.function.name, description: t.function.description ?? '' }));\n msg.content = compressSystemPrompt(m.content, activeToolDescs.length > 0 ? activeToolDescs : undefined);\n } else {\n msg.content = m.content;\n }\n if (m.toolCalls && m.toolCalls.length > 0) {\n msg.tool_calls = m.toolCalls.map(tc => {\n let parsedArgs: Record<string, unknown> = {};\n try {\n parsedArgs = JSON.parse(tc.function.arguments || '{}');\n } catch {\n logger.warn(COMPONENT, `Malformed tool arguments for ${tc.function.name}, using empty args`);\n }\n // v4.13: Gemini's Ollama-compat adapter rejects\n // function_call.name === ''. Some models emit empty\n // names for tool_calls when the call is malformed;\n // stamp a placeholder so the whole turn isn't\n // rejected with HTTP 400 \"Name cannot be empty\".\n const fnName = (tc.function.name || '').trim() || 'unknown_tool';\n const out: Record<string, unknown> = {\n id: tc.id,\n type: tc.type || 'function',\n function: {\n name: fnName,\n arguments: parsedArgs,\n },\n };\n // v4.13: relay Gemini thought_signature through the\n // round-trip. Ollama's Gemini proxy needs it on every\n // subsequent functionCall part or rejects with\n // \"Function call is missing a thought_signature\".\n if (tc.thoughtSignature) {\n (out.function as Record<string, unknown>).thought_signature = tc.thoughtSignature;\n out.thought_signature = tc.thoughtSignature;\n }\n return out;\n });\n }\n if (m.toolCallId) msg.tool_call_id = m.toolCallId;\n // Cloud models (Gemini API) require function_response.name to be non-empty.\n // Guarantee a non-empty name on every tool-role message.\n if (m.role === 'tool') {\n const toolName = (m.name || '').trim() || 'tool';\n msg.name = toolName;\n } else if (m.name) {\n msg.name = m.name;\n }\n return msg;\n }),\n stream: false,\n keep_alive: '30m',\n options: {\n // Auto-configure context window per model's known maximum.\n // getModelCtx() returns the correct num_ctx for each cloud/local model.\n // v4.10.0-local (cost cap): capped cloud num_predict to 8K\n // (was 32K). OpenRouter's paid models reject requests whose\n // max_tokens exceeds the remaining credit, even though most\n // responses don't come close to that. 8K is plenty for any\n // single turn and keeps us from getting HTTP 402s when\n // credit runs low.\n num_predict: options.maxTokens || (isCloudModel ? 8192 : 16384),\n num_ctx: getModelCtx(model),\n temperature: options.temperature ?? 0.7,\n },\n };\n\n // Model capabilities — adapts behavior per model family\n const caps = getModelCapabilities(model);\n\n // Thinking mode: explicitly control per model capabilities.\n // Models that don't benefit from thinking (thinkingWithTools=false) get it disabled\n // to prevent content being routed to the thinking field instead of content field.\n // This is critical for models like minimax-m2.7:cloud which put ALL output in\n // the thinking field when think is unset, leaving content empty.\n if (options.thinking === false) {\n body.think = false;\n } else if (!caps.thinkingWithTools) {\n // Model doesn't support thinking — disable it to prevent 400 errors\n // from Ollama (e.g. \"titan-qwen3.5:4b does not support thinking\").\n body.think = false;\n } else if (options.thinking === true) {\n body.think = true;\n }\n // Otherwise: omit body.think — let the model decide\n\n // Per-turn override: when the conversation contains tool-role messages,\n // force think=false regardless of caller intent. The GLM-family\n // tool-call parser on the server (vLLM #39611, confirmed by Z.ai docs\n // for GLM-5.1) silently drops tool results when enable_thinking=true,\n // breaking the multi-turn tool loop. Z.ai's own guidance: disable\n // thinking on tool-call turns. This keeps reasoning available for\n // planning turns while preventing the drop on execution turns.\n if (hasToolRoleMessages && body.think !== false) {\n const priorIntent = body.think === undefined ? 'unset' : String(body.think);\n body.think = false;\n logger.info(COMPONENT, `[ToolTurnThinkOverride] Forcing think=false for ${model} (tool-role message present, caller intent=${priorIntent})`);\n }\n\n if (options.tools && options.tools.length > 0) {\n body.tools = options.tools.map((t) => ({\n type: 'function',\n function: {\n name: t.function.name,\n description: t.function.description,\n parameters: simplifySchema(t.function.parameters),\n },\n }));\n // Per-model optimal sampling for tool calling\n (body.options as Record<string, unknown>).temperature = options.temperature ?? caps.toolTemperature ?? 0.5;\n if (caps.toolTopP) (body.options as Record<string, unknown>).top_p = caps.toolTopP;\n if (caps.toolTopK) (body.options as Record<string, unknown>).top_k = caps.toolTopK;\n\n // Force a tool call on the first round when the task requires it\n // Models that self-select tools well don't need forcing — it hurts them\n if (options.forceToolUse && !caps.selfSelectsTools) {\n body.tool_choice = 'required';\n logger.info(COMPONENT, `[ToolChoiceRequired] Setting tool_choice=required for ${model} (forceToolUse=true, selfSelectsTools=false)`);\n } else if (options.forceToolUse && caps.selfSelectsTools) {\n logger.info(COMPONENT, `[ToolChoiceSkipped] forceToolUse=true but selfSelectsTools=true for ${model} — NOT setting tool_choice`);\n }\n }\n\n // Ollama-native structured outputs — constrain generation to a JSON schema.\n // https://docs.ollama.com/capabilities/structured-outputs.md\n if (options.format !== undefined) {\n body.format = options.format;\n }\n\n // Cloud models: trim conversation history preserving tool call/response pairs.\n // With 131K context window, cloud models can handle much longer histories.\n // E1: Use >= 80 with margin (trim to 75) to prevent off-by-one at exact boundary.\n if (isCloudModel && hasTools) {\n const msgs = body.messages as Array<Record<string, unknown>>;\n if (msgs.length >= 80) {\n const trimmed = trimPreservingToolPairs(msgs, 80);\n logger.info(COMPONENT, `Cloud model context trim: ${msgs.length} → ${trimmed.length} messages`);\n body.messages = trimmed;\n }\n }\n\n // Some models ignore standalone system messages during tool calling.\n // Only merge when the model's capability profile says it needs it.\n if (hasTools && caps.needsSystemMerge) {\n const msgs = body.messages as Array<Record<string, unknown>>;\n const sysIdx = msgs.findIndex(m => m.role === 'system');\n const firstUserIdx = msgs.findIndex(m => m.role === 'user');\n if (sysIdx >= 0 && firstUserIdx >= 0 && msgs[sysIdx].content) {\n const sysContent = msgs[sysIdx].content as string;\n msgs[firstUserIdx].content = `[System Instructions]\\n${sysContent}\\n\\n[User Message]\\n${msgs[firstUserIdx].content}`;\n msgs.splice(sysIdx, 1); // Remove the standalone system message\n logger.info(COMPONENT, `Merged system prompt into first user message for cloud model compatibility`);\n }\n }\n\n const sentMessages = body.messages as Array<{role: string; content: string}>;\n const toolNames = body.tools ? (body.tools as Array<{function: {name: string}}>).map(t => t.function.name) : [];\n logger.info(COMPONENT, `Chat request: model=${model}, cloud=${isCloudModel}, tools=[${toolNames.join(',')}], think=${body.think}, messages=${sentMessages.length}`);\n \n if (process.env.DUMP_OLLAMA_BODY === '1' || model.includes('gemini')) {\n logger.error(COMPONENT, `[DUMP_BODY] Dumping failing request body for ${model} to /tmp/ollama-body-dump.json`);\n try {\n fs.writeFileSync('/tmp/ollama-body-dump.json', JSON.stringify(body, null, 2));\n } catch (e) {\n logger.error(COMPONENT, `Failed to dump body: ${e}`);\n }\n }\n\n // Cloud models routed through Ollama need longer timeouts (they proxy to remote APIs)\n const timeoutMs = isCloudModel ? 300_000 : 120_000; // 5min cloud, 2min local\n let response = await fetchWithRetry(`${this.baseUrl}/api/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n }, { timeoutMs });\n\n if (!response.ok) {\n const errorText = await response.text();\n // Fallback: if model doesn't support native tool calling or tokenization\n // fails with tools, retry without tools. Covers Gemini proxy errors like\n // \"does not support tools\" and \"tokenization\" failures on malformed schemas.\n if (response.status === 400 && body.tools && (\n errorText.includes('does not support tools') ||\n errorText.includes('tokenization') ||\n errorText.includes('tokenize') ||\n errorText.includes('Invalid JSON')\n )) {\n logger.warn(COMPONENT, `Model ${model} does not support native tool calling — running in chat-only mode`);\n delete body.tools;\n response = await fetchWithRetry(`${this.baseUrl}/api/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n }, { timeoutMs });\n if (!response.ok) {\n const retryText = await response.text();\n // Hunt Finding #37 (2026-04-14): use createProviderError to\n // attach status + parsed Retry-After so the router actually\n // respects the provider's backoff hint.\n const { createProviderError } = await import('./errorTaxonomy.js');\n throw createProviderError('Ollama', response, retryText, { provider: 'ollama', model });\n }\n } else {\n const { createProviderError } = await import('./errorTaxonomy.js');\n throw createProviderError('Ollama', response, errorText, { provider: 'ollama', model });\n }\n }\n\n // v4.13 ancestor-extraction (Hermes rate_limit_tracker): capture any\n // x-ratelimit-* headers the Ollama proxy exposes. Graceful no-op when\n // the headers aren't present. Provider name is 'ollama' so the router's\n // proactive-backoff logic can consult per-provider state.\n try {\n const { recordHeaders } = await import('./rateLimitTracker.js');\n recordHeaders('ollama', response.headers);\n } catch { /* never fail the chat on tracker issues */ }\n\n const data = await response.json() as Record<string, unknown>;\n const message = data.message as Record<string, unknown>;\n logger.info(COMPONENT, `Response from ${model}: tool_calls=${JSON.stringify(message.tool_calls)}, content_length=${((message.content as string) || '').length}`);\n const toolCalls: ToolCall[] = [];\n\n // v5.0.2: Only accept tool_calls from the model if tools were actually\n // sent in the request. Prevents hallucinated tool calls when the safety\n // system has stripped all tools (activeTools = []) or for models that\n // emit tool_calls even without tool definitions.\n if (message.tool_calls && options.tools && options.tools.length > 0) {\n for (const tc of message.tool_calls as Array<Record<string, unknown>>) {\n const fn = tc.function as Record<string, unknown>;\n // v4.13: capture Gemini thought_signature if present — needed\n // on the round-trip back or Gemini rejects the next request.\n const thoughtSig = (tc.thought_signature as string | undefined) ??\n (tc.thoughtSignature as string | undefined) ??\n (fn.thought_signature as string | undefined) ??\n (fn.thoughtSignature as string | undefined);\n toolCalls.push({\n id: uuid(),\n type: 'function',\n function: {\n name: fn.name as string,\n arguments: JSON.stringify(fn.arguments),\n },\n ...(thoughtSig ? { thoughtSignature: thoughtSig } : {}),\n });\n }\n }\n\n // A2: Hallucinated tool name detection at provider level (LangGraph pattern)\n if (options.tools && toolCalls.length > 0) {\n const validNames = new Set(options.tools.map(t => t.function.name));\n const invalid = toolCalls.filter(tc => !validNames.has(tc.function.name));\n if (invalid.length > 0) {\n logger.warn(COMPONENT, `[HallucinationGuard] Model hallucinated ${invalid.length} tool name(s): ${invalid.map(tc => tc.function.name).join(', ')}. Will be caught by toolRunner with corrective feedback.`);\n }\n }\n\n // If content is empty but thinking field has content, use it as a fallback.\n // This handles models that route output to thinking field when think is\n // unset or misconfigured. The router's stripThinkingFromResponse() will\n // clean up any reasoning that leaks through, so we can be permissive here.\n let content = (message.content as string) || '';\n if (!content && message.thinking) {\n const thinking = (message.thinking as string) || '';\n if (thinking.length > 0) {\n logger.info(COMPONENT, `[ThinkingFallback] Content empty, using thinking field (${thinking.length} chars)`);\n content = thinking;\n }\n }\n // Strip leaked thinking tags from Qwen/DeepSeek models\n content = content.replace(/^[\\s\\S]*?<\\/think>\\s*/m, '').trim();\n\n return {\n id: uuid(),\n content,\n toolCalls: toolCalls.length > 0 ? toolCalls : undefined,\n usage: {\n promptTokens: (data.prompt_eval_count as number) || 0,\n completionTokens: (data.eval_count as number) || 0,\n totalTokens: ((data.prompt_eval_count as number) || 0) + ((data.eval_count as number) || 0),\n },\n finishReason: toolCalls.length > 0 ? 'tool_calls' : 'stop',\n model: `ollama/${model}`,\n };\n }\n\n async *chatStream(options: ChatOptions): AsyncGenerator<ChatStreamChunk> {\n const model = (options.model || 'llama3.1').replace('ollama/', '');\n\n // Cloud models (Ollama Pro): detect via -cloud suffix or :cloud tag\n const isCloudModel = model.includes('-cloud') || model.includes(':cloud');\n const hasTools = options.tools && options.tools.length > 0;\n const hasToolRoleMessages = options.messages.some(m => m.role === 'tool');\n\n const body: Record<string, unknown> = {\n model,\n messages: options.messages.map((m) => {\n const msg: Record<string, unknown> = { role: m.role };\n // Compress system prompts for cloud models with tools — preserve active tool descriptions\n if (m.role === 'system' && isCloudModel && hasTools) {\n const activeToolDescs = (options.tools ?? [])\n .filter(t => (t.function.description?.length ?? 0) > 200)\n .map(t => ({ name: t.function.name, description: t.function.description ?? '' }));\n msg.content = compressSystemPrompt(m.content, activeToolDescs.length > 0 ? activeToolDescs : undefined);\n } else {\n msg.content = m.content;\n }\n if (m.toolCalls && m.toolCalls.length > 0) {\n msg.tool_calls = m.toolCalls.map(tc => ({\n id: tc.id,\n type: tc.type || 'function',\n function: { name: tc.function.name, arguments: JSON.parse(tc.function.arguments || '{}') }\n }));\n }\n if (m.toolCallId) msg.tool_call_id = m.toolCallId;\n // Cloud models (Gemini API) require function_response.name to be non-empty\n if (m.role === 'tool') {\n msg.name = m.name || 'tool';\n } else if (m.name) {\n msg.name = m.name;\n }\n return msg;\n }),\n stream: true,\n keep_alive: '30m',\n options: {\n // v4.10.0-local (cost cap): 8K cloud cap matches non-stream path\n num_predict: options.maxTokens || (isCloudModel ? 8192 : 16384),\n num_ctx: getModelCtx(model),\n temperature: options.temperature ?? 0.7,\n },\n };\n\n // Model capabilities — adapts behavior per model family\n const caps = getModelCapabilities(model);\n\n // Thinking mode: respect explicit setting, otherwise use model capabilities.\n // Disable for models that don't support thinking — prevents 400 errors.\n if (options.thinking === false) {\n body.think = false;\n } else if (!caps.thinkingWithTools) {\n body.think = false;\n } else if (options.thinking === true) {\n body.think = true;\n }\n\n // Per-turn override for tool-role turns (see chat() for rationale: vLLM #39611 / Z.ai docs).\n if (hasToolRoleMessages && body.think !== false) {\n const priorIntent = body.think === undefined ? 'unset' : String(body.think);\n body.think = false;\n logger.info(COMPONENT, `[ToolTurnThinkOverride] (stream) Forcing think=false for ${model} (tool-role message present, caller intent=${priorIntent})`);\n }\n\n if (hasTools) {\n body.tools = options.tools!.map((t) => ({\n type: 'function',\n function: {\n name: t.function.name,\n description: t.function.description,\n parameters: simplifySchema(t.function.parameters),\n },\n }));\n // Per-model optimal sampling for tool calling\n (body.options as Record<string, unknown>).temperature = options.temperature ?? caps.toolTemperature ?? 0.5;\n if (caps.toolTopP) (body.options as Record<string, unknown>).top_p = caps.toolTopP;\n if (caps.toolTopK) (body.options as Record<string, unknown>).top_k = caps.toolTopK;\n\n // Force tool_choice when requested — skip for models that self-select well\n if (options.forceToolUse && !caps.selfSelectsTools) {\n body.tool_choice = 'required';\n }\n }\n\n // Ollama-native structured outputs (stream variant).\n if (options.format !== undefined) {\n body.format = options.format;\n }\n\n // Optimize: trim history preserving tool pairs (cloud models only — local models have smaller contexts)\n if (isCloudModel && hasTools) {\n const msgs = body.messages as Array<Record<string, unknown>>;\n if (msgs.length > 80) {\n const trimmed = trimPreservingToolPairs(msgs, 80);\n logger.info(COMPONENT, `[Stream] Cloud model context trim: ${msgs.length} → ${trimmed.length} messages`);\n body.messages = trimmed;\n }\n }\n // Merge system into first user message only for models that need it\n if (hasTools && caps.needsSystemMerge) {\n const msgs2 = body.messages as Array<Record<string, unknown>>;\n const sysIdx = msgs2.findIndex(m => m.role === 'system');\n const firstUserIdx = msgs2.findIndex(m => m.role === 'user');\n if (sysIdx >= 0 && firstUserIdx >= 0 && msgs2[sysIdx].content) {\n const sysContent = msgs2[sysIdx].content as string;\n msgs2[firstUserIdx].content = `[System Instructions]\\n${sysContent}\\n\\n[User Message]\\n${msgs2[firstUserIdx].content}`;\n msgs2.splice(sysIdx, 1);\n }\n }\n\n try {\n // Cloud models need longer timeouts for streaming too\n const streamTimeoutMs = isCloudModel ? 300_000 : 120_000;\n let response = await fetch(`${this.baseUrl}/api/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(streamTimeoutMs),\n });\n\n if (!response.ok || !response.body) {\n const errorText = await response.text();\n // Fallback: if model doesn't support native tool calling, retry without tools\n if (response.status === 400 && errorText.includes('does not support tools') && body.tools) {\n logger.warn(COMPONENT, `Model ${model} does not support native tool calling — running in chat-only mode`);\n delete body.tools;\n response = await fetch(`${this.baseUrl}/api/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(streamTimeoutMs),\n });\n if (!response.ok || !response.body) {\n const retryText = await response.text();\n yield { type: 'error', error: `Ollama error (${response.status}): ${retryText}` };\n return;\n }\n } else {\n yield { type: 'error', error: `Ollama error (${response.status}): ${errorText}` };\n return;\n }\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let insideThink = false;\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (!line.trim()) continue;\n try {\n const chunk = JSON.parse(line);\n // Handle thinking field for models that put content there\n // Some models (e.g. qwen3.5, nemotron-super:cloud) use the thinking field\n // even when think=false is set — treat thinking as content in that case\n if (!chunk.message?.content && chunk.message?.thinking) {\n if (body.think === false) {\n chunk.message.content = chunk.message.thinking;\n }\n }\n if (chunk.message?.content) {\n let text = chunk.message.content;\n // Strip leaked <think>...</think> blocks from Qwen/DeepSeek\n if (text.includes('<think>')) insideThink = true;\n if (insideThink) {\n if (text.includes('</think>')) {\n text = text.split('</think>').pop()?.trim() || '';\n insideThink = false;\n } else {\n continue; // suppress thinking content\n }\n }\n if (text) yield { type: 'text', content: text };\n }\n // v5.0.2: Only yield tool_calls if tools were sent in the request\n if (chunk.message?.tool_calls && options.tools && options.tools.length > 0) {\n for (const tc of chunk.message.tool_calls) {\n const fn = tc.function as Record<string, unknown>;\n yield {\n type: 'tool_call',\n toolCall: { id: uuid(), type: 'function', function: { name: fn.name as string, arguments: JSON.stringify(fn.arguments) } },\n };\n }\n }\n if (chunk.done) break;\n } catch { /* skip malformed NDJSON lines */ }\n }\n }\n yield { type: 'done' };\n } catch (error) {\n yield { type: 'error', error: (error as Error).message };\n }\n }\n\n async listModels(): Promise<string[]> {\n try {\n const response = await fetch(`${this.baseUrl}/api/tags`);\n if (!response.ok) {\n // Hunt Finding #29 (2026-04-14): consume the body even on\n // error paths so the underlying socket can return to the\n // keep-alive pool. Without this, every non-200 response\n // leaks its socket until the GC gets around to it.\n await response.body?.cancel().catch(() => {});\n return [];\n }\n const data = await response.json() as { models?: Array<{ name: string }> };\n return (data.models || []).map((m) => m.name);\n } catch {\n return [];\n }\n }\n\n async healthCheck(): Promise<boolean> {\n try {\n const response = await fetch(`${this.baseUrl}/api/tags`);\n const ok = response.ok;\n // Hunt Finding #29 (2026-04-14): ALWAYS consume or cancel the\n // body. Previously we returned response.ok directly, leaving the\n // body stream dangling and the socket held open.\n await response.body?.cancel().catch(() => {});\n return ok;\n } catch {\n return false;\n }\n }\n}\n"],"mappings":"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","names":["modelName"]}
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync } from "fs";
3
+ const MUTATING_TOOLS = /* @__PURE__ */ new Set(["write_file", "edit_file", "append_file", "apply_patch"]);
4
+ function extractCommandLike(args) {
5
+ for (const key of ["command", "code", "script", "cmd", "shell"]) {
6
+ const val = args[key];
7
+ if (typeof val === "string") return val;
8
+ }
9
+ for (const val of Object.values(args)) {
10
+ if (typeof val === "string" && (val.includes("rm ") || val.includes("sudo") || val.includes("chmod"))) {
11
+ return val;
12
+ }
13
+ }
14
+ return null;
15
+ }
16
+ function extractFilePath(args) {
17
+ for (const key of ["path", "file_path", "filePath", "filename", "file"]) {
18
+ const val = args[key];
19
+ if (typeof val === "string") return val;
20
+ }
21
+ return void 0;
22
+ }
23
+ function extractContent(args) {
24
+ for (const key of ["content", "new_text", "data", "text", "patch", "body"]) {
25
+ const val = args[key];
26
+ if (typeof val === "string") return val;
27
+ }
28
+ return void 0;
29
+ }
30
+ function checkInvariants(toolName, args, _context) {
31
+ const commandLike = extractCommandLike(args);
32
+ if (commandLike) {
33
+ const rmPattern = /\brm\s+(?:-[a-zA-Z]*[rfRF][a-zA-Z]*\s+)*\/(?![a-zA-Z0-9_])/;
34
+ const rmEndPattern = /\brm\s+(?:-[a-zA-Z]*[rfRF][a-zA-Z]*\s+)*\/\s*$/;
35
+ if (rmPattern.test(commandLike) || rmEndPattern.test(commandLike)) {
36
+ return { pass: false, reason: "Blocked destructive command: rm -rf /" };
37
+ }
38
+ if (/\bsudo\b/.test(commandLike)) {
39
+ return { pass: false, reason: "Blocked privileged command: sudo" };
40
+ }
41
+ if (/\bchmod\s+777\b/.test(commandLike)) {
42
+ return { pass: false, reason: "Blocked overly permissive command: chmod 777" };
43
+ }
44
+ }
45
+ const filePath = extractFilePath(args);
46
+ if (filePath && MUTATING_TOOLS.has(toolName)) {
47
+ const content = extractContent(args);
48
+ if (content !== void 0 && content.trim() === "" && existsSync(filePath)) {
49
+ return { pass: false, reason: `Blocked empty write to existing file: ${filePath}` };
50
+ }
51
+ }
52
+ if (filePath && /\.env(\.|\b|$)/.test(filePath)) {
53
+ return { pass: false, reason: `Blocked access to sensitive file: ${filePath}` };
54
+ }
55
+ return { pass: true };
56
+ }
57
+ export {
58
+ checkInvariants
59
+ };
60
+ //# sourceMappingURL=invariants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/safety/invariants.ts"],"sourcesContent":["/**\n * TITAN — Swarm Invariants (Hard Safety Rules)\n * Fail-closed checks that run before every tool invocation.\n */\n\nimport { existsSync } from 'fs';\n\nexport interface InvariantResult {\n pass: boolean;\n reason?: string;\n}\n\nconst MUTATING_TOOLS = new Set(['write_file', 'edit_file', 'append_file', 'apply_patch']);\n\n/** Extract command-like strings from tool arguments */\nfunction extractCommandLike(args: Record<string, unknown>): string | null {\n for (const key of ['command', 'code', 'script', 'cmd', 'shell']) {\n const val = args[key];\n if (typeof val === 'string') return val;\n }\n // Fallback: scan any string value for dangerous command keywords\n for (const val of Object.values(args)) {\n if (typeof val === 'string' && (val.includes('rm ') || val.includes('sudo') || val.includes('chmod'))) {\n return val;\n }\n }\n return null;\n}\n\n/** Extract file path from tool arguments */\nfunction extractFilePath(args: Record<string, unknown>): string | undefined {\n for (const key of ['path', 'file_path', 'filePath', 'filename', 'file']) {\n const val = args[key];\n if (typeof val === 'string') return val;\n }\n return undefined;\n}\n\n/** Extract content body from tool arguments */\nfunction extractContent(args: Record<string, unknown>): string | undefined {\n for (const key of ['content', 'new_text', 'data', 'text', 'patch', 'body']) {\n const val = args[key];\n if (typeof val === 'string') return val;\n }\n return undefined;\n}\n\n/**\n * Check hard safety invariants for a tool call.\n * @param toolName - Name of the tool being invoked\n * @param args - Tool arguments\n * @param _context - Optional execution context (reserved for future use)\n * @returns InvariantResult — pass=true means execution may proceed\n */\nexport function checkInvariants(\n toolName: string,\n args: Record<string, unknown>,\n _context?: Record<string, unknown>,\n): InvariantResult {\n // Command-line invariants\n const commandLike = extractCommandLike(args);\n if (commandLike) {\n const rmPattern = /\\brm\\s+(?:-[a-zA-Z]*[rfRF][a-zA-Z]*\\s+)*\\/(?![a-zA-Z0-9_])/;\n const rmEndPattern = /\\brm\\s+(?:-[a-zA-Z]*[rfRF][a-zA-Z]*\\s+)*\\/\\s*$/;\n if (rmPattern.test(commandLike) || rmEndPattern.test(commandLike)) {\n return { pass: false, reason: 'Blocked destructive command: rm -rf /' };\n }\n\n if (/\\bsudo\\b/.test(commandLike)) {\n return { pass: false, reason: 'Blocked privileged command: sudo' };\n }\n\n if (/\\bchmod\\s+777\\b/.test(commandLike)) {\n return { pass: false, reason: 'Blocked overly permissive command: chmod 777' };\n }\n }\n\n // Filesystem invariants\n const filePath = extractFilePath(args);\n\n // Block empty writes to existing files\n if (filePath && MUTATING_TOOLS.has(toolName)) {\n const content = extractContent(args);\n if (content !== undefined && content.trim() === '' && existsSync(filePath)) {\n return { pass: false, reason: `Blocked empty write to existing file: ${filePath}` };\n }\n }\n\n // Block reading .env files\n if (filePath && /\\.env(\\.|\\b|$)/.test(filePath)) {\n return { pass: false, reason: `Blocked access to sensitive file: ${filePath}` };\n }\n\n return { pass: true };\n}\n"],"mappings":";AAKA,SAAS,kBAAkB;AAO3B,MAAM,iBAAiB,oBAAI,IAAI,CAAC,cAAc,aAAa,eAAe,aAAa,CAAC;AAGxF,SAAS,mBAAmB,MAA8C;AACtE,aAAW,OAAO,CAAC,WAAW,QAAQ,UAAU,OAAO,OAAO,GAAG;AAC7D,UAAM,MAAM,KAAK,GAAG;AACpB,QAAI,OAAO,QAAQ,SAAU,QAAO;AAAA,EACxC;AAEA,aAAW,OAAO,OAAO,OAAO,IAAI,GAAG;AACnC,QAAI,OAAO,QAAQ,aAAa,IAAI,SAAS,KAAK,KAAK,IAAI,SAAS,MAAM,KAAK,IAAI,SAAS,OAAO,IAAI;AACnG,aAAO;AAAA,IACX;AAAA,EACJ;AACA,SAAO;AACX;AAGA,SAAS,gBAAgB,MAAmD;AACxE,aAAW,OAAO,CAAC,QAAQ,aAAa,YAAY,YAAY,MAAM,GAAG;AACrE,UAAM,MAAM,KAAK,GAAG;AACpB,QAAI,OAAO,QAAQ,SAAU,QAAO;AAAA,EACxC;AACA,SAAO;AACX;AAGA,SAAS,eAAe,MAAmD;AACvE,aAAW,OAAO,CAAC,WAAW,YAAY,QAAQ,QAAQ,SAAS,MAAM,GAAG;AACxE,UAAM,MAAM,KAAK,GAAG;AACpB,QAAI,OAAO,QAAQ,SAAU,QAAO;AAAA,EACxC;AACA,SAAO;AACX;AASO,SAAS,gBACZ,UACA,MACA,UACe;AAEf,QAAM,cAAc,mBAAmB,IAAI;AAC3C,MAAI,aAAa;AACb,UAAM,YAAY;AAClB,UAAM,eAAe;AACrB,QAAI,UAAU,KAAK,WAAW,KAAK,aAAa,KAAK,WAAW,GAAG;AAC/D,aAAO,EAAE,MAAM,OAAO,QAAQ,wCAAwC;AAAA,IAC1E;AAEA,QAAI,WAAW,KAAK,WAAW,GAAG;AAC9B,aAAO,EAAE,MAAM,OAAO,QAAQ,mCAAmC;AAAA,IACrE;AAEA,QAAI,kBAAkB,KAAK,WAAW,GAAG;AACrC,aAAO,EAAE,MAAM,OAAO,QAAQ,+CAA+C;AAAA,IACjF;AAAA,EACJ;AAGA,QAAM,WAAW,gBAAgB,IAAI;AAGrC,MAAI,YAAY,eAAe,IAAI,QAAQ,GAAG;AAC1C,UAAM,UAAU,eAAe,IAAI;AACnC,QAAI,YAAY,UAAa,QAAQ,KAAK,MAAM,MAAM,WAAW,QAAQ,GAAG;AACxE,aAAO,EAAE,MAAM,OAAO,QAAQ,yCAAyC,QAAQ,GAAG;AAAA,IACtF;AAAA,EACJ;AAGA,MAAI,YAAY,iBAAiB,KAAK,QAAQ,GAAG;AAC7C,WAAO,EAAE,MAAM,OAAO,QAAQ,qCAAqC,QAAQ,GAAG;AAAA,EAClF;AAEA,SAAO,EAAE,MAAM,KAAK;AACxB;","names":[]}
@@ -219,7 +219,7 @@ async function reviewStagedBundle(input) {
219
219
  };
220
220
  }
221
221
  }
222
- let budget = rolloverIfNeeded(loadBudget());
222
+ const budget = rolloverIfNeeded(loadBudget());
223
223
  if (budget.daily.costUsd >= cfg.maxDailyUsd) {
224
224
  logger.warn(COMPONENT, `Daily cap $${cfg.maxDailyUsd.toFixed(2)} hit ($${budget.daily.costUsd.toFixed(2)} spent today) \u2014 skipping review`);
225
225
  return {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/safety/opusReview.ts"],"sourcesContent":["/**\n * TITAN — Opus Review Gate (v4.10.0-local polish)\n *\n * Before a staged self_mod_pr applies to /opt/TITAN, send the diff to\n * Claude Opus (via OpenRouter) for a quality review. Opus looks for:\n * - Correctness (does it do what the goal asked?)\n * - Integration (are new modules wired into existing systems?)\n * - Regressions (does it break anything obvious?)\n * - Security (sanity check beyond the scanner)\n * - Code quality (obvious bugs, missing error handling, etc.)\n *\n * Returns a structured verdict. Used in applyStagedPR before the copy\n * to live. If reject/needs_changes, the approval transitions to a new\n * blocked state with Opus's concerns in the payload.\n *\n * Why Opus specifically: the local LLMs (glm-5.1, qwen3.6) that write\n * the code are weaker reviewers. Opus is a much stronger critic and\n * has seen a huge code corpus — catches integration gaps, typos, bad\n * patterns the local model missed.\n *\n * Config-gated: autonomy.selfMod.reviewer.{enabled, model, maxDiffChars}.\n * If OpenRouter key isn't configured, review is skipped (fall through).\n */\nimport { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'fs';\nimport { join, dirname } from 'path';\nimport logger from '../utils/logger.js';\nimport { loadConfig } from '../config/config.js';\nimport { TITAN_HOME } from '../utils/constants.js';\n\nconst COMPONENT = 'OpusReview';\nconst BUDGET_PATH = join(TITAN_HOME, 'reviewer-budget.json');\n\n// ── Model price table (USD per 1M tokens) ────────────────────────\n\n// Kept small + conservative. Free models = 0. Unknown models = Opus-rate\n// (safer to over-estimate than under). Update as model list changes.\nconst MODEL_PRICES: Record<string, { input: number; output: number }> = {\n // OpenRouter — metered\n};\n\nfunction priceFor(model: string): { input: number; output: number } {\n return MODEL_PRICES[model] ?? { input: 15.0, output: 75.0 }; // Opus-rate fallback\n}\n\n// ── Budget tracker ───────────────────────────────────────────────\n\ninterface ReviewerBudget {\n daily: { date: string; costUsd: number };\n monthly: { yearMonth: string; costUsd: number };\n totalSpentUsd: number;\n totalReviews: number;\n updatedAt: string;\n}\n\nfunction loadBudget(): ReviewerBudget {\n if (existsSync(BUDGET_PATH)) {\n try {\n return JSON.parse(readFileSync(BUDGET_PATH, 'utf-8')) as ReviewerBudget;\n } catch { /* fall through to fresh */ }\n }\n const now = new Date();\n return {\n daily: { date: now.toISOString().slice(0, 10), costUsd: 0 },\n monthly: { yearMonth: now.toISOString().slice(0, 7), costUsd: 0 },\n totalSpentUsd: 0,\n totalReviews: 0,\n updatedAt: now.toISOString(),\n };\n}\n\nfunction saveBudget(b: ReviewerBudget): void {\n try {\n mkdirSync(dirname(BUDGET_PATH), { recursive: true });\n writeFileSync(BUDGET_PATH + '.tmp', JSON.stringify(b, null, 2));\n renameSync(BUDGET_PATH + '.tmp', BUDGET_PATH);\n } catch (err) {\n logger.warn(COMPONENT, `Budget persist failed: ${(err as Error).message}`);\n }\n}\n\nfunction rolloverIfNeeded(b: ReviewerBudget): ReviewerBudget {\n const now = new Date();\n const today = now.toISOString().slice(0, 10);\n const thisMonth = now.toISOString().slice(0, 7);\n if (b.daily.date !== today) b.daily = { date: today, costUsd: 0 };\n if (b.monthly.yearMonth !== thisMonth) b.monthly = { yearMonth: thisMonth, costUsd: 0 };\n return b;\n}\n\n/** Snapshot of current spend — exposed via /api/safety/reviewer-budget. */\nexport function getReviewerBudget(): ReviewerBudget & { caps: { perReview: number; daily: number; monthly: number } } {\n const b = rolloverIfNeeded(loadBudget());\n const cfg = resolveReviewerConfig();\n return {\n ...b,\n caps: {\n perReview: cfg.maxPerReviewUsd,\n daily: cfg.maxDailyUsd,\n monthly: cfg.maxMonthlyUsd,\n },\n };\n}\n\n// ── Types ────────────────────────────────────────────────────────\n\nexport type OpusVerdict = 'approve' | 'reject' | 'needs_changes' | 'skipped';\n\nexport interface OpusReview {\n verdict: OpusVerdict;\n confidence: number;\n reasoning: string;\n concerns: string[];\n suggestions: string[];\n /** The review model used (for audit). */\n model: string;\n /** Duration of the review call. */\n durationMs: number;\n /** Raw response (first 10KB, for debugging). */\n rawResponse?: string;\n}\n\nexport interface ReviewInput {\n goalId: string;\n goalTitle: string;\n goalDescription?: string;\n files: Array<{ targetPath: string; stagedPath: string; sizeBytes: number }>;\n tags?: string[];\n}\n\n// ── Config ───────────────────────────────────────────────────────\n\ninterface ReviewerConfig {\n enabled: boolean;\n model: string;\n maxDiffChars: number;\n blockOnReject: boolean;\n maxPerReviewUsd: number;\n maxDailyUsd: number;\n maxMonthlyUsd: number;\n}\n\nfunction resolveReviewerConfig(): ReviewerConfig {\n const cfg = loadConfig();\n const sm = (cfg.autonomy as unknown as { selfMod?: { reviewer?: Partial<ReviewerConfig> } }).selfMod;\n const r = sm?.reviewer ?? {};\n return {\n enabled: r.enabled ?? true,\n model: r.model ?? 'ollama/glm-5.1:cloud',\n maxDiffChars: r.maxDiffChars ?? 50_000,\n blockOnReject: r.blockOnReject ?? true,\n maxPerReviewUsd: r.maxPerReviewUsd ?? 0.25,\n maxDailyUsd: r.maxDailyUsd ?? 1.50,\n maxMonthlyUsd: r.maxMonthlyUsd ?? 5.00,\n };\n}\n\n// ── Build the review prompt ──────────────────────────────────────\n\nfunction buildReviewPrompt(input: ReviewInput): string {\n const sections: string[] = [];\n\n sections.push('You are a senior code reviewer for TITAN, an autonomous AI agent framework written in TypeScript/ESM.');\n sections.push('Another TITAN specialist (a smaller local LLM) just produced the following staged changes for a self-modification goal.');\n sections.push('Your job is ONE FINAL CHECK before these changes apply to /opt/TITAN.');\n sections.push('');\n sections.push('## Goal');\n sections.push(`Title: ${input.goalTitle}`);\n if (input.goalDescription) {\n sections.push(`Description: ${input.goalDescription.slice(0, 1500)}`);\n }\n if (input.tags && input.tags.length > 0) {\n sections.push(`Tags: ${input.tags.join(', ')}`);\n }\n sections.push('');\n sections.push('## Files in this bundle');\n for (const f of input.files) {\n sections.push(`- ${f.targetPath} (${f.sizeBytes} bytes)`);\n }\n sections.push('');\n\n // Inline each file's content (capped)\n const cap = resolveReviewerConfig().maxDiffChars;\n let remaining = cap;\n for (const f of input.files) {\n if (remaining <= 0) {\n sections.push(`[Truncated: remaining files not shown due to ${cap} char cap]`);\n break;\n }\n try {\n const content = readFileSync(f.stagedPath, 'utf-8');\n const slice = content.slice(0, Math.min(content.length, remaining));\n remaining -= slice.length;\n sections.push('');\n sections.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');\n sections.push(`FILE: ${f.targetPath}`);\n sections.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');\n sections.push(slice);\n if (content.length > slice.length) {\n sections.push(`[Truncated — ${content.length - slice.length} more chars]`);\n }\n } catch (err) {\n sections.push(`[Could not read ${f.stagedPath}: ${(err as Error).message}]`);\n }\n }\n\n sections.push('');\n sections.push('## What to check');\n sections.push('1. **Correctness**: does the code actually do what the goal asks?');\n sections.push('2. **Integration**: if new modules are added, are they imported/used somewhere? (TITAN\\'s rule: a file that isn\\'t imported anywhere is dead code.)');\n sections.push('3. **Regressions**: does it change existing behavior in a way that could break things?');\n sections.push('4. **Obvious bugs**: null dereferences, unhandled promise rejections, missing try/catch on I/O, wrong types, logic errors.');\n sections.push('5. **Security**: beyond secret-scanning, any paths that could be exploited? Arbitrary file write? Injection?');\n sections.push('6. **Code quality**: idiomatic TypeScript? Follows TITAN\\'s style (ESM, Zod schemas, explicit types)?');\n sections.push('');\n sections.push('## Your verdict options');\n sections.push('- `approve` — apply the bundle as-is (safe, correct, integrated)');\n sections.push('- `needs_changes` — close to good but has specific fixable issues; list them in `concerns`');\n sections.push('- `reject` — fundamentally flawed; do not apply (missing integration, security issue, wrong approach)');\n sections.push('');\n sections.push('## Output format');\n sections.push('Respond with ONLY a JSON block, no prose before or after:');\n sections.push('```json');\n sections.push('{');\n sections.push(' \"verdict\": \"approve\" | \"needs_changes\" | \"reject\",');\n sections.push(' \"confidence\": 0.0 to 1.0,');\n sections.push(' \"reasoning\": \"1-3 sentences explaining the verdict\",');\n sections.push(' \"concerns\": [\"specific issue 1\", \"specific issue 2\"],');\n sections.push(' \"suggestions\": [\"how to fix / improve 1\", \"...\"]');\n sections.push('}');\n sections.push('```');\n\n return sections.join('\\n');\n}\n\n// ── Parser ───────────────────────────────────────────────────────\n\nfunction parseReview(raw: string): Omit<OpusReview, 'model' | 'durationMs' | 'rawResponse'> {\n // Extract JSON block\n const fence = raw.match(/```(?:json)?\\s*([\\s\\S]+?)\\s*```/);\n let jsonText = fence ? fence[1] : '';\n if (!jsonText) {\n // Fallback: last {...} in text\n const last = raw.lastIndexOf('}');\n if (last > 0) {\n let depth = 0;\n for (let i = last; i >= 0; i--) {\n if (raw[i] === '}') depth++;\n else if (raw[i] === '{') { depth--; if (depth === 0) { jsonText = raw.slice(i, last + 1); break; } }\n }\n }\n }\n if (!jsonText) {\n return {\n verdict: 'needs_changes',\n confidence: 0.3,\n reasoning: 'Reviewer did not return structured JSON; treating as needs_changes for safety.',\n concerns: ['No JSON verdict in response'],\n suggestions: [],\n };\n }\n try {\n const p = JSON.parse(jsonText) as Record<string, unknown>;\n const rawVerdict = String(p.verdict ?? 'needs_changes');\n const verdict: OpusVerdict = ['approve', 'reject', 'needs_changes'].includes(rawVerdict)\n ? (rawVerdict as OpusVerdict) : 'needs_changes';\n return {\n verdict,\n confidence: Math.max(0, Math.min(1, typeof p.confidence === 'number' ? p.confidence : 0.5)),\n reasoning: String(p.reasoning ?? ''),\n concerns: Array.isArray(p.concerns) ? p.concerns.map(String) : [],\n suggestions: Array.isArray(p.suggestions) ? p.suggestions.map(String) : [],\n };\n } catch (err) {\n return {\n verdict: 'needs_changes',\n confidence: 0.2,\n reasoning: `JSON parse failed: ${(err as Error).message}`,\n concerns: ['malformed reviewer response'],\n suggestions: [],\n };\n }\n}\n\n// ── Main entry ───────────────────────────────────────────────────\n\n/**\n * Run the Opus review. Returns a verdict + concerns. Never throws —\n * on error, returns `{verdict: 'skipped'}` so the caller can decide\n * whether to apply anyway (fail-open).\n */\nexport async function reviewStagedBundle(input: ReviewInput): Promise<OpusReview> {\n const cfg = resolveReviewerConfig();\n const startedAt = Date.now();\n\n if (!cfg.enabled) {\n return {\n verdict: 'skipped', confidence: 0,\n reasoning: 'Opus review disabled in config',\n concerns: [], suggestions: [],\n model: cfg.model, durationMs: 0,\n };\n }\n\n // Provider-key sanity check.\n // openrouter/* → needs OPENROUTER_API_KEY\n // anything else → assume the router's provider will handle its own auth\n if (cfg.model.startsWith('openrouter/')) {\n const openrouterKey = process.env.OPENROUTER_API_KEY ||\n (loadConfig().providers as Record<string, { apiKey?: string } | undefined>).openrouter?.apiKey;\n if (!openrouterKey) {\n logger.warn(COMPONENT, 'OpenRouter key missing — skipping review');\n return {\n verdict: 'skipped', confidence: 0,\n reasoning: 'No OpenRouter API key configured; review skipped',\n concerns: [], suggestions: [],\n model: cfg.model, durationMs: 0,\n };\n }\n }\n\n // v4.10.0-local polish: enforce cost caps. Tony's OpenRouter balance\n // is $9.54 — we don't want a runaway Opus reviewer to spam-bill. Caps:\n // per-review: $0.25 default\n // daily: $1.50 default\n // monthly: $5.00 default\n // Free models (Qwen3.6 Plus) cost $0, so caps never trigger for them.\n let budget = rolloverIfNeeded(loadBudget());\n if (budget.daily.costUsd >= cfg.maxDailyUsd) {\n logger.warn(COMPONENT, `Daily cap $${cfg.maxDailyUsd.toFixed(2)} hit ($${budget.daily.costUsd.toFixed(2)} spent today) — skipping review`);\n return {\n verdict: 'skipped', confidence: 0,\n reasoning: `Reviewer daily cost cap ($${cfg.maxDailyUsd.toFixed(2)}) reached — review skipped. Bumps over for tomorrow.`,\n concerns: [], suggestions: [],\n model: cfg.model, durationMs: 0,\n };\n }\n if (budget.monthly.costUsd >= cfg.maxMonthlyUsd) {\n logger.warn(COMPONENT, `Monthly cap $${cfg.maxMonthlyUsd.toFixed(2)} hit — skipping review`);\n return {\n verdict: 'skipped', confidence: 0,\n reasoning: `Reviewer monthly cost cap ($${cfg.maxMonthlyUsd.toFixed(2)}) reached — review skipped.`,\n concerns: [], suggestions: [],\n model: cfg.model, durationMs: 0,\n };\n }\n\n // Estimate per-review cost BEFORE calling. Cap: 0.25 USD default.\n const price = priceFor(cfg.model);\n const promptChars = buildReviewPrompt(input).length;\n const estInputTokens = Math.ceil(promptChars / 4); // ~4 chars per token\n const estOutputTokens = 2000; // we cap maxTokens at 2000\n const estCost = (estInputTokens / 1_000_000) * price.input + (estOutputTokens / 1_000_000) * price.output;\n if (estCost > cfg.maxPerReviewUsd) {\n logger.warn(COMPONENT, `Estimated review cost $${estCost.toFixed(3)} > per-review cap $${cfg.maxPerReviewUsd.toFixed(2)} — skipping`);\n return {\n verdict: 'skipped', confidence: 0,\n reasoning: `Review would cost ~$${estCost.toFixed(3)} which exceeds per-review cap $${cfg.maxPerReviewUsd.toFixed(2)}. Trim the bundle (fewer/smaller files) or raise the cap.`,\n concerns: [], suggestions: [],\n model: cfg.model, durationMs: 0,\n };\n }\n\n const prompt = buildReviewPrompt(input);\n try {\n const { chat } = await import('../providers/router.js');\n logger.info(COMPONENT, `Reviewing bundle for goal ${input.goalId} (${input.files.length} files) via ${cfg.model} — est $${estCost.toFixed(3)}`);\n const response = await chat({\n model: cfg.model,\n messages: [\n { role: 'system', content: 'You are a rigorous code reviewer. Return ONLY a JSON verdict block.' },\n { role: 'user', content: prompt },\n ],\n temperature: 0.2,\n maxTokens: 2000,\n });\n const raw = response?.content || '';\n const parsed = parseReview(raw);\n const durationMs = Date.now() - startedAt;\n\n // Record actual spend from usage if available, else use estimate.\n const usage = response?.usage;\n const actualCost = usage\n ? (usage.promptTokens / 1_000_000) * price.input + (usage.completionTokens / 1_000_000) * price.output\n : estCost;\n budget.daily.costUsd += actualCost;\n budget.monthly.costUsd += actualCost;\n budget.totalSpentUsd += actualCost;\n budget.totalReviews += 1;\n budget.updatedAt = new Date().toISOString();\n saveBudget(budget);\n\n logger.info(COMPONENT, `Review verdict: ${parsed.verdict} (confidence ${parsed.confidence.toFixed(2)}, ${durationMs}ms, actual cost $${actualCost.toFixed(4)}, daily $${budget.daily.costUsd.toFixed(3)}/${cfg.maxDailyUsd.toFixed(2)})`);\n return {\n ...parsed,\n model: cfg.model,\n durationMs,\n rawResponse: raw.slice(0, 10_000),\n };\n } catch (err) {\n const durationMs = Date.now() - startedAt;\n logger.warn(COMPONENT, `Review call failed: ${(err as Error).message}`);\n return {\n verdict: 'skipped', confidence: 0,\n reasoning: `Review call error: ${(err as Error).message}`,\n concerns: [], suggestions: [],\n model: cfg.model, durationMs,\n };\n }\n}\n\n/**\n * Is the reviewer configured to block on reject? (Used by applyStagedPR\n * to decide whether a `reject` verdict actually stops the apply.)\n */\nexport function reviewerBlocksOnReject(): boolean {\n return resolveReviewerConfig().blockOnReject;\n}\n\n// Silence unused import (keep for future use)\nvoid join; void dirname; void existsSync;\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/safety/opusReview.ts"],"sourcesContent":["/**\n * TITAN — Opus Review Gate (v4.10.0-local polish)\n *\n * Before a staged self_mod_pr applies to /opt/TITAN, send the diff to\n * Claude Opus (via OpenRouter) for a quality review. Opus looks for:\n * - Correctness (does it do what the goal asked?)\n * - Integration (are new modules wired into existing systems?)\n * - Regressions (does it break anything obvious?)\n * - Security (sanity check beyond the scanner)\n * - Code quality (obvious bugs, missing error handling, etc.)\n *\n * Returns a structured verdict. Used in applyStagedPR before the copy\n * to live. If reject/needs_changes, the approval transitions to a new\n * blocked state with Opus's concerns in the payload.\n *\n * Why Opus specifically: the local LLMs (glm-5.1, qwen3.6) that write\n * the code are weaker reviewers. Opus is a much stronger critic and\n * has seen a huge code corpus — catches integration gaps, typos, bad\n * patterns the local model missed.\n *\n * Config-gated: autonomy.selfMod.reviewer.{enabled, model, maxDiffChars}.\n * If OpenRouter key isn't configured, review is skipped (fall through).\n */\nimport { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'fs';\nimport { join, dirname } from 'path';\nimport logger from '../utils/logger.js';\nimport { loadConfig } from '../config/config.js';\nimport { TITAN_HOME } from '../utils/constants.js';\n\nconst COMPONENT = 'OpusReview';\nconst BUDGET_PATH = join(TITAN_HOME, 'reviewer-budget.json');\n\n// ── Model price table (USD per 1M tokens) ────────────────────────\n\n// Kept small + conservative. Free models = 0. Unknown models = Opus-rate\n// (safer to over-estimate than under). Update as model list changes.\nconst MODEL_PRICES: Record<string, { input: number; output: number }> = {\n // OpenRouter — metered\n};\n\nfunction priceFor(model: string): { input: number; output: number } {\n return MODEL_PRICES[model] ?? { input: 15.0, output: 75.0 }; // Opus-rate fallback\n}\n\n// ── Budget tracker ───────────────────────────────────────────────\n\ninterface ReviewerBudget {\n daily: { date: string; costUsd: number };\n monthly: { yearMonth: string; costUsd: number };\n totalSpentUsd: number;\n totalReviews: number;\n updatedAt: string;\n}\n\nfunction loadBudget(): ReviewerBudget {\n if (existsSync(BUDGET_PATH)) {\n try {\n return JSON.parse(readFileSync(BUDGET_PATH, 'utf-8')) as ReviewerBudget;\n } catch { /* fall through to fresh */ }\n }\n const now = new Date();\n return {\n daily: { date: now.toISOString().slice(0, 10), costUsd: 0 },\n monthly: { yearMonth: now.toISOString().slice(0, 7), costUsd: 0 },\n totalSpentUsd: 0,\n totalReviews: 0,\n updatedAt: now.toISOString(),\n };\n}\n\nfunction saveBudget(b: ReviewerBudget): void {\n try {\n mkdirSync(dirname(BUDGET_PATH), { recursive: true });\n writeFileSync(BUDGET_PATH + '.tmp', JSON.stringify(b, null, 2));\n renameSync(BUDGET_PATH + '.tmp', BUDGET_PATH);\n } catch (err) {\n logger.warn(COMPONENT, `Budget persist failed: ${(err as Error).message}`);\n }\n}\n\nfunction rolloverIfNeeded(b: ReviewerBudget): ReviewerBudget {\n const now = new Date();\n const today = now.toISOString().slice(0, 10);\n const thisMonth = now.toISOString().slice(0, 7);\n if (b.daily.date !== today) b.daily = { date: today, costUsd: 0 };\n if (b.monthly.yearMonth !== thisMonth) b.monthly = { yearMonth: thisMonth, costUsd: 0 };\n return b;\n}\n\n/** Snapshot of current spend — exposed via /api/safety/reviewer-budget. */\nexport function getReviewerBudget(): ReviewerBudget & { caps: { perReview: number; daily: number; monthly: number } } {\n const b = rolloverIfNeeded(loadBudget());\n const cfg = resolveReviewerConfig();\n return {\n ...b,\n caps: {\n perReview: cfg.maxPerReviewUsd,\n daily: cfg.maxDailyUsd,\n monthly: cfg.maxMonthlyUsd,\n },\n };\n}\n\n// ── Types ────────────────────────────────────────────────────────\n\nexport type OpusVerdict = 'approve' | 'reject' | 'needs_changes' | 'skipped';\n\nexport interface OpusReview {\n verdict: OpusVerdict;\n confidence: number;\n reasoning: string;\n concerns: string[];\n suggestions: string[];\n /** The review model used (for audit). */\n model: string;\n /** Duration of the review call. */\n durationMs: number;\n /** Raw response (first 10KB, for debugging). */\n rawResponse?: string;\n}\n\nexport interface ReviewInput {\n goalId: string;\n goalTitle: string;\n goalDescription?: string;\n files: Array<{ targetPath: string; stagedPath: string; sizeBytes: number }>;\n tags?: string[];\n}\n\n// ── Config ───────────────────────────────────────────────────────\n\ninterface ReviewerConfig {\n enabled: boolean;\n model: string;\n maxDiffChars: number;\n blockOnReject: boolean;\n maxPerReviewUsd: number;\n maxDailyUsd: number;\n maxMonthlyUsd: number;\n}\n\nfunction resolveReviewerConfig(): ReviewerConfig {\n const cfg = loadConfig();\n const sm = (cfg.autonomy as unknown as { selfMod?: { reviewer?: Partial<ReviewerConfig> } }).selfMod;\n const r = sm?.reviewer ?? {};\n return {\n enabled: r.enabled ?? true,\n model: r.model ?? 'ollama/glm-5.1:cloud',\n maxDiffChars: r.maxDiffChars ?? 50_000,\n blockOnReject: r.blockOnReject ?? true,\n maxPerReviewUsd: r.maxPerReviewUsd ?? 0.25,\n maxDailyUsd: r.maxDailyUsd ?? 1.50,\n maxMonthlyUsd: r.maxMonthlyUsd ?? 5.00,\n };\n}\n\n// ── Build the review prompt ──────────────────────────────────────\n\nfunction buildReviewPrompt(input: ReviewInput): string {\n const sections: string[] = [];\n\n sections.push('You are a senior code reviewer for TITAN, an autonomous AI agent framework written in TypeScript/ESM.');\n sections.push('Another TITAN specialist (a smaller local LLM) just produced the following staged changes for a self-modification goal.');\n sections.push('Your job is ONE FINAL CHECK before these changes apply to /opt/TITAN.');\n sections.push('');\n sections.push('## Goal');\n sections.push(`Title: ${input.goalTitle}`);\n if (input.goalDescription) {\n sections.push(`Description: ${input.goalDescription.slice(0, 1500)}`);\n }\n if (input.tags && input.tags.length > 0) {\n sections.push(`Tags: ${input.tags.join(', ')}`);\n }\n sections.push('');\n sections.push('## Files in this bundle');\n for (const f of input.files) {\n sections.push(`- ${f.targetPath} (${f.sizeBytes} bytes)`);\n }\n sections.push('');\n\n // Inline each file's content (capped)\n const cap = resolveReviewerConfig().maxDiffChars;\n let remaining = cap;\n for (const f of input.files) {\n if (remaining <= 0) {\n sections.push(`[Truncated: remaining files not shown due to ${cap} char cap]`);\n break;\n }\n try {\n const content = readFileSync(f.stagedPath, 'utf-8');\n const slice = content.slice(0, Math.min(content.length, remaining));\n remaining -= slice.length;\n sections.push('');\n sections.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');\n sections.push(`FILE: ${f.targetPath}`);\n sections.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');\n sections.push(slice);\n if (content.length > slice.length) {\n sections.push(`[Truncated — ${content.length - slice.length} more chars]`);\n }\n } catch (err) {\n sections.push(`[Could not read ${f.stagedPath}: ${(err as Error).message}]`);\n }\n }\n\n sections.push('');\n sections.push('## What to check');\n sections.push('1. **Correctness**: does the code actually do what the goal asks?');\n sections.push('2. **Integration**: if new modules are added, are they imported/used somewhere? (TITAN\\'s rule: a file that isn\\'t imported anywhere is dead code.)');\n sections.push('3. **Regressions**: does it change existing behavior in a way that could break things?');\n sections.push('4. **Obvious bugs**: null dereferences, unhandled promise rejections, missing try/catch on I/O, wrong types, logic errors.');\n sections.push('5. **Security**: beyond secret-scanning, any paths that could be exploited? Arbitrary file write? Injection?');\n sections.push('6. **Code quality**: idiomatic TypeScript? Follows TITAN\\'s style (ESM, Zod schemas, explicit types)?');\n sections.push('');\n sections.push('## Your verdict options');\n sections.push('- `approve` — apply the bundle as-is (safe, correct, integrated)');\n sections.push('- `needs_changes` — close to good but has specific fixable issues; list them in `concerns`');\n sections.push('- `reject` — fundamentally flawed; do not apply (missing integration, security issue, wrong approach)');\n sections.push('');\n sections.push('## Output format');\n sections.push('Respond with ONLY a JSON block, no prose before or after:');\n sections.push('```json');\n sections.push('{');\n sections.push(' \"verdict\": \"approve\" | \"needs_changes\" | \"reject\",');\n sections.push(' \"confidence\": 0.0 to 1.0,');\n sections.push(' \"reasoning\": \"1-3 sentences explaining the verdict\",');\n sections.push(' \"concerns\": [\"specific issue 1\", \"specific issue 2\"],');\n sections.push(' \"suggestions\": [\"how to fix / improve 1\", \"...\"]');\n sections.push('}');\n sections.push('```');\n\n return sections.join('\\n');\n}\n\n// ── Parser ───────────────────────────────────────────────────────\n\nfunction parseReview(raw: string): Omit<OpusReview, 'model' | 'durationMs' | 'rawResponse'> {\n // Extract JSON block\n const fence = raw.match(/```(?:json)?\\s*([\\s\\S]+?)\\s*```/);\n let jsonText = fence ? fence[1] : '';\n if (!jsonText) {\n // Fallback: last {...} in text\n const last = raw.lastIndexOf('}');\n if (last > 0) {\n let depth = 0;\n for (let i = last; i >= 0; i--) {\n if (raw[i] === '}') depth++;\n else if (raw[i] === '{') { depth--; if (depth === 0) { jsonText = raw.slice(i, last + 1); break; } }\n }\n }\n }\n if (!jsonText) {\n return {\n verdict: 'needs_changes',\n confidence: 0.3,\n reasoning: 'Reviewer did not return structured JSON; treating as needs_changes for safety.',\n concerns: ['No JSON verdict in response'],\n suggestions: [],\n };\n }\n try {\n const p = JSON.parse(jsonText) as Record<string, unknown>;\n const rawVerdict = String(p.verdict ?? 'needs_changes');\n const verdict: OpusVerdict = ['approve', 'reject', 'needs_changes'].includes(rawVerdict)\n ? (rawVerdict as OpusVerdict) : 'needs_changes';\n return {\n verdict,\n confidence: Math.max(0, Math.min(1, typeof p.confidence === 'number' ? p.confidence : 0.5)),\n reasoning: String(p.reasoning ?? ''),\n concerns: Array.isArray(p.concerns) ? p.concerns.map(String) : [],\n suggestions: Array.isArray(p.suggestions) ? p.suggestions.map(String) : [],\n };\n } catch (err) {\n return {\n verdict: 'needs_changes',\n confidence: 0.2,\n reasoning: `JSON parse failed: ${(err as Error).message}`,\n concerns: ['malformed reviewer response'],\n suggestions: [],\n };\n }\n}\n\n// ── Main entry ───────────────────────────────────────────────────\n\n/**\n * Run the Opus review. Returns a verdict + concerns. Never throws —\n * on error, returns `{verdict: 'skipped'}` so the caller can decide\n * whether to apply anyway (fail-open).\n */\nexport async function reviewStagedBundle(input: ReviewInput): Promise<OpusReview> {\n const cfg = resolveReviewerConfig();\n const startedAt = Date.now();\n\n if (!cfg.enabled) {\n return {\n verdict: 'skipped', confidence: 0,\n reasoning: 'Opus review disabled in config',\n concerns: [], suggestions: [],\n model: cfg.model, durationMs: 0,\n };\n }\n\n // Provider-key sanity check.\n // openrouter/* → needs OPENROUTER_API_KEY\n // anything else → assume the router's provider will handle its own auth\n if (cfg.model.startsWith('openrouter/')) {\n const openrouterKey = process.env.OPENROUTER_API_KEY ||\n (loadConfig().providers as Record<string, { apiKey?: string } | undefined>).openrouter?.apiKey;\n if (!openrouterKey) {\n logger.warn(COMPONENT, 'OpenRouter key missing — skipping review');\n return {\n verdict: 'skipped', confidence: 0,\n reasoning: 'No OpenRouter API key configured; review skipped',\n concerns: [], suggestions: [],\n model: cfg.model, durationMs: 0,\n };\n }\n }\n\n // v4.10.0-local polish: enforce cost caps. Tony's OpenRouter balance\n // is $9.54 — we don't want a runaway Opus reviewer to spam-bill. Caps:\n // per-review: $0.25 default\n // daily: $1.50 default\n // monthly: $5.00 default\n // Free models (Qwen3.6 Plus) cost $0, so caps never trigger for them.\n const budget = rolloverIfNeeded(loadBudget());\n if (budget.daily.costUsd >= cfg.maxDailyUsd) {\n logger.warn(COMPONENT, `Daily cap $${cfg.maxDailyUsd.toFixed(2)} hit ($${budget.daily.costUsd.toFixed(2)} spent today) — skipping review`);\n return {\n verdict: 'skipped', confidence: 0,\n reasoning: `Reviewer daily cost cap ($${cfg.maxDailyUsd.toFixed(2)}) reached — review skipped. Bumps over for tomorrow.`,\n concerns: [], suggestions: [],\n model: cfg.model, durationMs: 0,\n };\n }\n if (budget.monthly.costUsd >= cfg.maxMonthlyUsd) {\n logger.warn(COMPONENT, `Monthly cap $${cfg.maxMonthlyUsd.toFixed(2)} hit — skipping review`);\n return {\n verdict: 'skipped', confidence: 0,\n reasoning: `Reviewer monthly cost cap ($${cfg.maxMonthlyUsd.toFixed(2)}) reached — review skipped.`,\n concerns: [], suggestions: [],\n model: cfg.model, durationMs: 0,\n };\n }\n\n // Estimate per-review cost BEFORE calling. Cap: 0.25 USD default.\n const price = priceFor(cfg.model);\n const promptChars = buildReviewPrompt(input).length;\n const estInputTokens = Math.ceil(promptChars / 4); // ~4 chars per token\n const estOutputTokens = 2000; // we cap maxTokens at 2000\n const estCost = (estInputTokens / 1_000_000) * price.input + (estOutputTokens / 1_000_000) * price.output;\n if (estCost > cfg.maxPerReviewUsd) {\n logger.warn(COMPONENT, `Estimated review cost $${estCost.toFixed(3)} > per-review cap $${cfg.maxPerReviewUsd.toFixed(2)} — skipping`);\n return {\n verdict: 'skipped', confidence: 0,\n reasoning: `Review would cost ~$${estCost.toFixed(3)} which exceeds per-review cap $${cfg.maxPerReviewUsd.toFixed(2)}. Trim the bundle (fewer/smaller files) or raise the cap.`,\n concerns: [], suggestions: [],\n model: cfg.model, durationMs: 0,\n };\n }\n\n const prompt = buildReviewPrompt(input);\n try {\n const { chat } = await import('../providers/router.js');\n logger.info(COMPONENT, `Reviewing bundle for goal ${input.goalId} (${input.files.length} files) via ${cfg.model} — est $${estCost.toFixed(3)}`);\n const response = await chat({\n model: cfg.model,\n messages: [\n { role: 'system', content: 'You are a rigorous code reviewer. Return ONLY a JSON verdict block.' },\n { role: 'user', content: prompt },\n ],\n temperature: 0.2,\n maxTokens: 2000,\n });\n const raw = response?.content || '';\n const parsed = parseReview(raw);\n const durationMs = Date.now() - startedAt;\n\n // Record actual spend from usage if available, else use estimate.\n const usage = response?.usage;\n const actualCost = usage\n ? (usage.promptTokens / 1_000_000) * price.input + (usage.completionTokens / 1_000_000) * price.output\n : estCost;\n budget.daily.costUsd += actualCost;\n budget.monthly.costUsd += actualCost;\n budget.totalSpentUsd += actualCost;\n budget.totalReviews += 1;\n budget.updatedAt = new Date().toISOString();\n saveBudget(budget);\n\n logger.info(COMPONENT, `Review verdict: ${parsed.verdict} (confidence ${parsed.confidence.toFixed(2)}, ${durationMs}ms, actual cost $${actualCost.toFixed(4)}, daily $${budget.daily.costUsd.toFixed(3)}/${cfg.maxDailyUsd.toFixed(2)})`);\n return {\n ...parsed,\n model: cfg.model,\n durationMs,\n rawResponse: raw.slice(0, 10_000),\n };\n } catch (err) {\n const durationMs = Date.now() - startedAt;\n logger.warn(COMPONENT, `Review call failed: ${(err as Error).message}`);\n return {\n verdict: 'skipped', confidence: 0,\n reasoning: `Review call error: ${(err as Error).message}`,\n concerns: [], suggestions: [],\n model: cfg.model, durationMs,\n };\n }\n}\n\n/**\n * Is the reviewer configured to block on reject? (Used by applyStagedPR\n * to decide whether a `reject` verdict actually stops the apply.)\n */\nexport function reviewerBlocksOnReject(): boolean {\n return resolveReviewerConfig().blockOnReject;\n}\n\n// Silence unused import (keep for future use)\nvoid join; void dirname; void existsSync;\n"],"mappings":"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","names":[]}
@@ -16,8 +16,8 @@ const DESTRUCTIVE_PATTERNS = [
16
16
  { pattern: /\bsed\s+-i\b/, score: 3, reason: "in-place file edit via sed" }
17
17
  ];
18
18
  const EXFILTRATION_PATTERNS = [
19
- { pattern: /\bcurl\b[^|;&\n]*\$\(cat\s+[~\/][^)]*(?:ssh|key|token|secret|password|credential|\.env)[^)]*\)/, score: 25, reason: "curl with secret file in command substitution" },
20
- { pattern: /\bwget\b[^|;&\n]*\$\(cat\s+[~\/][^)]*(?:ssh|key|token|secret|password|credential|\.env)[^)]*\)/, score: 25, reason: "wget with secret file in command substitution" },
19
+ { pattern: /\bcurl\b[^|;&\n]*\$\(cat\s+[~/][^)]*(?:ssh|key|token|secret|password|credential|\.env)[^)]*\)/, score: 25, reason: "curl with secret file in command substitution" },
20
+ { pattern: /\bwget\b[^|;&\n]*\$\(cat\s+[~/][^)]*(?:ssh|key|token|secret|password|credential|\.env)[^)]*\)/, score: 25, reason: "wget with secret file in command substitution" },
21
21
  { pattern: /\bcurl\s+[^|;&\n]+\|\s*(?:sudo\s+)?(?:bash|sh|zsh)\b/, score: 25, reason: "curl piped to shell (remote code execution)" },
22
22
  { pattern: /\bwget\s+-\w*O-?\s+[^|;&\n]+\|\s*(?:sudo\s+)?(?:bash|sh|zsh)\b/, score: 25, reason: "wget piped to shell" },
23
23
  { pattern: /\bbase64\b[^|;&\n]*\|\s*\bcurl\b/, score: 20, reason: "base64-encoded data piped to curl" },