titan-agent 5.4.2 → 5.5.6

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 (302) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/agent.js +9 -5
  3. package/dist/agent/agent.js.map +1 -1
  4. package/dist/agent/agentLoop.js +7 -3
  5. package/dist/agent/agentLoop.js.map +1 -1
  6. package/dist/agent/checkpoint.js +2 -2
  7. package/dist/agent/checkpoint.js.map +1 -1
  8. package/dist/agent/commandPost.js +3 -3
  9. package/dist/agent/commandPost.js.map +1 -1
  10. package/dist/agent/goalProposer.js +2 -2
  11. package/dist/agent/goalProposer.js.map +1 -1
  12. package/dist/agent/goals.js +3 -3
  13. package/dist/agent/goals.js.map +1 -1
  14. package/dist/agent/peerAdvise.js +1 -1
  15. package/dist/agent/peerAdvise.js.map +1 -1
  16. package/dist/agent/planner.js +4 -4
  17. package/dist/agent/planner.js.map +1 -1
  18. package/dist/agent/userProfile.js +2 -2
  19. package/dist/agent/userProfile.js.map +1 -1
  20. package/dist/cli/doctor.js +33 -0
  21. package/dist/cli/doctor.js.map +1 -1
  22. package/dist/cli/onboard.js +4 -4
  23. package/dist/cli/onboard.js.map +1 -1
  24. package/dist/config/config.js +3 -3
  25. package/dist/config/config.js.map +1 -1
  26. package/dist/config/schema.js +8 -1
  27. package/dist/config/schema.js.map +1 -1
  28. package/dist/gateway/routes/adminRouter.js +500 -0
  29. package/dist/gateway/routes/adminRouter.js.map +1 -0
  30. package/dist/gateway/routes/agents.js +231 -0
  31. package/dist/gateway/routes/agents.js.map +1 -0
  32. package/dist/gateway/routes/agentsRouter.js +32 -0
  33. package/dist/gateway/routes/agentsRouter.js.map +1 -0
  34. package/dist/gateway/routes/checkpoints.js +41 -0
  35. package/dist/gateway/routes/checkpoints.js.map +1 -0
  36. package/dist/gateway/routes/commandPost.js +755 -0
  37. package/dist/gateway/routes/commandPost.js.map +1 -0
  38. package/dist/gateway/routes/companies.js +166 -0
  39. package/dist/gateway/routes/companies.js.map +1 -0
  40. package/dist/gateway/routes/files.js +295 -0
  41. package/dist/gateway/routes/files.js.map +1 -0
  42. package/dist/gateway/routes/hardwareRouter.js +151 -0
  43. package/dist/gateway/routes/hardwareRouter.js.map +1 -0
  44. package/dist/gateway/routes/mcpRouter.js +88 -0
  45. package/dist/gateway/routes/mcpRouter.js.map +1 -0
  46. package/dist/gateway/routes/mesh.js +464 -0
  47. package/dist/gateway/routes/mesh.js.map +1 -0
  48. package/dist/gateway/routes/metricsRouter.js +131 -0
  49. package/dist/gateway/routes/metricsRouter.js.map +1 -0
  50. package/dist/gateway/routes/organism.js +82 -0
  51. package/dist/gateway/routes/organism.js.map +1 -0
  52. package/dist/gateway/routes/paperclip.js +101 -0
  53. package/dist/gateway/routes/paperclip.js.map +1 -0
  54. package/dist/gateway/routes/sessions.js +227 -0
  55. package/dist/gateway/routes/sessions.js.map +1 -0
  56. package/dist/gateway/routes/skills.js +295 -0
  57. package/dist/gateway/routes/skills.js.map +1 -0
  58. package/dist/gateway/routes/socialRouter.js +145 -0
  59. package/dist/gateway/routes/socialRouter.js.map +1 -0
  60. package/dist/gateway/routes/systemRouter.js +220 -0
  61. package/dist/gateway/routes/systemRouter.js.map +1 -0
  62. package/dist/gateway/routes/teamsRecipes.js +297 -0
  63. package/dist/gateway/routes/teamsRecipes.js.map +1 -0
  64. package/dist/gateway/routes/tests.js +401 -0
  65. package/dist/gateway/routes/tests.js.map +1 -0
  66. package/dist/gateway/routes/traces.js +33 -0
  67. package/dist/gateway/routes/traces.js.map +1 -0
  68. package/dist/gateway/routes/voiceRouter.js +770 -0
  69. package/dist/gateway/routes/voiceRouter.js.map +1 -0
  70. package/dist/gateway/routes/watchRouter.js +131 -0
  71. package/dist/gateway/routes/watchRouter.js.map +1 -0
  72. package/dist/gateway/server.js +1179 -7379
  73. package/dist/gateway/server.js.map +1 -1
  74. package/dist/mcp/registry.js +2 -2
  75. package/dist/mcp/registry.js.map +1 -1
  76. package/dist/memory/episodic.js +2 -2
  77. package/dist/memory/episodic.js.map +1 -1
  78. package/dist/memory/learning.js +3 -3
  79. package/dist/memory/learning.js.map +1 -1
  80. package/dist/memory/memory.js +3 -3
  81. package/dist/memory/memory.js.map +1 -1
  82. package/dist/organism/drives.js +2 -2
  83. package/dist/organism/drives.js.map +1 -1
  84. package/dist/providers/errorTaxonomy.js +13 -0
  85. package/dist/providers/errorTaxonomy.js.map +1 -1
  86. package/dist/providers/ollama.js +3 -1
  87. package/dist/providers/ollama.js.map +1 -1
  88. package/dist/providers/openai_compat.js +4 -3
  89. package/dist/providers/openai_compat.js.map +1 -1
  90. package/dist/providers/router.js +13 -0
  91. package/dist/providers/router.js.map +1 -1
  92. package/dist/safety/fixOscillation.js +15 -0
  93. package/dist/safety/fixOscillation.js.map +1 -1
  94. package/dist/safety/killSwitch.js +2 -2
  95. package/dist/safety/killSwitch.js.map +1 -1
  96. package/dist/safety/selfRepair.js +7 -3
  97. package/dist/safety/selfRepair.js.map +1 -1
  98. package/dist/skills/builtin/agent_debate.js +2 -2
  99. package/dist/skills/builtin/agent_debate.js.map +1 -1
  100. package/dist/skills/builtin/apply_patch.js +3 -3
  101. package/dist/skills/builtin/apply_patch.js.map +1 -1
  102. package/dist/skills/builtin/shell.js +2 -2
  103. package/dist/skills/builtin/shell.js.map +1 -1
  104. package/dist/skills/builtin/voice_control.js +49 -0
  105. package/dist/skills/builtin/voice_control.js.map +1 -0
  106. package/dist/skills/builtin/widget_gallery.js +6 -1
  107. package/dist/skills/builtin/widget_gallery.js.map +1 -1
  108. package/dist/skills/registry.js +15 -4
  109. package/dist/skills/registry.js.map +1 -1
  110. package/dist/storage/JsonStorage.js +4 -4
  111. package/dist/storage/JsonStorage.js.map +1 -1
  112. package/dist/utils/constants.js +1 -1
  113. package/dist/utils/constants.js.map +1 -1
  114. package/dist/utils/helpers.js +3 -1
  115. package/dist/utils/helpers.js.map +1 -1
  116. package/dist/utils/lifecycle.js +86 -0
  117. package/dist/utils/lifecycle.js.map +1 -0
  118. package/dist/voice/bridge.js +136 -0
  119. package/dist/voice/bridge.js.map +1 -0
  120. package/docs/COO-MASTER-PLAN-2026-05-02.md +474 -0
  121. package/docs/HANDOFF/2026-04-29.md +141 -0
  122. package/docs/HANDOFF-2026-04-30.md +144 -0
  123. package/docs/HANDOFF-2026-05-03.md +114 -0
  124. package/docs/adr/2026-04-29-widget-pipeline-traceability.md +49 -0
  125. package/docs/agent-memory/README.md +45 -0
  126. package/docs/agent-memory/commands.md +100 -0
  127. package/docs/agent-memory/context-tree.md +101 -0
  128. package/docs/agent-memory/current-state.md +54 -0
  129. package/docs/agent-memory/decisions.md +78 -0
  130. package/docs/agent-memory/known-issues.md +76 -0
  131. package/docs/agent-memory/reflections.md +52 -0
  132. package/docs/agent-memory/skills-candidates.md +80 -0
  133. package/docs/superpowers/plans/2026-04-29-comprehensive-audit.md +256 -0
  134. package/docs/superpowers/plans/2026-04-29-comprehensive-test-plan.md +396 -0
  135. package/docs/superpowers/plans/2026-04-29-fix-all-prs.md +251 -0
  136. package/docs/superpowers/plans/2026-04-29-gitnexus-gap-remediation.md +969 -0
  137. package/package.json +5 -2
  138. package/ui/dist/assets/{AuditPanel-CM6Wg9hO.js → AuditPanel-VzSndmDN.js} +2 -2
  139. package/ui/dist/assets/{AutonomyPanel-CESx3ANg.js → AutonomyPanel-BiFouzAV.js} +2 -2
  140. package/ui/dist/assets/AutopilotPanel-fjOfM668.js +1 -0
  141. package/ui/dist/assets/{AutoresearchPanel-DR47NqT5.js → AutoresearchPanel-CVCxzAH3.js} +2 -2
  142. package/ui/dist/assets/BackupPanel-CHVTG--q.js +1 -0
  143. package/ui/dist/assets/{BrowserPanel-C15x9bLn.js → BrowserPanel-D5mvMKFU.js} +2 -2
  144. package/ui/dist/assets/CPActivity-B12mt35m.js +1 -0
  145. package/ui/dist/assets/CPAgentDetail-DsdShc-1.js +1 -0
  146. package/ui/dist/assets/CPAgents-j_7C-oQV.js +1 -0
  147. package/ui/dist/assets/CPApprovals-BShKSX9X.js +1 -0
  148. package/ui/dist/assets/CPCosts-CKPlhBDs.js +1 -0
  149. package/ui/dist/assets/CPDashboard-11c0nkxK.js +1 -0
  150. package/ui/dist/assets/CPFiles-BhLEOnXy.js +1 -0
  151. package/ui/dist/assets/CPGoals-Bi3t1b2P.js +1 -0
  152. package/ui/dist/assets/CPInbox-Bbr7khp6.js +11 -0
  153. package/ui/dist/assets/CPIssueDetail-DSdgNK8r.js +1 -0
  154. package/ui/dist/assets/CPIssues-DDEVKhX6.js +1 -0
  155. package/ui/dist/assets/CPLayout-DgPOfyGv.js +17 -0
  156. package/ui/dist/assets/CPOrg-Df73RrRJ.js +8 -0
  157. package/ui/dist/assets/CPRuns-ByioAz8w.js +1 -0
  158. package/ui/dist/assets/{CPSocial-nb-j7sOE.js → CPSocial-Dlnr_w1X.js} +2 -2
  159. package/ui/dist/assets/ChannelsPanel-DQjQCTK5.js +1 -0
  160. package/ui/dist/assets/CheckpointsPanel-C4vKjlAJ.js +1 -0
  161. package/ui/dist/assets/CommandPostHub-C9pp5Giq.js +24 -0
  162. package/ui/dist/assets/CronPanel-C6bzUfrD.js +1 -0
  163. package/ui/dist/assets/DaemonPanel-BA5Tb_UO.js +1 -0
  164. package/ui/dist/assets/{DataTable-B2Ma8hfi.js → DataTable-CH7IYJJh.js} +1 -1
  165. package/ui/dist/assets/{EmptyState-CcKyk5Yn.js → EmptyState-jU6yNDnF.js} +1 -1
  166. package/ui/dist/assets/{EvalHarnessPanel-BqtMc1ZM.js → EvalHarnessPanel-DnYqredY.js} +2 -2
  167. package/ui/dist/assets/EvalPanel-ChO7CD1r.js +1 -0
  168. package/ui/dist/assets/{FilesPanel-3QKvrWPo.js → FilesPanel-CaUkv2is.js} +2 -2
  169. package/ui/dist/assets/FleetPanel-DC_5uj0N.js +1 -0
  170. package/ui/dist/assets/{HomelabPanel-DhrjTX9m.js → HomelabPanel-CE5PGRpL.js} +2 -2
  171. package/ui/dist/assets/InfraView-C-uSlvb9.js +2 -0
  172. package/ui/dist/assets/InlineEditableField-BMQjiE6-.js +1 -0
  173. package/ui/dist/assets/Input-Bu_b3qmY.js +1 -0
  174. package/ui/dist/assets/IntegrationsPanel-DsYpAq43.js +1 -0
  175. package/ui/dist/assets/IntelligenceView-DUdIO1K7.js +2 -0
  176. package/ui/dist/assets/LearningPanel-UpQZC-mA.js +1 -0
  177. package/ui/dist/assets/LogsPanel-ClXJ4fcr.js +1 -0
  178. package/ui/dist/assets/McpPanel-JKgtIERQ.js +1 -0
  179. package/ui/dist/assets/{MemoryGraphPanel-Bzvjmzvk.js → MemoryGraphPanel-Bo2OrvA6.js} +2 -2
  180. package/ui/dist/assets/MemoryWikiPanel-BqJ1AmYm.js +11 -0
  181. package/ui/dist/assets/{MeshPanel-C3LJSlht.js → MeshPanel-BJVGYvwk.js} +2 -2
  182. package/ui/dist/assets/Modal-CAAooiZU.js +1 -0
  183. package/ui/dist/assets/NvidiaPanel-BtCg3G4w.js +1 -0
  184. package/ui/dist/assets/OrganismPanel-DgrTTzcF.js +1 -0
  185. package/ui/dist/assets/OverviewPanel-rVav1Hox.js +1 -0
  186. package/ui/dist/assets/{PageHeader-BimceqQo.js → PageHeader-CnZtP8ek.js} +1 -1
  187. package/ui/dist/assets/PaperclipPanel-C-FKdhiF.js +1 -0
  188. package/ui/dist/assets/{PersonasPanel-L1j78p6H.js → PersonasPanel-BmlxokfB.js} +1 -1
  189. package/ui/dist/assets/RecipesPanel-BNKKChis.js +1 -0
  190. package/ui/dist/assets/SecurityPanel-I7JRHiNy.js +1 -0
  191. package/ui/dist/assets/SelfImprovePanel-u9h0Lt3p.js +1 -0
  192. package/ui/dist/assets/{SelfProposalsPanel-lNmiDThB.js → SelfProposalsPanel-DKl9iBjM.js} +2 -2
  193. package/ui/dist/assets/SessionsPanel-BhRiWI_g.js +1 -0
  194. package/ui/dist/assets/{SessionsTab-JQbltWww.js → SessionsTab-Bk08wyeY.js} +1 -1
  195. package/ui/dist/assets/SettingsPanel-haLfmG2k.js +1 -0
  196. package/ui/dist/assets/SettingsView--gi3fxI8.js +2 -0
  197. package/ui/dist/assets/{SkeletonLoader-atQtpcF5.js → SkeletonLoader-B5v09EF_.js} +1 -1
  198. package/ui/dist/assets/{SkillsPanel-DlFs2ih7.js → SkillsPanel-BlAHFLcQ.js} +1 -1
  199. package/ui/dist/assets/SomaView-CExtS3zw.js +5 -0
  200. package/ui/dist/assets/{StatCard-DciE_Iqc.js → StatCard-BIsyMybM.js} +1 -1
  201. package/ui/dist/assets/{StatusBadge-BtfSPoW2.js → StatusBadge-D5nU7El8.js} +1 -1
  202. package/ui/dist/assets/Tabs-BBYZrBI8.js +1 -0
  203. package/ui/dist/assets/TeamsPanel-LPXJg823.js +1 -0
  204. package/ui/dist/assets/TelemetryPanel-EqpRBmOI.js +1 -0
  205. package/ui/dist/assets/TitanCanvas-BCbWnLMd.js +985 -0
  206. package/ui/dist/assets/ToolsView-CeP0Zz-N.js +2 -0
  207. package/ui/dist/assets/{Tooltip-70UK0E2I.js → Tooltip-BSO2XVpF.js} +1 -1
  208. package/ui/dist/assets/TraceViewer-BKI7o5B0.js +1 -0
  209. package/ui/dist/assets/TrainingPanel-c-RhjdE1.js +1 -0
  210. package/ui/dist/assets/VoiceOverlay-D-gc58b0.js +27 -0
  211. package/ui/dist/assets/VramPanel-C6xc7zgd.js +1 -0
  212. package/ui/dist/assets/{WatchView-C-sGFpVy.js → WatchView-dqBVCVH0.js} +1 -1
  213. package/ui/dist/assets/WorkTab-CBoLNrTM.js +1 -0
  214. package/ui/dist/assets/{WorkflowsPanel-CvgQU1xI.js → WorkflowsPanel-BAnSTOYe.js} +2 -2
  215. package/ui/dist/assets/approvalHeadline-DB9SgR-9.js +1 -0
  216. package/ui/dist/assets/{arrow-left-DwqHtJiU.js → arrow-left-5chqas7J.js} +1 -1
  217. package/ui/dist/assets/briefcase-D4vLzudp.js +6 -0
  218. package/ui/dist/assets/{chart-column-BtNO6sRy.js → chart-column-CdFlBpoP.js} +1 -1
  219. package/ui/dist/assets/check-Bpm1IONe.js +6 -0
  220. package/ui/dist/assets/chevron-down-D7OLjvuD.js +6 -0
  221. package/ui/dist/assets/chevron-right-aQEw2mUW.js +6 -0
  222. package/ui/dist/assets/chevron-up-C5g6pEj8.js +6 -0
  223. package/ui/dist/assets/{circle-check-big-DZRE_MbN.js → circle-check-big-fPhEdP88.js} +1 -1
  224. package/ui/dist/assets/clock-CTsgP_Sn.js +6 -0
  225. package/ui/dist/assets/{dollar-sign-aVG3a5eL.js → dollar-sign-CudFVYFc.js} +1 -1
  226. package/ui/dist/assets/{download-BxiWJU4G.js → download-DZRxDn67.js} +1 -1
  227. package/ui/dist/assets/external-link-BZ0y_Ahx.js +6 -0
  228. package/ui/dist/assets/{eye-off-CkgfFYhm.js → eye-off-BmJF0YYx.js} +1 -1
  229. package/ui/dist/assets/folder-DA43TRCm.js +11 -0
  230. package/ui/dist/assets/{funnel-PkLdxKyC.js → funnel-J3mULzrz.js} +1 -1
  231. package/ui/dist/assets/{git-branch-BM-Gw95X.js → git-branch-oHibJqDq.js} +1 -1
  232. package/ui/dist/assets/{index-D0RJ8701.css → index-BR0vfkIi.css} +1 -1
  233. package/ui/dist/assets/{index-CahJbWSR.js → index-DzwowwSI.js} +20 -20
  234. package/ui/dist/assets/{layers-BuGf4FIJ.js → layers-DsyEyu7z.js} +1 -1
  235. package/ui/dist/assets/{legacy-CR6o4t-y.js → legacy-8ITl64sV.js} +1 -1
  236. package/ui/dist/assets/{lightbulb-n8gc_XAL.js → lightbulb-C54Ske-p.js} +1 -1
  237. package/ui/dist/assets/list-todo-Cnd4rdoK.js +6 -0
  238. package/ui/dist/assets/loader-circle-1YOBsoQp.js +6 -0
  239. package/ui/dist/assets/network-DbGDAdrn.js +6 -0
  240. package/ui/dist/assets/{pause-DCV52koX.js → pause-CYhO_uQo.js} +1 -1
  241. package/ui/dist/assets/{play-CcJ9BnCh.js → play-DVY9c5Ck.js} +1 -1
  242. package/ui/dist/assets/{plug-CfWBXfCl.js → plug-BcXjlPUL.js} +1 -1
  243. package/ui/dist/assets/plus-Csu2v9GN.js +6 -0
  244. package/ui/dist/assets/{proxy-CzZDfLmm.js → proxy-DxS2_9D7.js} +1 -1
  245. package/ui/dist/assets/rotate-ccw-Co-_W04j.js +6 -0
  246. package/ui/dist/assets/save-Btx-kpoW.js +6 -0
  247. package/ui/dist/assets/search-0hXTwEZR.js +6 -0
  248. package/ui/dist/assets/send-TEpapzQR.js +6 -0
  249. package/ui/dist/assets/shield-check-DjBJXZUr.js +6 -0
  250. package/ui/dist/assets/{square-DJpUhlxi.js → square-OweUvjP-.js} +1 -1
  251. package/ui/dist/assets/{target-DWcmM_9m.js → target-BRW80Xer.js} +1 -1
  252. package/ui/dist/assets/terminal-BtiqJ628.js +16 -0
  253. package/ui/dist/assets/{toggle-right-YusFQ69L.js → toggle-right-CKtSrl28.js} +1 -1
  254. package/ui/dist/assets/{trash-2-CK7yQ55V.js → trash-2-DgWrHVax.js} +1 -1
  255. package/ui/dist/assets/{trending-up-DGjFyubC.js → trending-up-MpIrE4j6.js} +1 -1
  256. package/ui/dist/assets/{trophy-uQv_NgDB.js → trophy-CECuZNhX.js} +1 -1
  257. package/ui/dist/assets/users-dZgv4ePG.js +16 -0
  258. package/ui/dist/assets/wrench-CDz3xYve.js +11 -0
  259. package/ui/dist/index.html +2 -2
  260. package/ui/dist/assets/AutopilotPanel-DtEet1hJ.js +0 -1
  261. package/ui/dist/assets/BackupPanel-BGP8p3l3.js +0 -1
  262. package/ui/dist/assets/CPAgents-DYUtPzSq.js +0 -1
  263. package/ui/dist/assets/CPDashboard-Bf0-SyCh.js +0 -6
  264. package/ui/dist/assets/CPFiles-CxgxjQcO.js +0 -1
  265. package/ui/dist/assets/CPGoals-BsmCMTvT.js +0 -1
  266. package/ui/dist/assets/CPInbox-tMSbmQ9H.js +0 -11
  267. package/ui/dist/assets/ChannelsPanel-DP5C2OKd.js +0 -1
  268. package/ui/dist/assets/CheckpointsPanel-DlranVLZ.js +0 -1
  269. package/ui/dist/assets/CommandPostHub-BgxIa4Ev.js +0 -29
  270. package/ui/dist/assets/CronPanel-LoT5yKwJ.js +0 -1
  271. package/ui/dist/assets/DaemonPanel-DBGMqaE_.js +0 -1
  272. package/ui/dist/assets/EvalPanel-Bc33j0pN.js +0 -1
  273. package/ui/dist/assets/FleetPanel-CSsXuQYj.js +0 -1
  274. package/ui/dist/assets/InfraView-CR6HyrL6.js +0 -2
  275. package/ui/dist/assets/InlineEditableField-CnvF-yFR.js +0 -1
  276. package/ui/dist/assets/Input-GTHp2Rkr.js +0 -1
  277. package/ui/dist/assets/IntegrationsPanel-CymCRE3T.js +0 -1
  278. package/ui/dist/assets/IntelligenceView-C1IHxJRC.js +0 -2
  279. package/ui/dist/assets/LearningPanel-DOCES3lH.js +0 -1
  280. package/ui/dist/assets/LogsPanel-BLnAqEaZ.js +0 -1
  281. package/ui/dist/assets/McpPanel-ChUzmr3z.js +0 -1
  282. package/ui/dist/assets/MemoryWikiPanel-Dwk3Aqwd.js +0 -11
  283. package/ui/dist/assets/NvidiaPanel-CeZK_-CV.js +0 -1
  284. package/ui/dist/assets/OrganismPanel-BB6YOiQV.js +0 -1
  285. package/ui/dist/assets/OverviewPanel-BmtBhQnv.js +0 -1
  286. package/ui/dist/assets/PaperclipPanel-C-brgwA3.js +0 -1
  287. package/ui/dist/assets/RecipesPanel-34lCfynJ.js +0 -1
  288. package/ui/dist/assets/SecurityPanel-CBTPWLj6.js +0 -1
  289. package/ui/dist/assets/SelfImprovePanel-BrPbFHhG.js +0 -1
  290. package/ui/dist/assets/SessionsPanel-DAEYIn83.js +0 -1
  291. package/ui/dist/assets/SettingsPanel-CzRROAYQ.js +0 -1
  292. package/ui/dist/assets/SettingsView-CN7ii2uw.js +0 -2
  293. package/ui/dist/assets/SomaView-Ba642Oqb.js +0 -5
  294. package/ui/dist/assets/TeamsPanel-DKQ5z2Qe.js +0 -1
  295. package/ui/dist/assets/TelemetryPanel-B6KAc55Q.js +0 -1
  296. package/ui/dist/assets/TitanCanvas-C-s0A-lv.js +0 -1092
  297. package/ui/dist/assets/ToolsView-Dei0KMP0.js +0 -2
  298. package/ui/dist/assets/TraceViewer-BniolyBx.js +0 -1
  299. package/ui/dist/assets/TrainingPanel-Bz4CTPGW.js +0 -1
  300. package/ui/dist/assets/VoiceOverlay-CmNCrLcd.js +0 -37
  301. package/ui/dist/assets/VramPanel-Xh_OtRDR.js +0 -1
  302. package/ui/dist/assets/WorkTab-BjLNmgIK.js +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/organism/drives.ts"],"sourcesContent":["/**\n * TITAN — Drive Layer (Soma organism / endocrine system)\n *\n * Five homeostatic drives. Each computes a 0-1 \"satisfaction\" from existing\n * TITAN telemetry — no new instrumentation. When satisfaction dips below the\n * drive's setpoint, pressure accumulates. Cross-drive pressure fusion (see\n * pressure.ts) eventually produces a soma_proposal for human approval.\n *\n * Gated by config.organism.enabled — this module is inert when disabled.\n *\n * DRIVES SHIPPED IN v4.0:\n * Purpose — alignment with priority-1 goals\n * Hunger — backlog size vs. throughput\n * Curiosity — task-type diversity in recent trajectories\n * Safety — budget runway + recent error rate\n * Social — stale agent fraction\n *\n * DEFERRED TO v4.1+:\n * Hygiene — needs npm test + git status shell hooks\n */\nimport { existsSync, readFileSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport { ensureDir } from '../utils/helpers.js';\nimport { listGoals, getReadyTasks, type Goal } from '../agent/goals.js';\nimport { getRegisteredAgents, getBudgetPolicies, listRuns, type RegisteredAgent, type BudgetPolicy, type CPRun } from '../agent/commandPost.js';\nimport { getRecentTrajectories, type TaskTrajectory } from '../agent/trajectoryLogger.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Drives';\nconst DRIVE_STATE_PATH = join(TITAN_HOME, 'drive-state.json');\n\n// ── Types ────────────────────────────────────────────────────────\n\nexport type DriveId = 'purpose' | 'hunger' | 'curiosity' | 'safety' | 'social';\n\nexport interface DriveSnapshot {\n /** Timestamp of the snapshot in epoch ms. */\n now: number;\n /** All goals from goals.ts. */\n goals: Goal[];\n /** Output of getReadyTasks() — ready-to-execute subtasks. */\n readyTasks: Array<{ goal: Goal; subtask: Goal['subtasks'][number] }>;\n /** Recent CPRun history (up to 100 most recent). */\n recentRuns: CPRun[];\n /** Active budget policies. */\n budgets: BudgetPolicy[];\n /** All registered agents. */\n agents: RegisteredAgent[];\n /** Last 100 trajectory entries. */\n trajectories: TaskTrajectory[];\n /**\n * v4.9.0: fraction of GPU VRAM in use (0–1). Undefined when no GPU\n * is attached or the orchestrator hasn't refreshed yet.\n */\n vramSaturation?: number;\n /**\n * v4.9.0: error rate across recent LLM / tool calls from the\n * gateway metrics layer (0–1). Undefined when metrics are unavailable.\n */\n telemetryErrorRate?: number;\n /** v4.9.0: total LLM + tool-call requests since gateway start. */\n telemetryTotalRequests?: number;\n /**\n * v4.9.0: count of error patterns the learning layer has accumulated\n * but not yet resolved. High count pulls Curiosity toward an\n * investigate/improve proposal.\n */\n unresolvedErrorPatterns?: number;\n /**\n * v5.3.2 (Phase 8 / Track B): timestamp of the most recent successful\n * Facebook page post in epoch ms, or null if TITAN hasn't posted yet\n * since fb-autopilot was enabled. Sourced from\n * `~/.titan/fb-autopilot-state.json`. Drives the Social-drive's\n * \"social media presence\" factor — without this, social pressure was\n * 100% about agent heartbeat staleness, which the README implied was\n * about social posting. Now both factors blend.\n */\n lastFacebookPostAt?: number | null;\n}\n\nexport interface DriveDefinition {\n id: DriveId;\n label: string;\n /** Satisfaction level below which this drive starts contributing pressure. */\n defaultSetpoint: number;\n /** Relative weight in cross-drive pressure fusion (1.0 is baseline). */\n weight: number;\n /** Pure function — computes satisfaction 0-1 from the snapshot. */\n compute: (snapshot: DriveSnapshot) => { satisfaction: number; inputs?: Record<string, unknown> };\n /** Short human-readable explanation used in prompts, UI tooltips, and activity feed. */\n describe: (satisfaction: number, inputs?: Record<string, unknown>) => string;\n}\n\nexport interface DriveState {\n id: DriveId;\n label: string;\n satisfaction: number;\n setpoint: number;\n /** 0 when satisfaction >= setpoint, else (setpoint − satisfaction) × weight. */\n pressure: number;\n weight: number;\n inputs?: Record<string, unknown>;\n description: string;\n}\n\nexport interface DriveTickResult {\n timestamp: string;\n drives: DriveState[];\n totalPressure: number;\n dominantDrives: DriveId[];\n}\n\n// ── Numeric helpers ──────────────────────────────────────────────\n\n/** Clamp to [0,1]. */\nfunction clamp01(v: number): number {\n if (!Number.isFinite(v)) return 0;\n return Math.max(0, Math.min(1, v));\n}\n\n/** Sigmoid centred on `mid` with slope `k`. Returns high → 1 when x is low. */\nfunction invertedSigmoid(x: number, mid: number, k = 1): number {\n return clamp01(1 / (1 + Math.exp(k * (x - mid))));\n}\n\n/** Gini coefficient of a count distribution. 0 = uniform, 1 = all same task. */\nfunction gini(counts: number[]): number {\n if (counts.length === 0) return 0;\n const n = counts.length;\n const sum = counts.reduce((a, b) => a + b, 0);\n if (sum === 0) return 0;\n const sorted = [...counts].sort((a, b) => a - b);\n let cum = 0;\n for (let i = 0; i < n; i++) cum += (i + 1) * sorted[i];\n return clamp01((2 * cum) / (n * sum) - (n + 1) / n);\n}\n\n// ── Drive definitions ────────────────────────────────────────────\n\nconst PURPOSE: DriveDefinition = {\n id: 'purpose',\n label: 'Purpose',\n defaultSetpoint: 0.7,\n weight: 1.4,\n compute: (snap) => {\n // Priority-1 goals tagged as high-priority. Satisfaction reflects how\n // recently any of them progressed. No priority-1 goals → satiated\n // (nothing to worry about).\n const priorityOne = snap.goals.filter(g =>\n g.status === 'active' && g.priority === 1,\n );\n if (priorityOne.length === 0) {\n return { satisfaction: 0.9, inputs: { priorityOneCount: 0 } };\n }\n const latest = Math.max(...priorityOne.map(g =>\n new Date(g.updatedAt || g.createdAt).getTime(),\n ));\n const hoursSince = Math.max(0, (snap.now - latest) / 3_600_000);\n const satisfaction = clamp01(1 - hoursSince / 24);\n return {\n satisfaction,\n inputs: { priorityOneCount: priorityOne.length, hoursSinceProgress: Math.round(hoursSince * 10) / 10 },\n };\n },\n describe: (s, inputs) => {\n const count = (inputs?.priorityOneCount as number) ?? 0;\n if (count === 0) return 'no priority-1 goals in flight';\n const hours = (inputs?.hoursSinceProgress as number) ?? 0;\n if (s < 0.3) return `${count} priority-1 goal(s) stalled — no progress in ${hours.toFixed(1)}h`;\n if (s < 0.6) return `${count} priority-1 goal(s) need attention`;\n return `${count} priority-1 goal(s) on track`;\n },\n};\n\nconst HUNGER: DriveDefinition = {\n id: 'hunger',\n label: 'Hunger',\n defaultSetpoint: 0.6,\n weight: 1.0,\n compute: (snap) => {\n const readyCount = snap.readyTasks.length;\n // Oldest ready subtask age in hours, using parent goal createdAt as proxy.\n const oldestAgeHours = snap.readyTasks.length === 0\n ? 0\n : Math.max(...snap.readyTasks.map(r =>\n (snap.now - new Date(r.goal.createdAt).getTime()) / 3_600_000,\n ));\n // Both signals independently drag satisfaction down.\n // v5.0.0: floor backlog satisfaction at 0.15 so extreme backlogs\n // (e.g. 1000+ zombie goals) don't drive hunger to absolute zero,\n // which causes SOMA to panic-propose even more goals.\n const backlogSatisfaction = Math.max(0.15, invertedSigmoid(readyCount, 5, 0.35));\n const ageSatisfaction = invertedSigmoid(oldestAgeHours, 4, 0.5);\n const satisfaction = Math.min(backlogSatisfaction, ageSatisfaction);\n return {\n satisfaction,\n inputs: { readyCount, oldestAgeHours: Math.round(oldestAgeHours * 10) / 10 },\n };\n },\n describe: (s, inputs) => {\n const count = (inputs?.readyCount as number) ?? 0;\n const age = (inputs?.oldestAgeHours as number) ?? 0;\n if (count === 0) return 'backlog empty';\n if (s < 0.3) return `backlog ${count}, oldest ${age.toFixed(1)}h — elevated`;\n if (s < 0.6) return `backlog ${count}, oldest ${age.toFixed(1)}h`;\n return `backlog ${count} — fed`;\n },\n};\n\nconst CURIOSITY: DriveDefinition = {\n id: 'curiosity',\n label: 'Curiosity',\n defaultSetpoint: 0.5,\n weight: 0.8,\n compute: (snap) => {\n // Novelty = task-type diversity across recent trajectories.\n // Few distinct task types → elevated curiosity (stale). Rich variety\n // → satiated. We compose two signals:\n // 1) coverage: how many distinct types relative to a target of 5\n // 2) balance: how evenly distributed those types are (1 − gini)\n // Satisfaction = min(coverage, balance) so either deficit pulls it\n // down. Low sample counts default to middling satisfaction.\n if (snap.trajectories.length < 5) {\n return { satisfaction: 0.6, inputs: { trajectoryCount: snap.trajectories.length } };\n }\n const typeCounts: Record<string, number> = {};\n for (const t of snap.trajectories) {\n typeCounts[t.taskType || 'unknown'] = (typeCounts[t.taskType || 'unknown'] || 0) + 1;\n }\n const typeCount = Object.keys(typeCounts).length;\n const coverage = clamp01(typeCount / 5);\n const counts = Object.values(typeCounts);\n const balance = typeCount <= 1 ? 0 : clamp01(1 - gini(counts));\n const diversitySat = typeCount <= 1 ? coverage : Math.min(coverage, balance);\n\n // v4.9.0: unresolved error patterns are a form of \"task-type\n // novelty the organism hasn't figured out yet.\" More than a\n // handful of unresolved patterns pulls Curiosity toward an\n // investigate-and-improve proposal (feeds Self-Improve pipeline).\n // Scales 0→10+ patterns linearly.\n let errorPatternSat = 1;\n if (typeof snap.unresolvedErrorPatterns === 'number' && snap.unresolvedErrorPatterns > 2) {\n errorPatternSat = clamp01(1 - (snap.unresolvedErrorPatterns - 2) / 10);\n }\n\n const satisfaction = Math.min(diversitySat, errorPatternSat);\n return {\n satisfaction,\n inputs: {\n trajectoryCount: snap.trajectories.length,\n taskTypes: typeCount,\n coverage: Math.round(coverage * 100) / 100,\n balance: Math.round(balance * 100) / 100,\n unresolvedErrorPatterns: snap.unresolvedErrorPatterns ?? 0,\n errorPatternSat: Math.round(errorPatternSat * 100) / 100,\n },\n };\n },\n describe: (s, inputs) => {\n const types = (inputs?.taskTypes as number) ?? 0;\n const patterns = (inputs?.unresolvedErrorPatterns as number) ?? 0;\n if (patterns >= 5) return `${patterns} unresolved error patterns — needs investigation`;\n if (s < 0.3) return `stuck in ${types} task type(s) — stale`;\n if (s < 0.6) return `${types} task type(s) — could use novelty`;\n return `${types} distinct task type(s) — engaged`;\n },\n};\n\nconst SAFETY: DriveDefinition = {\n id: 'safety',\n label: 'Safety',\n defaultSetpoint: 0.8,\n weight: 1.6,\n compute: (snap) => {\n // Budget runway: min runway across all enabled budgets.\n let budgetSatisfaction = 1;\n const relevantBudgets = snap.budgets.filter(b => b.enabled && b.limitUsd > 0);\n if (relevantBudgets.length > 0) {\n const runways = relevantBudgets.map(b => clamp01(1 - b.currentSpend / b.limitUsd));\n budgetSatisfaction = Math.min(...runways);\n }\n // Recent error rate from last 100 CPRuns in the last 24h.\n const dayMs = 86_400_000;\n const recent = snap.recentRuns.filter(r =>\n snap.now - new Date(r.startedAt).getTime() < dayMs,\n );\n let errorSatisfaction = 1;\n if (recent.length >= 5) {\n const errors = recent.filter(r => r.status === 'error' || r.status === 'failed').length;\n errorSatisfaction = clamp01(1 - errors / recent.length);\n }\n\n // v4.9.0: VRAM saturation above 85% presses Safety. Below 85%,\n // saturation has no effect. Scales linearly 85%–100% → sat 1→0.\n let vramSatisfaction = 1;\n if (snap.vramSaturation !== undefined) {\n if (snap.vramSaturation > 0.85) {\n vramSatisfaction = clamp01(1 - (snap.vramSaturation - 0.85) / 0.15);\n }\n }\n\n // v4.9.0: gateway-level telemetry error rate (LLM/tool calls).\n // Independent of CPRun error rate — catches tool failures that\n // never bubbled up to Command Post.\n let telemetrySatisfaction = 1;\n if (snap.telemetryErrorRate !== undefined) {\n telemetrySatisfaction = clamp01(1 - snap.telemetryErrorRate * 2);\n }\n\n // Safety is a min-aggregate — the weakest link dominates.\n const satisfaction = Math.min(\n budgetSatisfaction,\n errorSatisfaction,\n vramSatisfaction,\n telemetrySatisfaction,\n );\n return {\n satisfaction,\n inputs: {\n budgetSatisfaction: Math.round(budgetSatisfaction * 100) / 100,\n errorSatisfaction: Math.round(errorSatisfaction * 100) / 100,\n vramSatisfaction: Math.round(vramSatisfaction * 100) / 100,\n telemetrySatisfaction: Math.round(telemetrySatisfaction * 100) / 100,\n recentRunCount: recent.length,\n vramSaturationPct: snap.vramSaturation !== undefined ? Math.round(snap.vramSaturation * 100) : null,\n telemetryErrorRatePct: snap.telemetryErrorRate !== undefined ? Math.round(snap.telemetryErrorRate * 100) : null,\n },\n };\n },\n describe: (s, inputs) => {\n const budget = (inputs?.budgetSatisfaction as number) ?? 1;\n const errors = (inputs?.errorSatisfaction as number) ?? 1;\n const vram = (inputs?.vramSatisfaction as number) ?? 1;\n const tel = (inputs?.telemetrySatisfaction as number) ?? 1;\n if (budget < 0.2) return 'budget runway critical';\n if (vram < 0.4) return `VRAM saturated (${inputs?.vramSaturationPct}%) — spawns at risk`;\n if (tel < 0.5) return `gateway error rate elevated (${inputs?.telemetryErrorRatePct}%)`;\n if (errors < 0.5) return 'elevated error rate in recent runs';\n if (s < 0.6) return 'safety posture weakening';\n return 'safety posture healthy';\n },\n};\n\nconst SOCIAL: DriveDefinition = {\n id: 'social',\n label: 'Social',\n defaultSetpoint: 0.7,\n weight: 0.7,\n compute: (snap) => {\n // v4.8.1: ignore specialists that were registered but never given\n // work (`totalTasksCompleted === 0`). They have nothing to heartbeat\n // about; counting them as \"unresponsive\" was a false negative.\n const eligible = snap.agents.filter(a => (a.totalTasksCompleted ?? 0) > 0 || a.status === 'active');\n const hourMs = 3_600_000;\n\n // ── Factor 1: agent liveness (legacy) ───────────────────────\n let agentSat = 0.9; // healthy default when no eligible agents\n let stale = 0;\n if (eligible.length > 0) {\n stale = eligible.filter(a =>\n snap.now - new Date(a.lastHeartbeat).getTime() > hourMs,\n ).length;\n agentSat = clamp01(1 - stale / eligible.length);\n }\n\n // ── Factor 2: social-media presence (v5.3.2) ────────────────\n // The README promises a Social drive that asks \"should I post or\n // reply?\" — that requires the drive to actually track posting\n // cadence, not just agent heartbeat. lastFacebookPostAt is wired\n // through buildSnapshot from fb-autopilot-state.json.\n //\n // Saturates at 24h: a drought of 24h+ with no post pulls\n // satisfaction to 0; a fresh post within the last hour keeps it\n // near 1. Linear in between. If lastFacebookPostAt is null/missing\n // (autopilot never ran or never posted), we treat the gap as\n // \"long\" — encourages a first post when a user enables FB.\n const POST_DROUGHT_HOURS = 24;\n let postSat: number;\n let hoursSinceLastPost: number;\n if (snap.lastFacebookPostAt && snap.lastFacebookPostAt > 0) {\n hoursSinceLastPost = Math.max(0, (snap.now - snap.lastFacebookPostAt) / hourMs);\n postSat = clamp01(1 - hoursSinceLastPost / POST_DROUGHT_HOURS);\n } else {\n // Treat \"never posted\" as ~12h drought. Don't peg to 0 —\n // organism shouldn't fire a Soma proposal the moment FB is\n // enabled before the user has even configured anything.\n hoursSinceLastPost = POST_DROUGHT_HOURS / 2;\n postSat = 0.5;\n }\n\n // Equal-weight blend. Either factor low → drive deficits.\n const satisfaction = clamp01((agentSat + postSat) / 2);\n\n return {\n satisfaction,\n inputs: {\n totalAgents: eligible.length,\n staleAgents: stale,\n hoursSinceLastPost: Number(hoursSinceLastPost.toFixed(2)),\n agentSatisfaction: Number(agentSat.toFixed(3)),\n postSatisfaction: Number(postSat.toFixed(3)),\n },\n };\n },\n describe: (_s, inputs) => {\n const total = (inputs?.totalAgents as number) ?? 0;\n const stale = (inputs?.staleAgents as number) ?? 0;\n const hoursSince = (inputs?.hoursSinceLastPost as number) ?? 0;\n const reasons: string[] = [];\n if (stale > 0) reasons.push(`${stale}/${total} agent(s) unresponsive`);\n if (hoursSince >= 12) reasons.push(`${Math.round(hoursSince)}h since last FB post`);\n if (reasons.length === 0) return `${total} agent(s) all alive · posted recently`;\n return reasons.join(' · ');\n },\n};\n\nexport const DRIVES: DriveDefinition[] = [PURPOSE, HUNGER, CURIOSITY, SAFETY, SOCIAL];\n\n// ── Snapshot builder ─────────────────────────────────────────────\n\n/** Build a DriveSnapshot by reading current TITAN state. Synchronous —\n * all inputs are in-memory or cheap disk reads. */\nexport function buildSnapshot(): DriveSnapshot {\n const goals = listGoals();\n let readyTasks: DriveSnapshot['readyTasks'] = [];\n try { readyTasks = getReadyTasks(); } catch { /* empty */ }\n const agents = getRegisteredAgents();\n const budgets = getBudgetPolicies();\n let recentRuns: CPRun[] = [];\n try { recentRuns = listRuns(undefined, 100); } catch { /* empty */ }\n let trajectories: TaskTrajectory[] = [];\n try { trajectories = getRecentTrajectories(100); } catch { /* empty */ }\n\n // v4.9.0 — pull optional closed-loop signals. Each wrapped in try so\n // drive tick never fails if a downstream module is missing or throws.\n\n let vramSaturation: number | undefined;\n try {\n const vr = readCachedVRAMSignal();\n if (vr !== null) vramSaturation = vr;\n } catch { /* no signal */ }\n\n let telemetryErrorRate: number | undefined;\n let telemetryTotalRequests: number | undefined;\n try {\n const metrics = readCachedTelemetrySignal();\n if (metrics) {\n telemetryErrorRate = metrics.errorRate;\n telemetryTotalRequests = metrics.totalRequests;\n }\n } catch { /* no signal */ }\n\n let unresolvedErrorPatterns: number | undefined;\n try {\n const patterns = readUnresolvedErrorPatternCount();\n if (patterns !== null) unresolvedErrorPatterns = patterns;\n } catch { /* no signal */ }\n\n // v5.3.2 Track B: read fb-autopilot's last successful post timestamp so\n // the Social drive's \"social media presence\" factor has real input.\n // Best-effort: never throws — Social drive falls back to a neutral\n // \"12h drought\" when this is absent (see SOCIAL.compute).\n let lastFacebookPostAt: number | null = null;\n try {\n const fbStatePath = join(TITAN_HOME, 'fb-autopilot-state.json');\n if (existsSync(fbStatePath)) {\n const raw = readFileSync(fbStatePath, 'utf-8');\n const state = JSON.parse(raw) as { lastPostAt?: string | null };\n if (state.lastPostAt) {\n const parsed = new Date(state.lastPostAt).getTime();\n if (Number.isFinite(parsed)) lastFacebookPostAt = parsed;\n }\n }\n } catch { /* ok — autopilot state missing or malformed; fall back */ }\n\n return {\n now: Date.now(),\n goals,\n readyTasks,\n recentRuns,\n budgets,\n agents,\n trajectories,\n vramSaturation,\n telemetryErrorRate,\n telemetryTotalRequests,\n unresolvedErrorPatterns,\n lastFacebookPostAt,\n };\n}\n\n// ── v4.9.0 signal readers ──────────────────────────────────────────\n\n/**\n * Reads the VRAM orchestrator's last cached snapshot (no refresh) and\n * returns used/total saturation as 0–1. Returns null when no GPU is\n * attached or the orchestrator hasn't polled yet.\n *\n * Synchronous: buildSnapshot() is called in the drive-tick hot path\n * every 60s, and we don't want to add an async nvidia-smi probe on\n * top of the existing 10s VRAM refresh.\n */\nfunction readCachedVRAMSignal(): number | null {\n try {\n // Dynamic require-like import from the already-loaded module\n // singleton. If VRAM module hasn't been initialized (e.g., in\n // tests), just return null.\n const mod = (globalThis as unknown as { __titan_vram_last?: { freeMB?: number; totalMB?: number; usedMB?: number } }).__titan_vram_last;\n if (!mod) return null;\n const total = mod.totalMB ?? 0;\n if (!Number.isFinite(total) || total <= 0) return null;\n const used = Number.isFinite(mod.usedMB) ? mod.usedMB! : (total - (mod.freeMB ?? total));\n const pct = used / total;\n if (!Number.isFinite(pct)) return null;\n return Math.max(0, Math.min(1, pct));\n } catch {\n return null;\n }\n}\n\n/** Reads the gateway metrics layer's summary (sync, in-memory). */\nfunction readCachedTelemetrySignal(): { errorRate: number; totalRequests: number } | null {\n try {\n // Using require-style resolve so tests that mock the drives\n // module don't pull in the metrics graph.\n const mod = (globalThis as unknown as { __titan_metrics_summary?: () => { totalRequests?: number; errorRate?: number } | null }).__titan_metrics_summary;\n if (typeof mod !== 'function') return null;\n const s = mod();\n if (!s || typeof s.totalRequests !== 'number' || typeof s.errorRate !== 'number') return null;\n // Only treat the signal as meaningful once we have enough samples.\n if (s.totalRequests < 10) return null;\n return { errorRate: s.errorRate, totalRequests: s.totalRequests };\n } catch {\n return null;\n }\n}\n\n/** Reads count of unresolved error patterns from the learning layer. */\nfunction readUnresolvedErrorPatternCount(): number | null {\n try {\n const mod = (globalThis as unknown as { __titan_unresolved_error_patterns?: () => number }).__titan_unresolved_error_patterns;\n if (typeof mod !== 'function') return null;\n const n = mod();\n if (typeof n !== 'number' || !Number.isFinite(n)) return null;\n return n;\n } catch {\n return null;\n }\n}\n\n// ── Drive state computation ──────────────────────────────────────\n\n/** Compute all drive states for a given snapshot, applying per-drive\n * setpoint + weight overrides + disabled-drive filter (all from\n * config.organism.{driveSetpoints,driveWeights,disabledDrives}). */\nexport function computeAllDrives(\n snapshot: DriveSnapshot,\n setpointOverrides: Partial<Record<DriveId, number>> = {},\n weightOverrides: Partial<Record<DriveId, number>> = {},\n disabledDrives: DriveId[] = [],\n): DriveState[] {\n const out: DriveState[] = [];\n const disabled = new Set(disabledDrives);\n for (const def of DRIVES) {\n if (disabled.has(def.id)) continue;\n const { satisfaction, inputs } = def.compute(snapshot);\n const setpoint = setpointOverrides[def.id] ?? def.defaultSetpoint;\n const weight = weightOverrides[def.id] ?? def.weight;\n const pressure = satisfaction < setpoint\n ? (setpoint - satisfaction) * weight\n : 0;\n out.push({\n id: def.id,\n label: def.label,\n satisfaction: clamp01(satisfaction),\n setpoint,\n pressure,\n weight,\n inputs,\n description: def.describe(satisfaction, inputs),\n });\n }\n return out;\n}\n\n// ── Persistence ──────────────────────────────────────────────────\n\nexport interface PersistedDriveHistory {\n latest: DriveTickResult;\n /** Ring buffer of last ≤1440 ticks (~24h at 60s cadence). */\n history: Array<{ timestamp: string; satisfactions: Record<DriveId, number> }>;\n}\n\n/** Load the last-written drive state (if any). Returns null on first run. */\nexport function loadDriveHistory(): PersistedDriveHistory | null {\n if (!existsSync(DRIVE_STATE_PATH)) return null;\n try {\n return JSON.parse(readFileSync(DRIVE_STATE_PATH, 'utf-8')) as PersistedDriveHistory;\n } catch (err) {\n logger.warn(COMPONENT, `drive-state.json corrupt: ${(err as Error).message}`);\n return null;\n }\n}\n\n/** Persist the tick. Ring-buffers history to a max of 1440 entries. */\nexport function saveDriveTick(tick: DriveTickResult): void {\n try {\n ensureDir(TITAN_HOME);\n const existing = loadDriveHistory();\n const satisfactions: Record<string, number> = {};\n for (const d of tick.drives) satisfactions[d.id] = d.satisfaction;\n const history = (existing?.history || []).concat([{\n timestamp: tick.timestamp,\n satisfactions: satisfactions as Record<DriveId, number>,\n }]);\n const trimmed = history.length > 1440 ? history.slice(-1440) : history;\n const payload: PersistedDriveHistory = { latest: tick, history: trimmed };\n writeFileSync(DRIVE_STATE_PATH, JSON.stringify(payload, null, 2), 'utf-8');\n } catch (err) {\n logger.warn(COMPONENT, `Failed to save drive state: ${(err as Error).message}`);\n }\n}\n\n// ── One-call convenience ─────────────────────────────────────────\n\n/** Build snapshot → compute drives → package as a DriveTickResult. Does NOT\n * persist; callers decide whether to save (daemon tick does; read-only API\n * endpoints don't). */\nexport function runDriveTick(\n setpointOverrides: Partial<Record<DriveId, number>> = {},\n weightOverrides: Partial<Record<DriveId, number>> = {},\n disabledDrives: DriveId[] = [],\n): DriveTickResult {\n const snapshot = buildSnapshot();\n const drives = computeAllDrives(snapshot, setpointOverrides, weightOverrides, disabledDrives);\n const totalPressure = drives.reduce((sum, d) => sum + d.pressure, 0);\n const dominantDrives = drives\n .filter(d => d.pressure > 0)\n .sort((a, b) => b.pressure - a.pressure)\n .slice(0, 2)\n .map(d => d.id);\n return {\n timestamp: new Date().toISOString(),\n drives,\n totalPressure,\n dominantDrives,\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/organism/drives.ts"],"sourcesContent":["/**\n * TITAN — Drive Layer (Soma organism / endocrine system)\n *\n * Five homeostatic drives. Each computes a 0-1 \"satisfaction\" from existing\n * TITAN telemetry — no new instrumentation. When satisfaction dips below the\n * drive's setpoint, pressure accumulates. Cross-drive pressure fusion (see\n * pressure.ts) eventually produces a soma_proposal for human approval.\n *\n * Gated by config.organism.enabled — this module is inert when disabled.\n *\n * DRIVES SHIPPED IN v4.0:\n * Purpose — alignment with priority-1 goals\n * Hunger — backlog size vs. throughput\n * Curiosity — task-type diversity in recent trajectories\n * Safety — budget runway + recent error rate\n * Social — stale agent fraction\n *\n * DEFERRED TO v4.1+:\n * Hygiene — needs npm test + git status shell hooks\n */\nimport { existsSync, readFileSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport { mkdirIfNotExists } from '../utils/helpers.js';\nimport { listGoals, getReadyTasks, type Goal } from '../agent/goals.js';\nimport { getRegisteredAgents, getBudgetPolicies, listRuns, type RegisteredAgent, type BudgetPolicy, type CPRun } from '../agent/commandPost.js';\nimport { getRecentTrajectories, type TaskTrajectory } from '../agent/trajectoryLogger.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Drives';\nconst DRIVE_STATE_PATH = join(TITAN_HOME, 'drive-state.json');\n\n// ── Types ────────────────────────────────────────────────────────\n\nexport type DriveId = 'purpose' | 'hunger' | 'curiosity' | 'safety' | 'social';\n\nexport interface DriveSnapshot {\n /** Timestamp of the snapshot in epoch ms. */\n now: number;\n /** All goals from goals.ts. */\n goals: Goal[];\n /** Output of getReadyTasks() — ready-to-execute subtasks. */\n readyTasks: Array<{ goal: Goal; subtask: Goal['subtasks'][number] }>;\n /** Recent CPRun history (up to 100 most recent). */\n recentRuns: CPRun[];\n /** Active budget policies. */\n budgets: BudgetPolicy[];\n /** All registered agents. */\n agents: RegisteredAgent[];\n /** Last 100 trajectory entries. */\n trajectories: TaskTrajectory[];\n /**\n * v4.9.0: fraction of GPU VRAM in use (0–1). Undefined when no GPU\n * is attached or the orchestrator hasn't refreshed yet.\n */\n vramSaturation?: number;\n /**\n * v4.9.0: error rate across recent LLM / tool calls from the\n * gateway metrics layer (0–1). Undefined when metrics are unavailable.\n */\n telemetryErrorRate?: number;\n /** v4.9.0: total LLM + tool-call requests since gateway start. */\n telemetryTotalRequests?: number;\n /**\n * v4.9.0: count of error patterns the learning layer has accumulated\n * but not yet resolved. High count pulls Curiosity toward an\n * investigate/improve proposal.\n */\n unresolvedErrorPatterns?: number;\n /**\n * v5.3.2 (Phase 8 / Track B): timestamp of the most recent successful\n * Facebook page post in epoch ms, or null if TITAN hasn't posted yet\n * since fb-autopilot was enabled. Sourced from\n * `~/.titan/fb-autopilot-state.json`. Drives the Social-drive's\n * \"social media presence\" factor — without this, social pressure was\n * 100% about agent heartbeat staleness, which the README implied was\n * about social posting. Now both factors blend.\n */\n lastFacebookPostAt?: number | null;\n}\n\nexport interface DriveDefinition {\n id: DriveId;\n label: string;\n /** Satisfaction level below which this drive starts contributing pressure. */\n defaultSetpoint: number;\n /** Relative weight in cross-drive pressure fusion (1.0 is baseline). */\n weight: number;\n /** Pure function — computes satisfaction 0-1 from the snapshot. */\n compute: (snapshot: DriveSnapshot) => { satisfaction: number; inputs?: Record<string, unknown> };\n /** Short human-readable explanation used in prompts, UI tooltips, and activity feed. */\n describe: (satisfaction: number, inputs?: Record<string, unknown>) => string;\n}\n\nexport interface DriveState {\n id: DriveId;\n label: string;\n satisfaction: number;\n setpoint: number;\n /** 0 when satisfaction >= setpoint, else (setpoint − satisfaction) × weight. */\n pressure: number;\n weight: number;\n inputs?: Record<string, unknown>;\n description: string;\n}\n\nexport interface DriveTickResult {\n timestamp: string;\n drives: DriveState[];\n totalPressure: number;\n dominantDrives: DriveId[];\n}\n\n// ── Numeric helpers ──────────────────────────────────────────────\n\n/** Clamp to [0,1]. */\nfunction clamp01(v: number): number {\n if (!Number.isFinite(v)) return 0;\n return Math.max(0, Math.min(1, v));\n}\n\n/** Sigmoid centred on `mid` with slope `k`. Returns high → 1 when x is low. */\nfunction invertedSigmoid(x: number, mid: number, k = 1): number {\n return clamp01(1 / (1 + Math.exp(k * (x - mid))));\n}\n\n/** Gini coefficient of a count distribution. 0 = uniform, 1 = all same task. */\nfunction gini(counts: number[]): number {\n if (counts.length === 0) return 0;\n const n = counts.length;\n const sum = counts.reduce((a, b) => a + b, 0);\n if (sum === 0) return 0;\n const sorted = [...counts].sort((a, b) => a - b);\n let cum = 0;\n for (let i = 0; i < n; i++) cum += (i + 1) * sorted[i];\n return clamp01((2 * cum) / (n * sum) - (n + 1) / n);\n}\n\n// ── Drive definitions ────────────────────────────────────────────\n\nconst PURPOSE: DriveDefinition = {\n id: 'purpose',\n label: 'Purpose',\n defaultSetpoint: 0.7,\n weight: 1.4,\n compute: (snap) => {\n // Priority-1 goals tagged as high-priority. Satisfaction reflects how\n // recently any of them progressed. No priority-1 goals → satiated\n // (nothing to worry about).\n const priorityOne = snap.goals.filter(g =>\n g.status === 'active' && g.priority === 1,\n );\n if (priorityOne.length === 0) {\n return { satisfaction: 0.9, inputs: { priorityOneCount: 0 } };\n }\n const latest = Math.max(...priorityOne.map(g =>\n new Date(g.updatedAt || g.createdAt).getTime(),\n ));\n const hoursSince = Math.max(0, (snap.now - latest) / 3_600_000);\n const satisfaction = clamp01(1 - hoursSince / 24);\n return {\n satisfaction,\n inputs: { priorityOneCount: priorityOne.length, hoursSinceProgress: Math.round(hoursSince * 10) / 10 },\n };\n },\n describe: (s, inputs) => {\n const count = (inputs?.priorityOneCount as number) ?? 0;\n if (count === 0) return 'no priority-1 goals in flight';\n const hours = (inputs?.hoursSinceProgress as number) ?? 0;\n if (s < 0.3) return `${count} priority-1 goal(s) stalled — no progress in ${hours.toFixed(1)}h`;\n if (s < 0.6) return `${count} priority-1 goal(s) need attention`;\n return `${count} priority-1 goal(s) on track`;\n },\n};\n\nconst HUNGER: DriveDefinition = {\n id: 'hunger',\n label: 'Hunger',\n defaultSetpoint: 0.6,\n weight: 1.0,\n compute: (snap) => {\n const readyCount = snap.readyTasks.length;\n // Oldest ready subtask age in hours, using parent goal createdAt as proxy.\n const oldestAgeHours = snap.readyTasks.length === 0\n ? 0\n : Math.max(...snap.readyTasks.map(r =>\n (snap.now - new Date(r.goal.createdAt).getTime()) / 3_600_000,\n ));\n // Both signals independently drag satisfaction down.\n // v5.0.0: floor backlog satisfaction at 0.15 so extreme backlogs\n // (e.g. 1000+ zombie goals) don't drive hunger to absolute zero,\n // which causes SOMA to panic-propose even more goals.\n const backlogSatisfaction = Math.max(0.15, invertedSigmoid(readyCount, 5, 0.35));\n const ageSatisfaction = invertedSigmoid(oldestAgeHours, 4, 0.5);\n const satisfaction = Math.min(backlogSatisfaction, ageSatisfaction);\n return {\n satisfaction,\n inputs: { readyCount, oldestAgeHours: Math.round(oldestAgeHours * 10) / 10 },\n };\n },\n describe: (s, inputs) => {\n const count = (inputs?.readyCount as number) ?? 0;\n const age = (inputs?.oldestAgeHours as number) ?? 0;\n if (count === 0) return 'backlog empty';\n if (s < 0.3) return `backlog ${count}, oldest ${age.toFixed(1)}h — elevated`;\n if (s < 0.6) return `backlog ${count}, oldest ${age.toFixed(1)}h`;\n return `backlog ${count} — fed`;\n },\n};\n\nconst CURIOSITY: DriveDefinition = {\n id: 'curiosity',\n label: 'Curiosity',\n defaultSetpoint: 0.5,\n weight: 0.8,\n compute: (snap) => {\n // Novelty = task-type diversity across recent trajectories.\n // Few distinct task types → elevated curiosity (stale). Rich variety\n // → satiated. We compose two signals:\n // 1) coverage: how many distinct types relative to a target of 5\n // 2) balance: how evenly distributed those types are (1 − gini)\n // Satisfaction = min(coverage, balance) so either deficit pulls it\n // down. Low sample counts default to middling satisfaction.\n if (snap.trajectories.length < 5) {\n return { satisfaction: 0.6, inputs: { trajectoryCount: snap.trajectories.length } };\n }\n const typeCounts: Record<string, number> = {};\n for (const t of snap.trajectories) {\n typeCounts[t.taskType || 'unknown'] = (typeCounts[t.taskType || 'unknown'] || 0) + 1;\n }\n const typeCount = Object.keys(typeCounts).length;\n const coverage = clamp01(typeCount / 5);\n const counts = Object.values(typeCounts);\n const balance = typeCount <= 1 ? 0 : clamp01(1 - gini(counts));\n const diversitySat = typeCount <= 1 ? coverage : Math.min(coverage, balance);\n\n // v4.9.0: unresolved error patterns are a form of \"task-type\n // novelty the organism hasn't figured out yet.\" More than a\n // handful of unresolved patterns pulls Curiosity toward an\n // investigate-and-improve proposal (feeds Self-Improve pipeline).\n // Scales 0→10+ patterns linearly.\n let errorPatternSat = 1;\n if (typeof snap.unresolvedErrorPatterns === 'number' && snap.unresolvedErrorPatterns > 2) {\n errorPatternSat = clamp01(1 - (snap.unresolvedErrorPatterns - 2) / 10);\n }\n\n const satisfaction = Math.min(diversitySat, errorPatternSat);\n return {\n satisfaction,\n inputs: {\n trajectoryCount: snap.trajectories.length,\n taskTypes: typeCount,\n coverage: Math.round(coverage * 100) / 100,\n balance: Math.round(balance * 100) / 100,\n unresolvedErrorPatterns: snap.unresolvedErrorPatterns ?? 0,\n errorPatternSat: Math.round(errorPatternSat * 100) / 100,\n },\n };\n },\n describe: (s, inputs) => {\n const types = (inputs?.taskTypes as number) ?? 0;\n const patterns = (inputs?.unresolvedErrorPatterns as number) ?? 0;\n if (patterns >= 5) return `${patterns} unresolved error patterns — needs investigation`;\n if (s < 0.3) return `stuck in ${types} task type(s) — stale`;\n if (s < 0.6) return `${types} task type(s) — could use novelty`;\n return `${types} distinct task type(s) — engaged`;\n },\n};\n\nconst SAFETY: DriveDefinition = {\n id: 'safety',\n label: 'Safety',\n defaultSetpoint: 0.8,\n weight: 1.6,\n compute: (snap) => {\n // Budget runway: min runway across all enabled budgets.\n let budgetSatisfaction = 1;\n const relevantBudgets = snap.budgets.filter(b => b.enabled && b.limitUsd > 0);\n if (relevantBudgets.length > 0) {\n const runways = relevantBudgets.map(b => clamp01(1 - b.currentSpend / b.limitUsd));\n budgetSatisfaction = Math.min(...runways);\n }\n // Recent error rate from last 100 CPRuns in the last 24h.\n const dayMs = 86_400_000;\n const recent = snap.recentRuns.filter(r =>\n snap.now - new Date(r.startedAt).getTime() < dayMs,\n );\n let errorSatisfaction = 1;\n if (recent.length >= 5) {\n const errors = recent.filter(r => r.status === 'error' || r.status === 'failed').length;\n errorSatisfaction = clamp01(1 - errors / recent.length);\n }\n\n // v4.9.0: VRAM saturation above 85% presses Safety. Below 85%,\n // saturation has no effect. Scales linearly 85%–100% → sat 1→0.\n let vramSatisfaction = 1;\n if (snap.vramSaturation !== undefined) {\n if (snap.vramSaturation > 0.85) {\n vramSatisfaction = clamp01(1 - (snap.vramSaturation - 0.85) / 0.15);\n }\n }\n\n // v4.9.0: gateway-level telemetry error rate (LLM/tool calls).\n // Independent of CPRun error rate — catches tool failures that\n // never bubbled up to Command Post.\n let telemetrySatisfaction = 1;\n if (snap.telemetryErrorRate !== undefined) {\n telemetrySatisfaction = clamp01(1 - snap.telemetryErrorRate * 2);\n }\n\n // Safety is a min-aggregate — the weakest link dominates.\n const satisfaction = Math.min(\n budgetSatisfaction,\n errorSatisfaction,\n vramSatisfaction,\n telemetrySatisfaction,\n );\n return {\n satisfaction,\n inputs: {\n budgetSatisfaction: Math.round(budgetSatisfaction * 100) / 100,\n errorSatisfaction: Math.round(errorSatisfaction * 100) / 100,\n vramSatisfaction: Math.round(vramSatisfaction * 100) / 100,\n telemetrySatisfaction: Math.round(telemetrySatisfaction * 100) / 100,\n recentRunCount: recent.length,\n vramSaturationPct: snap.vramSaturation !== undefined ? Math.round(snap.vramSaturation * 100) : null,\n telemetryErrorRatePct: snap.telemetryErrorRate !== undefined ? Math.round(snap.telemetryErrorRate * 100) : null,\n },\n };\n },\n describe: (s, inputs) => {\n const budget = (inputs?.budgetSatisfaction as number) ?? 1;\n const errors = (inputs?.errorSatisfaction as number) ?? 1;\n const vram = (inputs?.vramSatisfaction as number) ?? 1;\n const tel = (inputs?.telemetrySatisfaction as number) ?? 1;\n if (budget < 0.2) return 'budget runway critical';\n if (vram < 0.4) return `VRAM saturated (${inputs?.vramSaturationPct}%) — spawns at risk`;\n if (tel < 0.5) return `gateway error rate elevated (${inputs?.telemetryErrorRatePct}%)`;\n if (errors < 0.5) return 'elevated error rate in recent runs';\n if (s < 0.6) return 'safety posture weakening';\n return 'safety posture healthy';\n },\n};\n\nconst SOCIAL: DriveDefinition = {\n id: 'social',\n label: 'Social',\n defaultSetpoint: 0.7,\n weight: 0.7,\n compute: (snap) => {\n // v4.8.1: ignore specialists that were registered but never given\n // work (`totalTasksCompleted === 0`). They have nothing to heartbeat\n // about; counting them as \"unresponsive\" was a false negative.\n const eligible = snap.agents.filter(a => (a.totalTasksCompleted ?? 0) > 0 || a.status === 'active');\n const hourMs = 3_600_000;\n\n // ── Factor 1: agent liveness (legacy) ───────────────────────\n let agentSat = 0.9; // healthy default when no eligible agents\n let stale = 0;\n if (eligible.length > 0) {\n stale = eligible.filter(a =>\n snap.now - new Date(a.lastHeartbeat).getTime() > hourMs,\n ).length;\n agentSat = clamp01(1 - stale / eligible.length);\n }\n\n // ── Factor 2: social-media presence (v5.3.2) ────────────────\n // The README promises a Social drive that asks \"should I post or\n // reply?\" — that requires the drive to actually track posting\n // cadence, not just agent heartbeat. lastFacebookPostAt is wired\n // through buildSnapshot from fb-autopilot-state.json.\n //\n // Saturates at 24h: a drought of 24h+ with no post pulls\n // satisfaction to 0; a fresh post within the last hour keeps it\n // near 1. Linear in between. If lastFacebookPostAt is null/missing\n // (autopilot never ran or never posted), we treat the gap as\n // \"long\" — encourages a first post when a user enables FB.\n const POST_DROUGHT_HOURS = 24;\n let postSat: number;\n let hoursSinceLastPost: number;\n if (snap.lastFacebookPostAt && snap.lastFacebookPostAt > 0) {\n hoursSinceLastPost = Math.max(0, (snap.now - snap.lastFacebookPostAt) / hourMs);\n postSat = clamp01(1 - hoursSinceLastPost / POST_DROUGHT_HOURS);\n } else {\n // Treat \"never posted\" as ~12h drought. Don't peg to 0 —\n // organism shouldn't fire a Soma proposal the moment FB is\n // enabled before the user has even configured anything.\n hoursSinceLastPost = POST_DROUGHT_HOURS / 2;\n postSat = 0.5;\n }\n\n // Equal-weight blend. Either factor low → drive deficits.\n const satisfaction = clamp01((agentSat + postSat) / 2);\n\n return {\n satisfaction,\n inputs: {\n totalAgents: eligible.length,\n staleAgents: stale,\n hoursSinceLastPost: Number(hoursSinceLastPost.toFixed(2)),\n agentSatisfaction: Number(agentSat.toFixed(3)),\n postSatisfaction: Number(postSat.toFixed(3)),\n },\n };\n },\n describe: (_s, inputs) => {\n const total = (inputs?.totalAgents as number) ?? 0;\n const stale = (inputs?.staleAgents as number) ?? 0;\n const hoursSince = (inputs?.hoursSinceLastPost as number) ?? 0;\n const reasons: string[] = [];\n if (stale > 0) reasons.push(`${stale}/${total} agent(s) unresponsive`);\n if (hoursSince >= 12) reasons.push(`${Math.round(hoursSince)}h since last FB post`);\n if (reasons.length === 0) return `${total} agent(s) all alive · posted recently`;\n return reasons.join(' · ');\n },\n};\n\nexport const DRIVES: DriveDefinition[] = [PURPOSE, HUNGER, CURIOSITY, SAFETY, SOCIAL];\n\n// ── Snapshot builder ─────────────────────────────────────────────\n\n/** Build a DriveSnapshot by reading current TITAN state. Synchronous —\n * all inputs are in-memory or cheap disk reads. */\nexport function buildSnapshot(): DriveSnapshot {\n const goals = listGoals();\n let readyTasks: DriveSnapshot['readyTasks'] = [];\n try { readyTasks = getReadyTasks(); } catch { /* empty */ }\n const agents = getRegisteredAgents();\n const budgets = getBudgetPolicies();\n let recentRuns: CPRun[] = [];\n try { recentRuns = listRuns(undefined, 100); } catch { /* empty */ }\n let trajectories: TaskTrajectory[] = [];\n try { trajectories = getRecentTrajectories(100); } catch { /* empty */ }\n\n // v4.9.0 — pull optional closed-loop signals. Each wrapped in try so\n // drive tick never fails if a downstream module is missing or throws.\n\n let vramSaturation: number | undefined;\n try {\n const vr = readCachedVRAMSignal();\n if (vr !== null) vramSaturation = vr;\n } catch { /* no signal */ }\n\n let telemetryErrorRate: number | undefined;\n let telemetryTotalRequests: number | undefined;\n try {\n const metrics = readCachedTelemetrySignal();\n if (metrics) {\n telemetryErrorRate = metrics.errorRate;\n telemetryTotalRequests = metrics.totalRequests;\n }\n } catch { /* no signal */ }\n\n let unresolvedErrorPatterns: number | undefined;\n try {\n const patterns = readUnresolvedErrorPatternCount();\n if (patterns !== null) unresolvedErrorPatterns = patterns;\n } catch { /* no signal */ }\n\n // v5.3.2 Track B: read fb-autopilot's last successful post timestamp so\n // the Social drive's \"social media presence\" factor has real input.\n // Best-effort: never throws — Social drive falls back to a neutral\n // \"12h drought\" when this is absent (see SOCIAL.compute).\n let lastFacebookPostAt: number | null = null;\n try {\n const fbStatePath = join(TITAN_HOME, 'fb-autopilot-state.json');\n if (existsSync(fbStatePath)) {\n const raw = readFileSync(fbStatePath, 'utf-8');\n const state = JSON.parse(raw) as { lastPostAt?: string | null };\n if (state.lastPostAt) {\n const parsed = new Date(state.lastPostAt).getTime();\n if (Number.isFinite(parsed)) lastFacebookPostAt = parsed;\n }\n }\n } catch { /* ok — autopilot state missing or malformed; fall back */ }\n\n return {\n now: Date.now(),\n goals,\n readyTasks,\n recentRuns,\n budgets,\n agents,\n trajectories,\n vramSaturation,\n telemetryErrorRate,\n telemetryTotalRequests,\n unresolvedErrorPatterns,\n lastFacebookPostAt,\n };\n}\n\n// ── v4.9.0 signal readers ──────────────────────────────────────────\n\n/**\n * Reads the VRAM orchestrator's last cached snapshot (no refresh) and\n * returns used/total saturation as 0–1. Returns null when no GPU is\n * attached or the orchestrator hasn't polled yet.\n *\n * Synchronous: buildSnapshot() is called in the drive-tick hot path\n * every 60s, and we don't want to add an async nvidia-smi probe on\n * top of the existing 10s VRAM refresh.\n */\nfunction readCachedVRAMSignal(): number | null {\n try {\n // Dynamic require-like import from the already-loaded module\n // singleton. If VRAM module hasn't been initialized (e.g., in\n // tests), just return null.\n const mod = (globalThis as unknown as { __titan_vram_last?: { freeMB?: number; totalMB?: number; usedMB?: number } }).__titan_vram_last;\n if (!mod) return null;\n const total = mod.totalMB ?? 0;\n if (!Number.isFinite(total) || total <= 0) return null;\n const used = Number.isFinite(mod.usedMB) ? mod.usedMB! : (total - (mod.freeMB ?? total));\n const pct = used / total;\n if (!Number.isFinite(pct)) return null;\n return Math.max(0, Math.min(1, pct));\n } catch {\n return null;\n }\n}\n\n/** Reads the gateway metrics layer's summary (sync, in-memory). */\nfunction readCachedTelemetrySignal(): { errorRate: number; totalRequests: number } | null {\n try {\n // Using require-style resolve so tests that mock the drives\n // module don't pull in the metrics graph.\n const mod = (globalThis as unknown as { __titan_metrics_summary?: () => { totalRequests?: number; errorRate?: number } | null }).__titan_metrics_summary;\n if (typeof mod !== 'function') return null;\n const s = mod();\n if (!s || typeof s.totalRequests !== 'number' || typeof s.errorRate !== 'number') return null;\n // Only treat the signal as meaningful once we have enough samples.\n if (s.totalRequests < 10) return null;\n return { errorRate: s.errorRate, totalRequests: s.totalRequests };\n } catch {\n return null;\n }\n}\n\n/** Reads count of unresolved error patterns from the learning layer. */\nfunction readUnresolvedErrorPatternCount(): number | null {\n try {\n const mod = (globalThis as unknown as { __titan_unresolved_error_patterns?: () => number }).__titan_unresolved_error_patterns;\n if (typeof mod !== 'function') return null;\n const n = mod();\n if (typeof n !== 'number' || !Number.isFinite(n)) return null;\n return n;\n } catch {\n return null;\n }\n}\n\n// ── Drive state computation ──────────────────────────────────────\n\n/** Compute all drive states for a given snapshot, applying per-drive\n * setpoint + weight overrides + disabled-drive filter (all from\n * config.organism.{driveSetpoints,driveWeights,disabledDrives}). */\nexport function computeAllDrives(\n snapshot: DriveSnapshot,\n setpointOverrides: Partial<Record<DriveId, number>> = {},\n weightOverrides: Partial<Record<DriveId, number>> = {},\n disabledDrives: DriveId[] = [],\n): DriveState[] {\n const out: DriveState[] = [];\n const disabled = new Set(disabledDrives);\n for (const def of DRIVES) {\n if (disabled.has(def.id)) continue;\n const { satisfaction, inputs } = def.compute(snapshot);\n const setpoint = setpointOverrides[def.id] ?? def.defaultSetpoint;\n const weight = weightOverrides[def.id] ?? def.weight;\n const pressure = satisfaction < setpoint\n ? (setpoint - satisfaction) * weight\n : 0;\n out.push({\n id: def.id,\n label: def.label,\n satisfaction: clamp01(satisfaction),\n setpoint,\n pressure,\n weight,\n inputs,\n description: def.describe(satisfaction, inputs),\n });\n }\n return out;\n}\n\n// ── Persistence ──────────────────────────────────────────────────\n\nexport interface PersistedDriveHistory {\n latest: DriveTickResult;\n /** Ring buffer of last ≤1440 ticks (~24h at 60s cadence). */\n history: Array<{ timestamp: string; satisfactions: Record<DriveId, number> }>;\n}\n\n/** Load the last-written drive state (if any). Returns null on first run. */\nexport function loadDriveHistory(): PersistedDriveHistory | null {\n if (!existsSync(DRIVE_STATE_PATH)) return null;\n try {\n return JSON.parse(readFileSync(DRIVE_STATE_PATH, 'utf-8')) as PersistedDriveHistory;\n } catch (err) {\n logger.warn(COMPONENT, `drive-state.json corrupt: ${(err as Error).message}`);\n return null;\n }\n}\n\n/** Persist the tick. Ring-buffers history to a max of 1440 entries. */\nexport function saveDriveTick(tick: DriveTickResult): void {\n try {\n mkdirIfNotExists(TITAN_HOME);\n const existing = loadDriveHistory();\n const satisfactions: Record<string, number> = {};\n for (const d of tick.drives) satisfactions[d.id] = d.satisfaction;\n const history = (existing?.history || []).concat([{\n timestamp: tick.timestamp,\n satisfactions: satisfactions as Record<DriveId, number>,\n }]);\n const trimmed = history.length > 1440 ? history.slice(-1440) : history;\n const payload: PersistedDriveHistory = { latest: tick, history: trimmed };\n writeFileSync(DRIVE_STATE_PATH, JSON.stringify(payload, null, 2), 'utf-8');\n } catch (err) {\n logger.warn(COMPONENT, `Failed to save drive state: ${(err as Error).message}`);\n }\n}\n\n// ── One-call convenience ─────────────────────────────────────────\n\n/** Build snapshot → compute drives → package as a DriveTickResult. Does NOT\n * persist; callers decide whether to save (daemon tick does; read-only API\n * endpoints don't). */\nexport function runDriveTick(\n setpointOverrides: Partial<Record<DriveId, number>> = {},\n weightOverrides: Partial<Record<DriveId, number>> = {},\n disabledDrives: DriveId[] = [],\n): DriveTickResult {\n const snapshot = buildSnapshot();\n const drives = computeAllDrives(snapshot, setpointOverrides, weightOverrides, disabledDrives);\n const totalPressure = drives.reduce((sum, d) => sum + d.pressure, 0);\n const dominantDrives = drives\n .filter(d => d.pressure > 0)\n .sort((a, b) => b.pressure - a.pressure)\n .slice(0, 2)\n .map(d => d.id);\n return {\n timestamp: new Date().toISOString(),\n drives,\n totalPressure,\n dominantDrives,\n };\n}\n"],"mappings":"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","names":[]}
@@ -15,6 +15,7 @@ var FailoverReason = /* @__PURE__ */ ((FailoverReason2) => {
15
15
  FailoverReason2["OVERLOADED"] = "overloaded";
16
16
  FailoverReason2["EMPTY_RESPONSE"] = "empty_response";
17
17
  FailoverReason2["FORMAT_ERROR"] = "format_error";
18
+ FailoverReason2["THINKING_NOT_SUPPORTED"] = "thinking_not_supported";
18
19
  FailoverReason2["UNKNOWN"] = "unknown";
19
20
  return FailoverReason2;
20
21
  })(FailoverReason || {});
@@ -111,6 +112,15 @@ const RECOVERY_PROFILES = {
111
112
  shouldFallback: false,
112
113
  cooldownMs: 0
113
114
  },
115
+ ["thinking_not_supported" /* THINKING_NOT_SUPPORTED */]: {
116
+ // Retry once on the same provider after stripping thinking options.
117
+ // The router handles the option mutation; after one retry we fall through.
118
+ retryable: true,
119
+ shouldCompress: false,
120
+ shouldRotateCredential: false,
121
+ shouldFallback: false,
122
+ cooldownMs: 0
123
+ },
114
124
  ["unknown" /* UNKNOWN */]: {
115
125
  retryable: false,
116
126
  shouldCompress: false,
@@ -196,6 +206,9 @@ function classifyProviderError(error) {
196
206
  if (body.includes("content_filter") || body.includes("content filter") || body.includes("safety") || body.includes("harmful") || body.includes("flagged") || body.includes("blocked")) {
197
207
  return buildResult("content_filtered" /* CONTENT_FILTERED */, status, msg);
198
208
  }
209
+ if (body.includes("does not support thinking") || body.includes("thinking is not supported") || body.includes("extended-thinking") || body.includes("think_mode") || body.includes("budget_tokens") || body.includes("thinking_mode")) {
210
+ return buildResult("thinking_not_supported" /* THINKING_NOT_SUPPORTED */, status, msg);
211
+ }
199
212
  if (body.includes("invalid") || body.includes("malformed") || body.includes("parse") || body.includes("json")) {
200
213
  return buildResult("format_error" /* FORMAT_ERROR */, status, msg);
201
214
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/providers/errorTaxonomy.ts"],"sourcesContent":["/**\n * TITAN — Centralized Error Taxonomy\n *\n * Replaces scattered error handling across router.ts, toolRunner.ts, and server.ts\n * with a single 7-step classification pipeline. Inspired by Hermes error_classifier.py.\n *\n * Each provider error is classified into a FailoverReason with structured recovery hints\n * (retryable, shouldCompress, shouldRotateCredential, shouldFallback, cooldownMs).\n */\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'ErrorTaxonomy';\n\n// ── Error Categories ──────────────────────────────────────────────\nexport enum FailoverReason {\n RATE_LIMIT = 'rate_limit',\n QUOTA_EXCEEDED = 'quota_exceeded',\n CONTEXT_OVERFLOW = 'context_overflow',\n AUTH_INVALID = 'auth_invalid',\n AUTH_EXPIRED = 'auth_expired',\n MODEL_NOT_FOUND = 'model_not_found',\n CONTENT_FILTERED = 'content_filtered',\n SERVER_ERROR = 'server_error',\n TIMEOUT = 'timeout',\n NETWORK_ERROR = 'network_error',\n OVERLOADED = 'overloaded',\n EMPTY_RESPONSE = 'empty_response',\n FORMAT_ERROR = 'format_error',\n UNKNOWN = 'unknown',\n}\n\n// ── Classification Result ─────────────────────────────────────────\nexport interface ClassifiedError {\n /** The category of failure */\n reason: FailoverReason;\n /** HTTP status code if available */\n httpStatus?: number;\n /** Whether this error can be retried with the same provider/key */\n retryable: boolean;\n /** Whether context compression might resolve the issue */\n shouldCompress: boolean;\n /** Whether the API key should be rotated (for credential pools) */\n shouldRotateCredential: boolean;\n /** Whether to try a different provider entirely */\n shouldFallback: boolean;\n /** Suggested delay before retry in ms (0 = no delay) */\n cooldownMs: number;\n /** Human-readable error message */\n message: string;\n}\n\n// ── Default recovery profiles ─────────────────────────────────────\nconst RECOVERY_PROFILES: Record<FailoverReason, Omit<ClassifiedError, 'reason' | 'httpStatus' | 'message'>> = {\n [FailoverReason.RATE_LIMIT]: {\n retryable: true,\n shouldCompress: false,\n shouldRotateCredential: true,\n shouldFallback: false,\n cooldownMs: 5000,\n },\n [FailoverReason.QUOTA_EXCEEDED]: {\n retryable: false,\n shouldCompress: false,\n shouldRotateCredential: true,\n shouldFallback: true,\n cooldownMs: 3600000, // 1 hour\n },\n [FailoverReason.CONTEXT_OVERFLOW]: {\n retryable: true,\n shouldCompress: true,\n shouldRotateCredential: false,\n shouldFallback: false,\n cooldownMs: 0,\n },\n [FailoverReason.AUTH_INVALID]: {\n retryable: false,\n shouldCompress: false,\n shouldRotateCredential: true,\n shouldFallback: true,\n cooldownMs: 0,\n },\n [FailoverReason.AUTH_EXPIRED]: {\n retryable: false,\n shouldCompress: false,\n shouldRotateCredential: true,\n shouldFallback: false,\n cooldownMs: 0,\n },\n [FailoverReason.MODEL_NOT_FOUND]: {\n retryable: false,\n shouldCompress: false,\n shouldRotateCredential: false,\n shouldFallback: true,\n cooldownMs: 0,\n },\n [FailoverReason.CONTENT_FILTERED]: {\n retryable: false,\n shouldCompress: false,\n shouldRotateCredential: false,\n shouldFallback: false,\n cooldownMs: 0,\n },\n [FailoverReason.SERVER_ERROR]: {\n retryable: true,\n shouldCompress: false,\n shouldRotateCredential: false,\n shouldFallback: false,\n cooldownMs: 2000,\n },\n [FailoverReason.TIMEOUT]: {\n retryable: true,\n shouldCompress: false,\n shouldRotateCredential: false,\n shouldFallback: false,\n cooldownMs: 1000,\n },\n [FailoverReason.NETWORK_ERROR]: {\n retryable: true,\n shouldCompress: false,\n shouldRotateCredential: false,\n shouldFallback: true,\n cooldownMs: 2000,\n },\n [FailoverReason.OVERLOADED]: {\n retryable: true,\n shouldCompress: false,\n shouldRotateCredential: false,\n shouldFallback: true,\n cooldownMs: 10000,\n },\n [FailoverReason.EMPTY_RESPONSE]: {\n retryable: true,\n shouldCompress: false,\n shouldRotateCredential: false,\n shouldFallback: false,\n cooldownMs: 1000,\n },\n [FailoverReason.FORMAT_ERROR]: {\n retryable: true,\n shouldCompress: false,\n shouldRotateCredential: false,\n shouldFallback: false,\n cooldownMs: 0,\n },\n [FailoverReason.UNKNOWN]: {\n retryable: false,\n shouldCompress: false,\n shouldRotateCredential: false,\n shouldFallback: false,\n cooldownMs: 0,\n },\n};\n\n// ── Status code extraction ────────────────────────────────────────\nfunction extractStatus(error: unknown): number | undefined {\n if (!error || typeof error !== 'object') return undefined;\n const e = error as Record<string, unknown>;\n // Direct status property\n if (typeof e.status === 'number') return e.status;\n if (typeof e.statusCode === 'number') return e.statusCode;\n // Nested in response\n if (e.response && typeof e.response === 'object') {\n const resp = e.response as Record<string, unknown>;\n if (typeof resp.status === 'number') return resp.status;\n if (typeof resp.statusCode === 'number') return resp.statusCode;\n }\n // Check error property (Anthropic SDK)\n if (e.error && typeof e.error === 'object') {\n const nested = e.error as Record<string, unknown>;\n if (typeof nested.status === 'number') return nested.status;\n }\n return undefined;\n}\n\n// ── Error body extraction ─────────────────────────────────────────\nfunction extractBody(error: unknown): string {\n if (!error || typeof error !== 'object') return '';\n const e = error as Record<string, unknown>;\n const parts: string[] = [];\n\n if (typeof e.message === 'string') parts.push(e.message);\n if (e.response && typeof e.response === 'object') {\n const resp = e.response as Record<string, unknown>;\n if (typeof resp.data === 'string') parts.push(resp.data);\n if (resp.data && typeof resp.data === 'object') {\n try { parts.push(JSON.stringify(resp.data)); } catch { /* skip */ }\n }\n }\n if (e.body && typeof e.body === 'string') parts.push(e.body);\n if (e.error && typeof e.error === 'object') {\n try { parts.push(JSON.stringify(e.error)); } catch { /* skip */ }\n }\n return parts.join(' ').toLowerCase();\n}\n\n// ── Build result helper ───────────────────────────────────────────\nfunction buildResult(reason: FailoverReason, status: number | undefined, message: string): ClassifiedError {\n return {\n reason,\n httpStatus: status,\n message,\n ...RECOVERY_PROFILES[reason],\n };\n}\n\n// ── 7-Step Classification Pipeline ────────────────────────────────\n/**\n * Classify a provider error into a structured recovery recommendation.\n *\n * Pipeline steps:\n * 1. Extract HTTP status code\n * 2. Extract error body text\n * 3. Match exact status codes (429, 401, 402, 404, 503, 529)\n * 4. Disambiguate 400 (context overflow vs content filter vs format error)\n * 5. Disambiguate 402 (billing vs quota)\n * 6. Match network/timeout patterns from error message\n * 7. Default to UNKNOWN\n */\nexport function classifyProviderError(error: unknown): ClassifiedError {\n // Step 1: Extract HTTP status\n const status = extractStatus(error);\n const msg = (error as Error)?.message || String(error);\n\n // Step 2: Extract body text for pattern matching\n const body = extractBody(error);\n\n // Step 3: Match exact status codes\n if (status === 429) {\n return buildResult(FailoverReason.RATE_LIMIT, status, msg);\n }\n\n if (status === 401) {\n // Step 3a: Disambiguate expired vs invalid auth\n if (body.includes('expired') || body.includes('token expired') || body.includes('refresh')) {\n return buildResult(FailoverReason.AUTH_EXPIRED, status, msg);\n }\n return buildResult(FailoverReason.AUTH_INVALID, status, msg);\n }\n\n if (status === 403) {\n return buildResult(FailoverReason.AUTH_INVALID, status, msg);\n }\n\n if (status === 404) {\n if (body.includes('model') || body.includes('not found') || body.includes('does not exist')) {\n return buildResult(FailoverReason.MODEL_NOT_FOUND, status, msg);\n }\n return buildResult(FailoverReason.UNKNOWN, status, msg);\n }\n\n // Step 4: Disambiguate 400 errors\n if (status === 400) {\n if (body.includes('context length') || body.includes('maximum context') ||\n body.includes('max tokens') || body.includes('token limit') ||\n body.includes('too long') || body.includes('too many tokens') ||\n body.includes('context window') || body.includes('input too large')) {\n return buildResult(FailoverReason.CONTEXT_OVERFLOW, status, msg);\n }\n if (body.includes('content_filter') || body.includes('content filter') ||\n body.includes('safety') || body.includes('harmful') ||\n body.includes('flagged') || body.includes('blocked')) {\n return buildResult(FailoverReason.CONTENT_FILTERED, status, msg);\n }\n if (body.includes('invalid') || body.includes('malformed') ||\n body.includes('parse') || body.includes('json')) {\n return buildResult(FailoverReason.FORMAT_ERROR, status, msg);\n }\n return buildResult(FailoverReason.FORMAT_ERROR, status, msg);\n }\n\n // Step 5: Disambiguate 402 (billing vs transient quota)\n if (status === 402) {\n if (body.includes('billing') || body.includes('payment') ||\n body.includes('subscription') || body.includes('plan')) {\n return buildResult(FailoverReason.QUOTA_EXCEEDED, status, msg);\n }\n // Default 402 to quota exceeded (usually means out of credits)\n return buildResult(FailoverReason.QUOTA_EXCEEDED, status, msg);\n }\n\n // Anthropic overloaded (529)\n if (status === 529) {\n return buildResult(FailoverReason.OVERLOADED, status, msg);\n }\n\n // 5xx server errors\n if (status && status >= 500 && status < 600) {\n if (body.includes('overloaded') || body.includes('capacity')) {\n return buildResult(FailoverReason.OVERLOADED, status, msg);\n }\n return buildResult(FailoverReason.SERVER_ERROR, status, msg);\n }\n\n // Step 6: Match patterns from error message (no status code available)\n const lowerMsg = msg.toLowerCase();\n\n // Timeout patterns\n if (lowerMsg.includes('timed out') || lowerMsg.includes('timeout') ||\n lowerMsg.includes('etimedout') || lowerMsg.includes('aborted')) {\n // v4.10.0-local (cost cap): claude-code timeouts are NOT retryable.\n // Each retry burns another ~2 min of MAX plan quota on what is\n // probably a stuck tool loop; better to fail fast and let the\n // fallback-ladder try a cheaper model.\n if (lowerMsg.includes('claude cli') || lowerMsg.includes('claude-code')) {\n return {\n ...buildResult(FailoverReason.TIMEOUT, status, msg),\n retryable: false,\n shouldFallback: true,\n cooldownMs: 0,\n };\n }\n return buildResult(FailoverReason.TIMEOUT, status, msg);\n }\n\n // Rate limit patterns (from message without status code)\n if (lowerMsg.includes('rate limit') || lowerMsg.includes('rate_limit') ||\n lowerMsg.includes('too many requests') || lowerMsg.includes('quota exceeded') ||\n lowerMsg.includes('429')) {\n return buildResult(FailoverReason.RATE_LIMIT, status, msg);\n }\n\n // Network/connection patterns\n if (lowerMsg.includes('econnrefused') || lowerMsg.includes('econnreset') ||\n lowerMsg.includes('econnaborted') || lowerMsg.includes('epipe') ||\n lowerMsg.includes('enotfound') || lowerMsg.includes('fetch failed') ||\n lowerMsg.includes('network error') || lowerMsg.includes('socket hang up') ||\n lowerMsg.includes('connection closed') || lowerMsg.includes('connection refused') ||\n lowerMsg.includes('dns')) {\n return buildResult(FailoverReason.NETWORK_ERROR, status, msg);\n }\n\n // Overloaded patterns\n if (lowerMsg.includes('overloaded') || lowerMsg.includes('service unavailable') ||\n lowerMsg.includes('capacity') || lowerMsg.includes('503') || lowerMsg.includes('502')) {\n return buildResult(FailoverReason.OVERLOADED, status, msg);\n }\n\n // Server error patterns\n if (lowerMsg.includes('500') || lowerMsg.includes('internal server error') ||\n lowerMsg.includes('server error')) {\n return buildResult(FailoverReason.SERVER_ERROR, status, msg);\n }\n\n // Auth patterns from message\n if (lowerMsg.includes('unauthorized') || lowerMsg.includes('invalid api key') ||\n lowerMsg.includes('invalid_api_key') || lowerMsg.includes('authentication')) {\n return buildResult(FailoverReason.AUTH_INVALID, status, msg);\n }\n\n // Context overflow patterns from message\n if (lowerMsg.includes('context length') || lowerMsg.includes('token limit') ||\n lowerMsg.includes('too long') || lowerMsg.includes('context window')) {\n return buildResult(FailoverReason.CONTEXT_OVERFLOW, status, msg);\n }\n\n // Empty response\n if (lowerMsg.includes('empty response') || lowerMsg.includes('no content') ||\n lowerMsg.includes('null response')) {\n return buildResult(FailoverReason.EMPTY_RESPONSE, status, msg);\n }\n\n // Step 7: Default\n logger.debug(COMPONENT, `Unclassified error (status=${status}): ${msg.slice(0, 200)}`);\n return buildResult(FailoverReason.UNKNOWN, status, msg);\n}\n\n/**\n * Check if a classified error should prevent the circuit breaker from opening.\n * Auth errors and model-not-found are config problems, not provider instability.\n */\nexport function shouldAffectCircuitBreaker(classified: ClassifiedError): boolean {\n return classified.reason !== FailoverReason.AUTH_INVALID\n && classified.reason !== FailoverReason.AUTH_EXPIRED\n && classified.reason !== FailoverReason.MODEL_NOT_FOUND\n && classified.reason !== FailoverReason.CONTENT_FILTERED\n && classified.reason !== FailoverReason.FORMAT_ERROR;\n}\n\n/**\n * Hunt Finding #37 (2026-04-14): a shared helper for building errors from\n * a failed fetch Response. Previous code pattern was:\n *\n * throw new Error(`Provider error (${response.status}): ${errorText}`);\n *\n * This dropped the `Retry-After` header on the floor. Downstream code in\n * router.ts tried `(error as Response)?.headers?.get?.('Retry-After')`\n * which always returned undefined at runtime because the error is an\n * Error object, not a Response. Result: Retry-After headers were NEVER\n * respected, even though the code pretended to.\n *\n * New pattern: use this helper to attach status + parsed Retry-After\n * (in ms) + the original body snippet to the error, so router.ts can\n * read them back via typed properties.\n */\nexport interface ProviderHttpError extends Error {\n status?: number;\n retryAfterMs?: number | null;\n provider?: string;\n model?: string;\n}\n\nexport function createProviderError(\n providerDisplayName: string,\n response: { status: number; headers?: { get(name: string): string | null } },\n errorText: string,\n opts?: { provider?: string; model?: string },\n): ProviderHttpError {\n const err: ProviderHttpError = new Error(\n `${providerDisplayName} error (${response.status}): ${errorText}`,\n );\n err.status = response.status;\n err.provider = opts?.provider;\n err.model = opts?.model;\n\n // Parse Retry-After at throw time — router.ts reads err.retryAfterMs later\n try {\n const raw = response.headers?.get('Retry-After');\n if (raw) {\n const sec = parseInt(raw, 10);\n if (Number.isFinite(sec)) {\n err.retryAfterMs = Math.max(0, Math.min(sec * 1000, 300_000));\n } else {\n const date = new Date(raw);\n if (!isNaN(date.getTime())) {\n err.retryAfterMs = Math.max(0, Math.min(date.getTime() - Date.now(), 300_000));\n }\n }\n }\n } catch {\n // Header parsing failures are non-fatal — just fall back to calculated backoff.\n }\n\n return err;\n}\n"],"mappings":"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","names":["FailoverReason"]}
1
+ {"version":3,"sources":["../../src/providers/errorTaxonomy.ts"],"sourcesContent":["/**\n * TITAN — Centralized Error Taxonomy\n *\n * Replaces scattered error handling across router.ts, toolRunner.ts, and server.ts\n * with a single 7-step classification pipeline. Inspired by Hermes error_classifier.py.\n *\n * Each provider error is classified into a FailoverReason with structured recovery hints\n * (retryable, shouldCompress, shouldRotateCredential, shouldFallback, cooldownMs).\n */\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'ErrorTaxonomy';\n\n// ── Error Categories ──────────────────────────────────────────────\nexport enum FailoverReason {\n RATE_LIMIT = 'rate_limit',\n QUOTA_EXCEEDED = 'quota_exceeded',\n CONTEXT_OVERFLOW = 'context_overflow',\n AUTH_INVALID = 'auth_invalid',\n AUTH_EXPIRED = 'auth_expired',\n MODEL_NOT_FOUND = 'model_not_found',\n CONTENT_FILTERED = 'content_filtered',\n SERVER_ERROR = 'server_error',\n TIMEOUT = 'timeout',\n NETWORK_ERROR = 'network_error',\n OVERLOADED = 'overloaded',\n EMPTY_RESPONSE = 'empty_response',\n FORMAT_ERROR = 'format_error',\n /** Model returned 400 because thinking/extended-thinking is not supported.\n * Recovery: strip thinking flag and retry once on the same provider. */\n THINKING_NOT_SUPPORTED = 'thinking_not_supported',\n UNKNOWN = 'unknown',\n}\n\n// ── Classification Result ─────────────────────────────────────────\nexport interface ClassifiedError {\n /** The category of failure */\n reason: FailoverReason;\n /** HTTP status code if available */\n httpStatus?: number;\n /** Whether this error can be retried with the same provider/key */\n retryable: boolean;\n /** Whether context compression might resolve the issue */\n shouldCompress: boolean;\n /** Whether the API key should be rotated (for credential pools) */\n shouldRotateCredential: boolean;\n /** Whether to try a different provider entirely */\n shouldFallback: boolean;\n /** Suggested delay before retry in ms (0 = no delay) */\n cooldownMs: number;\n /** Human-readable error message */\n message: string;\n}\n\n// ── Default recovery profiles ─────────────────────────────────────\nconst RECOVERY_PROFILES: Record<FailoverReason, Omit<ClassifiedError, 'reason' | 'httpStatus' | 'message'>> = {\n [FailoverReason.RATE_LIMIT]: {\n retryable: true,\n shouldCompress: false,\n shouldRotateCredential: true,\n shouldFallback: false,\n cooldownMs: 5000,\n },\n [FailoverReason.QUOTA_EXCEEDED]: {\n retryable: false,\n shouldCompress: false,\n shouldRotateCredential: true,\n shouldFallback: true,\n cooldownMs: 3600000, // 1 hour\n },\n [FailoverReason.CONTEXT_OVERFLOW]: {\n retryable: true,\n shouldCompress: true,\n shouldRotateCredential: false,\n shouldFallback: false,\n cooldownMs: 0,\n },\n [FailoverReason.AUTH_INVALID]: {\n retryable: false,\n shouldCompress: false,\n shouldRotateCredential: true,\n shouldFallback: true,\n cooldownMs: 0,\n },\n [FailoverReason.AUTH_EXPIRED]: {\n retryable: false,\n shouldCompress: false,\n shouldRotateCredential: true,\n shouldFallback: false,\n cooldownMs: 0,\n },\n [FailoverReason.MODEL_NOT_FOUND]: {\n retryable: false,\n shouldCompress: false,\n shouldRotateCredential: false,\n shouldFallback: true,\n cooldownMs: 0,\n },\n [FailoverReason.CONTENT_FILTERED]: {\n retryable: false,\n shouldCompress: false,\n shouldRotateCredential: false,\n shouldFallback: false,\n cooldownMs: 0,\n },\n [FailoverReason.SERVER_ERROR]: {\n retryable: true,\n shouldCompress: false,\n shouldRotateCredential: false,\n shouldFallback: false,\n cooldownMs: 2000,\n },\n [FailoverReason.TIMEOUT]: {\n retryable: true,\n shouldCompress: false,\n shouldRotateCredential: false,\n shouldFallback: false,\n cooldownMs: 1000,\n },\n [FailoverReason.NETWORK_ERROR]: {\n retryable: true,\n shouldCompress: false,\n shouldRotateCredential: false,\n shouldFallback: true,\n cooldownMs: 2000,\n },\n [FailoverReason.OVERLOADED]: {\n retryable: true,\n shouldCompress: false,\n shouldRotateCredential: false,\n shouldFallback: true,\n cooldownMs: 10000,\n },\n [FailoverReason.EMPTY_RESPONSE]: {\n retryable: true,\n shouldCompress: false,\n shouldRotateCredential: false,\n shouldFallback: false,\n cooldownMs: 1000,\n },\n [FailoverReason.FORMAT_ERROR]: {\n retryable: true,\n shouldCompress: false,\n shouldRotateCredential: false,\n shouldFallback: false,\n cooldownMs: 0,\n },\n [FailoverReason.THINKING_NOT_SUPPORTED]: {\n // Retry once on the same provider after stripping thinking options.\n // The router handles the option mutation; after one retry we fall through.\n retryable: true,\n shouldCompress: false,\n shouldRotateCredential: false,\n shouldFallback: false,\n cooldownMs: 0,\n },\n [FailoverReason.UNKNOWN]: {\n retryable: false,\n shouldCompress: false,\n shouldRotateCredential: false,\n shouldFallback: false,\n cooldownMs: 0,\n },\n};\n\n// ── Status code extraction ────────────────────────────────────────\nfunction extractStatus(error: unknown): number | undefined {\n if (!error || typeof error !== 'object') return undefined;\n const e = error as Record<string, unknown>;\n // Direct status property\n if (typeof e.status === 'number') return e.status;\n if (typeof e.statusCode === 'number') return e.statusCode;\n // Nested in response\n if (e.response && typeof e.response === 'object') {\n const resp = e.response as Record<string, unknown>;\n if (typeof resp.status === 'number') return resp.status;\n if (typeof resp.statusCode === 'number') return resp.statusCode;\n }\n // Check error property (Anthropic SDK)\n if (e.error && typeof e.error === 'object') {\n const nested = e.error as Record<string, unknown>;\n if (typeof nested.status === 'number') return nested.status;\n }\n return undefined;\n}\n\n// ── Error body extraction ─────────────────────────────────────────\nfunction extractBody(error: unknown): string {\n if (!error || typeof error !== 'object') return '';\n const e = error as Record<string, unknown>;\n const parts: string[] = [];\n\n if (typeof e.message === 'string') parts.push(e.message);\n if (e.response && typeof e.response === 'object') {\n const resp = e.response as Record<string, unknown>;\n if (typeof resp.data === 'string') parts.push(resp.data);\n if (resp.data && typeof resp.data === 'object') {\n try { parts.push(JSON.stringify(resp.data)); } catch { /* skip */ }\n }\n }\n if (e.body && typeof e.body === 'string') parts.push(e.body);\n if (e.error && typeof e.error === 'object') {\n try { parts.push(JSON.stringify(e.error)); } catch { /* skip */ }\n }\n return parts.join(' ').toLowerCase();\n}\n\n// ── Build result helper ───────────────────────────────────────────\nfunction buildResult(reason: FailoverReason, status: number | undefined, message: string): ClassifiedError {\n return {\n reason,\n httpStatus: status,\n message,\n ...RECOVERY_PROFILES[reason],\n };\n}\n\n// ── 7-Step Classification Pipeline ────────────────────────────────\n/**\n * Classify a provider error into a structured recovery recommendation.\n *\n * Pipeline steps:\n * 1. Extract HTTP status code\n * 2. Extract error body text\n * 3. Match exact status codes (429, 401, 402, 404, 503, 529)\n * 4. Disambiguate 400 (context overflow vs content filter vs format error)\n * 5. Disambiguate 402 (billing vs quota)\n * 6. Match network/timeout patterns from error message\n * 7. Default to UNKNOWN\n */\nexport function classifyProviderError(error: unknown): ClassifiedError {\n // Step 1: Extract HTTP status\n const status = extractStatus(error);\n const msg = (error as Error)?.message || String(error);\n\n // Step 2: Extract body text for pattern matching\n const body = extractBody(error);\n\n // Step 3: Match exact status codes\n if (status === 429) {\n return buildResult(FailoverReason.RATE_LIMIT, status, msg);\n }\n\n if (status === 401) {\n // Step 3a: Disambiguate expired vs invalid auth\n if (body.includes('expired') || body.includes('token expired') || body.includes('refresh')) {\n return buildResult(FailoverReason.AUTH_EXPIRED, status, msg);\n }\n return buildResult(FailoverReason.AUTH_INVALID, status, msg);\n }\n\n if (status === 403) {\n return buildResult(FailoverReason.AUTH_INVALID, status, msg);\n }\n\n if (status === 404) {\n if (body.includes('model') || body.includes('not found') || body.includes('does not exist')) {\n return buildResult(FailoverReason.MODEL_NOT_FOUND, status, msg);\n }\n return buildResult(FailoverReason.UNKNOWN, status, msg);\n }\n\n // Step 4: Disambiguate 400 errors\n if (status === 400) {\n if (body.includes('context length') || body.includes('maximum context') ||\n body.includes('max tokens') || body.includes('token limit') ||\n body.includes('too long') || body.includes('too many tokens') ||\n body.includes('context window') || body.includes('input too large')) {\n return buildResult(FailoverReason.CONTEXT_OVERFLOW, status, msg);\n }\n if (body.includes('content_filter') || body.includes('content filter') ||\n body.includes('safety') || body.includes('harmful') ||\n body.includes('flagged') || body.includes('blocked')) {\n return buildResult(FailoverReason.CONTENT_FILTERED, status, msg);\n }\n // Detect thinking-not-supported (e.g. Ollama qwen3.5:4b, some OpenAI-compat endpoints)\n if (body.includes('does not support thinking') || body.includes('thinking is not supported') ||\n body.includes('extended-thinking') || body.includes('think_mode') ||\n body.includes('budget_tokens') || body.includes('thinking_mode')) {\n return buildResult(FailoverReason.THINKING_NOT_SUPPORTED, status, msg);\n }\n if (body.includes('invalid') || body.includes('malformed') ||\n body.includes('parse') || body.includes('json')) {\n return buildResult(FailoverReason.FORMAT_ERROR, status, msg);\n }\n return buildResult(FailoverReason.FORMAT_ERROR, status, msg);\n }\n\n // Step 5: Disambiguate 402 (billing vs transient quota)\n if (status === 402) {\n if (body.includes('billing') || body.includes('payment') ||\n body.includes('subscription') || body.includes('plan')) {\n return buildResult(FailoverReason.QUOTA_EXCEEDED, status, msg);\n }\n // Default 402 to quota exceeded (usually means out of credits)\n return buildResult(FailoverReason.QUOTA_EXCEEDED, status, msg);\n }\n\n // Anthropic overloaded (529)\n if (status === 529) {\n return buildResult(FailoverReason.OVERLOADED, status, msg);\n }\n\n // 5xx server errors\n if (status && status >= 500 && status < 600) {\n if (body.includes('overloaded') || body.includes('capacity')) {\n return buildResult(FailoverReason.OVERLOADED, status, msg);\n }\n return buildResult(FailoverReason.SERVER_ERROR, status, msg);\n }\n\n // Step 6: Match patterns from error message (no status code available)\n const lowerMsg = msg.toLowerCase();\n\n // Timeout patterns\n if (lowerMsg.includes('timed out') || lowerMsg.includes('timeout') ||\n lowerMsg.includes('etimedout') || lowerMsg.includes('aborted')) {\n // v4.10.0-local (cost cap): claude-code timeouts are NOT retryable.\n // Each retry burns another ~2 min of MAX plan quota on what is\n // probably a stuck tool loop; better to fail fast and let the\n // fallback-ladder try a cheaper model.\n if (lowerMsg.includes('claude cli') || lowerMsg.includes('claude-code')) {\n return {\n ...buildResult(FailoverReason.TIMEOUT, status, msg),\n retryable: false,\n shouldFallback: true,\n cooldownMs: 0,\n };\n }\n return buildResult(FailoverReason.TIMEOUT, status, msg);\n }\n\n // Rate limit patterns (from message without status code)\n if (lowerMsg.includes('rate limit') || lowerMsg.includes('rate_limit') ||\n lowerMsg.includes('too many requests') || lowerMsg.includes('quota exceeded') ||\n lowerMsg.includes('429')) {\n return buildResult(FailoverReason.RATE_LIMIT, status, msg);\n }\n\n // Network/connection patterns\n if (lowerMsg.includes('econnrefused') || lowerMsg.includes('econnreset') ||\n lowerMsg.includes('econnaborted') || lowerMsg.includes('epipe') ||\n lowerMsg.includes('enotfound') || lowerMsg.includes('fetch failed') ||\n lowerMsg.includes('network error') || lowerMsg.includes('socket hang up') ||\n lowerMsg.includes('connection closed') || lowerMsg.includes('connection refused') ||\n lowerMsg.includes('dns')) {\n return buildResult(FailoverReason.NETWORK_ERROR, status, msg);\n }\n\n // Overloaded patterns\n if (lowerMsg.includes('overloaded') || lowerMsg.includes('service unavailable') ||\n lowerMsg.includes('capacity') || lowerMsg.includes('503') || lowerMsg.includes('502')) {\n return buildResult(FailoverReason.OVERLOADED, status, msg);\n }\n\n // Server error patterns\n if (lowerMsg.includes('500') || lowerMsg.includes('internal server error') ||\n lowerMsg.includes('server error')) {\n return buildResult(FailoverReason.SERVER_ERROR, status, msg);\n }\n\n // Auth patterns from message\n if (lowerMsg.includes('unauthorized') || lowerMsg.includes('invalid api key') ||\n lowerMsg.includes('invalid_api_key') || lowerMsg.includes('authentication')) {\n return buildResult(FailoverReason.AUTH_INVALID, status, msg);\n }\n\n // Context overflow patterns from message\n if (lowerMsg.includes('context length') || lowerMsg.includes('token limit') ||\n lowerMsg.includes('too long') || lowerMsg.includes('context window')) {\n return buildResult(FailoverReason.CONTEXT_OVERFLOW, status, msg);\n }\n\n // Empty response\n if (lowerMsg.includes('empty response') || lowerMsg.includes('no content') ||\n lowerMsg.includes('null response')) {\n return buildResult(FailoverReason.EMPTY_RESPONSE, status, msg);\n }\n\n // Step 7: Default\n logger.debug(COMPONENT, `Unclassified error (status=${status}): ${msg.slice(0, 200)}`);\n return buildResult(FailoverReason.UNKNOWN, status, msg);\n}\n\n/**\n * Check if a classified error should prevent the circuit breaker from opening.\n * Auth errors and model-not-found are config problems, not provider instability.\n */\nexport function shouldAffectCircuitBreaker(classified: ClassifiedError): boolean {\n return classified.reason !== FailoverReason.AUTH_INVALID\n && classified.reason !== FailoverReason.AUTH_EXPIRED\n && classified.reason !== FailoverReason.MODEL_NOT_FOUND\n && classified.reason !== FailoverReason.CONTENT_FILTERED\n && classified.reason !== FailoverReason.FORMAT_ERROR;\n}\n\n/**\n * Hunt Finding #37 (2026-04-14): a shared helper for building errors from\n * a failed fetch Response. Previous code pattern was:\n *\n * throw new Error(`Provider error (${response.status}): ${errorText}`);\n *\n * This dropped the `Retry-After` header on the floor. Downstream code in\n * router.ts tried `(error as Response)?.headers?.get?.('Retry-After')`\n * which always returned undefined at runtime because the error is an\n * Error object, not a Response. Result: Retry-After headers were NEVER\n * respected, even though the code pretended to.\n *\n * New pattern: use this helper to attach status + parsed Retry-After\n * (in ms) + the original body snippet to the error, so router.ts can\n * read them back via typed properties.\n */\nexport interface ProviderHttpError extends Error {\n status?: number;\n retryAfterMs?: number | null;\n provider?: string;\n model?: string;\n}\n\nexport function createProviderError(\n providerDisplayName: string,\n response: { status: number; headers?: { get(name: string): string | null } },\n errorText: string,\n opts?: { provider?: string; model?: string },\n): ProviderHttpError {\n const err: ProviderHttpError = new Error(\n `${providerDisplayName} error (${response.status}): ${errorText}`,\n );\n err.status = response.status;\n err.provider = opts?.provider;\n err.model = opts?.model;\n\n // Parse Retry-After at throw time — router.ts reads err.retryAfterMs later\n try {\n const raw = response.headers?.get('Retry-After');\n if (raw) {\n const sec = parseInt(raw, 10);\n if (Number.isFinite(sec)) {\n err.retryAfterMs = Math.max(0, Math.min(sec * 1000, 300_000));\n } else {\n const date = new Date(raw);\n if (!isNaN(date.getTime())) {\n err.retryAfterMs = Math.max(0, Math.min(date.getTime() - Date.now(), 300_000));\n }\n }\n }\n } catch {\n // Header parsing failures are non-fatal — just fall back to calculated backoff.\n }\n\n return err;\n}\n"],"mappings":"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","names":["FailoverReason"]}
@@ -147,7 +147,9 @@ function getModelCapabilities(modelName) {
147
147
  })).catch((err) => logger.warn(COMPONENT, `Background probe failed for ${modelName2}: ${err.message}`)).finally(() => probeInFlight.delete(modelName2));
148
148
  }
149
149
  const bare = modelName.includes("/") ? modelName.split("/").slice(1).join("/") : modelName;
150
- const baseName = bare.replace(/:(cloud|latest|\d+b(-cloud)?)$/i, "");
150
+ const noTag = bare.replace(/:(cloud|latest|\d+b(-cloud)?)$/i, "");
151
+ const knownPrefixes = /^(titan|local|custom|myorg|org|priv|private|dev|test|prod|staging)-/i;
152
+ const baseName = noTag.replace(knownPrefixes, "");
151
153
  let bestMatch;
152
154
  let bestLen = 0;
153
155
  for (const [pattern, caps] of Object.entries(MODEL_CAPABILITIES)) {