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
@@ -2,7 +2,7 @@
2
2
  import { existsSync, readFileSync, writeFileSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { TITAN_HOME } from "../utils/constants.js";
5
- import { ensureDir } from "../utils/helpers.js";
5
+ import { mkdirIfNotExists } from "../utils/helpers.js";
6
6
  import { connectMcpServer, disconnectMcpServer, getMcpConnections } from "./client.js";
7
7
  import logger from "../utils/logger.js";
8
8
  const COMPONENT = "MCPRegistry";
@@ -39,7 +39,7 @@ function loadServers() {
39
39
  }
40
40
  }
41
41
  function saveServers(servers) {
42
- ensureDir(TITAN_HOME);
42
+ mkdirIfNotExists(TITAN_HOME);
43
43
  writeFileSync(MCP_CONFIG_PATH, JSON.stringify(servers, null, 2), "utf-8");
44
44
  }
45
45
  function getBuiltinPreset(id) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/mcp/registry.ts"],"sourcesContent":["/**\n * TITAN — MCP Registry\n * Persists the list of configured MCP servers to ~/.titan/mcp.json\n * and bootstraps connections on startup.\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 { connectMcpServer, disconnectMcpServer, getMcpConnections, type McpServer } from './client.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'MCPRegistry';\nconst MCP_CONFIG_PATH = join(TITAN_HOME, 'mcp.json');\n\n// ─── Built-in Presets ────────────────────────────────────────────\n// Users can activate these with `titan mcp --add <preset-id>`\n\nexport const BUILTIN_PRESETS: Omit<McpServer, 'timeoutMs'>[] = [\n {\n id: 'skyvern',\n name: 'Skyvern AI Browser',\n description: 'AI-powered browser automation with natural language commands, self-healing selectors, and credential management',\n type: 'stdio',\n command: 'skyvern',\n args: ['run', 'mcp'],\n env: {\n SKYVERN_BASE_URL: 'http://localhost:8000',\n SKYVERN_API_KEY: 'local',\n },\n enabled: false,\n },\n {\n id: 'hindsight',\n name: 'Hindsight Memory',\n description: 'Cross-session episodic memory with 4 networks (world, experience, opinion, observation). retain/recall/reflect operations. 91% LongMemEval. By Vectorize.io.',\n type: 'http',\n url: 'http://localhost:8888/mcp',\n enabled: false,\n },\n];\n\n// ─── Persistence ──────────────────────────────────────────────────\nfunction loadServers(): McpServer[] {\n try {\n if (!existsSync(MCP_CONFIG_PATH)) return [];\n return JSON.parse(readFileSync(MCP_CONFIG_PATH, 'utf-8')) as McpServer[];\n } catch {\n return [];\n }\n}\n\nfunction saveServers(servers: McpServer[]): void {\n ensureDir(TITAN_HOME);\n writeFileSync(MCP_CONFIG_PATH, JSON.stringify(servers, null, 2), 'utf-8');\n}\n\n// ─── Public API ───────────────────────────────────────────────────\n\n/** Look up a built-in preset by ID */\nexport function getBuiltinPreset(id: string): Omit<McpServer, 'timeoutMs'> | undefined {\n return BUILTIN_PRESETS.find((p) => p.id === id);\n}\n\n/** List all configured MCP servers */\nexport function listMcpServers(): McpServer[] {\n return loadServers();\n}\n\n/** Add a new MCP server */\nexport function addMcpServer(server: Omit<McpServer, 'timeoutMs' | 'enabled'>): McpServer {\n const servers = loadServers();\n const full: McpServer = { ...server, timeoutMs: 30000, enabled: true };\n\n if (servers.find((s) => s.id === full.id)) {\n throw new Error(`MCP server with ID \"${full.id}\" already exists.`);\n }\n servers.push(full);\n saveServers(servers);\n logger.info(COMPONENT, `Added MCP server: ${full.name}`);\n return full;\n}\n\n/** Add a built-in preset by its ID (e.g. 'skyvern') */\nexport function addPreset(presetId: string): McpServer {\n const preset = getBuiltinPreset(presetId);\n if (!preset) {\n const available = BUILTIN_PRESETS.map((p) => p.id).join(', ');\n throw new Error(`Unknown preset \"${presetId}\". Available presets: ${available}`);\n }\n return addMcpServer(preset as Omit<McpServer, 'timeoutMs' | 'enabled'>);\n}\n\n/** Remove an MCP server */\nexport function removeMcpServer(id: string): void {\n disconnectMcpServer(id);\n const servers = loadServers().filter((s) => s.id !== id);\n saveServers(servers);\n logger.info(COMPONENT, `Removed MCP server: ${id}`);\n}\n\n/** Enable or disable a server */\nexport function setMcpServerEnabled(id: string, enabled: boolean): void {\n const servers = loadServers();\n const server = servers.find((s) => s.id === id);\n if (!server) throw new Error(`MCP server \"${id}\" not found`);\n server.enabled = enabled;\n saveServers(servers);\n if (!enabled) disconnectMcpServer(id);\n}\n\n/** Boot all enabled MCP servers on startup */\nexport async function initMcpServers(): Promise<void> {\n const servers = loadServers().filter((s) => s.enabled);\n if (servers.length === 0) return;\n logger.info(COMPONENT, `Initialising ${servers.length} MCP server(s)...`);\n await Promise.all(servers.map((s) => connectMcpServer(s).catch((e) => {\n logger.error(COMPONENT, `Failed to start MCP server ${s.name}: ${e.message}`);\n })));\n}\n\n/** Get live status of all connections */\nexport function getMcpStatus(): { server: McpServer; status: string; toolCount: number }[] {\n const connections = getMcpConnections();\n return connections.map((c) => ({\n server: c.server,\n status: c.status,\n toolCount: c.tools.length,\n }));\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/mcp/registry.ts"],"sourcesContent":["/**\n * TITAN — MCP Registry\n * Persists the list of configured MCP servers to ~/.titan/mcp.json\n * and bootstraps connections on startup.\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 { connectMcpServer, disconnectMcpServer, getMcpConnections, type McpServer } from './client.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'MCPRegistry';\nconst MCP_CONFIG_PATH = join(TITAN_HOME, 'mcp.json');\n\n// ─── Built-in Presets ────────────────────────────────────────────\n// Users can activate these with `titan mcp --add <preset-id>`\n\nexport const BUILTIN_PRESETS: Omit<McpServer, 'timeoutMs'>[] = [\n {\n id: 'skyvern',\n name: 'Skyvern AI Browser',\n description: 'AI-powered browser automation with natural language commands, self-healing selectors, and credential management',\n type: 'stdio',\n command: 'skyvern',\n args: ['run', 'mcp'],\n env: {\n SKYVERN_BASE_URL: 'http://localhost:8000',\n SKYVERN_API_KEY: 'local',\n },\n enabled: false,\n },\n {\n id: 'hindsight',\n name: 'Hindsight Memory',\n description: 'Cross-session episodic memory with 4 networks (world, experience, opinion, observation). retain/recall/reflect operations. 91% LongMemEval. By Vectorize.io.',\n type: 'http',\n url: 'http://localhost:8888/mcp',\n enabled: false,\n },\n];\n\n// ─── Persistence ──────────────────────────────────────────────────\nfunction loadServers(): McpServer[] {\n try {\n if (!existsSync(MCP_CONFIG_PATH)) return [];\n return JSON.parse(readFileSync(MCP_CONFIG_PATH, 'utf-8')) as McpServer[];\n } catch {\n return [];\n }\n}\n\nfunction saveServers(servers: McpServer[]): void {\n mkdirIfNotExists(TITAN_HOME);\n writeFileSync(MCP_CONFIG_PATH, JSON.stringify(servers, null, 2), 'utf-8');\n}\n\n// ─── Public API ───────────────────────────────────────────────────\n\n/** Look up a built-in preset by ID */\nexport function getBuiltinPreset(id: string): Omit<McpServer, 'timeoutMs'> | undefined {\n return BUILTIN_PRESETS.find((p) => p.id === id);\n}\n\n/** List all configured MCP servers */\nexport function listMcpServers(): McpServer[] {\n return loadServers();\n}\n\n/** Add a new MCP server */\nexport function addMcpServer(server: Omit<McpServer, 'timeoutMs' | 'enabled'>): McpServer {\n const servers = loadServers();\n const full: McpServer = { ...server, timeoutMs: 30000, enabled: true };\n\n if (servers.find((s) => s.id === full.id)) {\n throw new Error(`MCP server with ID \"${full.id}\" already exists.`);\n }\n servers.push(full);\n saveServers(servers);\n logger.info(COMPONENT, `Added MCP server: ${full.name}`);\n return full;\n}\n\n/** Add a built-in preset by its ID (e.g. 'skyvern') */\nexport function addPreset(presetId: string): McpServer {\n const preset = getBuiltinPreset(presetId);\n if (!preset) {\n const available = BUILTIN_PRESETS.map((p) => p.id).join(', ');\n throw new Error(`Unknown preset \"${presetId}\". Available presets: ${available}`);\n }\n return addMcpServer(preset as Omit<McpServer, 'timeoutMs' | 'enabled'>);\n}\n\n/** Remove an MCP server */\nexport function removeMcpServer(id: string): void {\n disconnectMcpServer(id);\n const servers = loadServers().filter((s) => s.id !== id);\n saveServers(servers);\n logger.info(COMPONENT, `Removed MCP server: ${id}`);\n}\n\n/** Enable or disable a server */\nexport function setMcpServerEnabled(id: string, enabled: boolean): void {\n const servers = loadServers();\n const server = servers.find((s) => s.id === id);\n if (!server) throw new Error(`MCP server \"${id}\" not found`);\n server.enabled = enabled;\n saveServers(servers);\n if (!enabled) disconnectMcpServer(id);\n}\n\n/** Boot all enabled MCP servers on startup */\nexport async function initMcpServers(): Promise<void> {\n const servers = loadServers().filter((s) => s.enabled);\n if (servers.length === 0) return;\n logger.info(COMPONENT, `Initialising ${servers.length} MCP server(s)...`);\n await Promise.all(servers.map((s) => connectMcpServer(s).catch((e) => {\n logger.error(COMPONENT, `Failed to start MCP server ${s.name}: ${e.message}`);\n })));\n}\n\n/** Get live status of all connections */\nexport function getMcpStatus(): { server: McpServer; status: string; toolCount: number }[] {\n const connections = getMcpConnections();\n return connections.map((c) => ({\n server: c.server,\n status: c.status,\n toolCount: c.tools.length,\n }));\n}\n"],"mappings":"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","names":[]}
@@ -8,7 +8,7 @@ import logger from "../utils/logger.js";
8
8
  const COMPONENT = "Episodic";
9
9
  const EPISODIC_PATH = join(TITAN_HOME, "episodic.jsonl");
10
10
  const EPISODIC_MAX_LINES = 2e4;
11
- function ensureDir() {
11
+ function mkdirIfNotExists() {
12
12
  try {
13
13
  mkdirSync(dirname(EPISODIC_PATH), { recursive: true });
14
14
  } catch {
@@ -34,7 +34,7 @@ function loadAll() {
34
34
  }
35
35
  }
36
36
  function appendLine(ep) {
37
- ensureDir();
37
+ mkdirIfNotExists();
38
38
  const line = JSON.stringify(ep) + "\n";
39
39
  try {
40
40
  appendFileSync(EPISODIC_PATH, line, "utf-8");
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/memory/episodic.ts"],"sourcesContent":["/**\n * TITAN — Episodic Memory (v4.9.0+, local hard-takeoff)\n *\n * The \"what did I do and when\" layer. Thin but strategic — it pulls\n * together pieces that already exist:\n * - src/memory/graph.ts — episodes + entities + vector embedding\n * - src/memory/vectors.ts — Ollama-backed semantic search\n * - src/memory/experiments.ts — attempt/outcome/lesson tracker\n * - src/agent/trajectoryLogger.ts — per-session tool trajectories\n *\n * Public API:\n * recordEpisode(event) — append a high-signal event (drive-fired\n * proposal, goal completion, kill, self-mod PR,\n * identity drift resolution, etc.) to the\n * episodic store. Attaches vector embedding\n * so future recall is semantic, not lexical.\n *\n * recallSimilarEpisodes(query, opts) — semantic search across past\n * events, filtered by time window + kind.\n *\n * recallRecent(opts) — time-window recall without a semantic query.\n *\n * renderRecallBlock(...) — formatted recall output for goalProposer\n * context blocks. Used to tell Soma's proposer\n * \"here's what TITAN has done recently —\n * don't re-propose things in this list.\"\n *\n * Why this matters:\n * Curiosity drive kept firing \"Explore Novel Stimuli\" daily. TITAN\n * built three ant colony sims from scratch because it had no episodic\n * \"I already did that yesterday\" recall. The experiment tracker\n * landed a lexical Jaccard version of this; episodic memory upgrades\n * to semantic recall + broadens the event surface beyond experiments.\n */\nimport { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'fs';\nimport { atomicWriteFileSync } from '../utils/helpers.js';\nimport { dirname, join } from 'path';\nimport { randomUUID } from 'crypto';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Episodic';\nconst EPISODIC_PATH = join(TITAN_HOME, 'episodic.jsonl');\nconst EPISODIC_MAX_LINES = 20_000;\n\n// ── Types ────────────────────────────────────────────────────────\n\nexport type EpisodeKind =\n | 'goal_completed'\n | 'goal_failed'\n | 'goal_paused'\n | 'goal_abandoned'\n | 'drive_fired'\n | 'proposal_approved'\n | 'proposal_rejected'\n | 'self_mod_pr_merged'\n | 'self_mod_pr_rejected'\n | 'experiment_succeeded'\n | 'experiment_failed'\n | 'kill_switch_fired'\n | 'kill_switch_resumed'\n | 'fix_oscillation'\n | 'identity_drift'\n | 'canary_degradation'\n | 'significant_learning'\n | 'note';\n\nexport interface Episode {\n id: string;\n at: string;\n kind: EpisodeKind;\n /** Short headline — ≤140 chars. Used as the embedding target. */\n summary: string;\n /** Optional long-form detail — not embedded, just stored. */\n detail?: string;\n /** Which drive / goal / session / specialist triggered this. */\n attribution?: {\n drive?: string;\n goalId?: string;\n sessionId?: string;\n agentId?: string;\n approvalId?: string;\n experimentId?: string;\n };\n /** Free-form tags for filtering (e.g. 'bio-sim', 'node-contract'). */\n tags: string[];\n /** Content hash — lets us detect silent edits to the jsonl file. */\n contentHash?: string;\n}\n\n// ── Storage (append-only jsonl, bounded) ─────────────────────────\n\nfunction ensureDir(): void {\n try { mkdirSync(dirname(EPISODIC_PATH), { recursive: true }); } catch { /* ok */ }\n}\n\nfunction loadAll(): Episode[] {\n if (!existsSync(EPISODIC_PATH)) return [];\n try {\n const raw = readFileSync(EPISODIC_PATH, 'utf-8');\n const out: Episode[] = [];\n for (const line of raw.split('\\n')) {\n if (!line.trim()) continue;\n try {\n const ep = JSON.parse(line) as Episode;\n if (ep.id && ep.at && ep.kind) out.push(ep);\n } catch { /* skip malformed */ }\n }\n return out;\n } catch (err) {\n logger.warn(COMPONENT, `episodic.jsonl parse failed: ${(err as Error).message}`);\n return [];\n }\n}\n\nfunction appendLine(ep: Episode): void {\n ensureDir();\n const line = JSON.stringify(ep) + '\\n';\n try {\n // Append for efficiency; bounded file rewriting handled below.\n // v4.10.0-local fix: use ESM import, not require() — the bundle is\n // ESM and `require('fs')` throws \"Dynamic require not supported\".\n appendFileSync(EPISODIC_PATH, line, 'utf-8');\n } catch (err) {\n logger.warn(COMPONENT, `append failed: ${(err as Error).message}`);\n }\n}\n\n/**\n * Keep the jsonl bounded — periodic tail-rewrite. Called only when the\n * line count is likely over the max, to avoid rewriting every record.\n */\nfunction trimIfNeeded(): void {\n try {\n if (!existsSync(EPISODIC_PATH)) return;\n const raw = readFileSync(EPISODIC_PATH, 'utf-8');\n const lines = raw.split('\\n').filter(Boolean);\n if (lines.length > EPISODIC_MAX_LINES) {\n const tail = lines.slice(-EPISODIC_MAX_LINES).join('\\n') + '\\n';\n atomicWriteFileSync(EPISODIC_PATH, tail);\n logger.info(COMPONENT, `Trimmed episodic.jsonl to last ${EPISODIC_MAX_LINES} episodes`);\n }\n } catch { /* ok */ }\n}\n\n// ── Public API ───────────────────────────────────────────────────\n\n/**\n * Record an episode. Fire-and-forget — never throws. Kicks off an\n * async vector embedding via the existing graph.ts addEpisode pipeline\n * so future semantic recall has the record indexed.\n */\nexport function recordEpisode(opts: {\n kind: EpisodeKind;\n summary: string;\n detail?: string;\n attribution?: Episode['attribution'];\n tags?: string[];\n provenanceSource?: import('./provenance.js').ProvenanceSource;\n provenanceConfidence?: number;\n provenanceWrittenBy?: string;\n}): Episode {\n const ep: Episode = {\n id: randomUUID(),\n at: new Date().toISOString(),\n kind: opts.kind,\n summary: opts.summary.slice(0, 280),\n detail: opts.detail,\n attribution: opts.attribution,\n tags: opts.tags ?? [],\n };\n appendLine(ep);\n\n // Record provenance for this episode if source info is provided.\n if (opts.provenanceSource) {\n void (async () => {\n try {\n const { recordProvenance } = await import('./provenance.js');\n recordProvenance({\n memoryId: ep.id,\n memoryType: 'episode',\n source: opts.provenanceSource!,\n confidence: opts.provenanceConfidence,\n writtenBy: opts.provenanceWrittenBy,\n content: ep.summary,\n });\n } catch { /* ok */ }\n })();\n }\n\n // Opportunistically trim — cheap (just a linecount check) at scale\n // via bounded max. Not per-insert; only every ~100 inserts via\n // randomness so we don't hammer disk.\n if (Math.random() < 0.01) trimIfNeeded();\n\n // Async: push into the graph so the existing vector embedding\n // pipeline indexes the summary. Fire-and-forget; never blocks.\n void indexEpisodeForRecall(ep);\n return ep;\n}\n\nasync function indexEpisodeForRecall(ep: Episode): Promise<void> {\n try {\n const { addEpisode: graphAddEpisode } = await import('./graph.js');\n // Format the summary + kind so retrieved episodes still carry\n // their type when they come back via graph search.\n const content = `[${ep.kind}] ${ep.summary}`;\n await graphAddEpisode(content, `episodic:${ep.id}`);\n } catch (err) {\n logger.debug(COMPONENT, `graph indexing skipped: ${(err as Error).message}`);\n }\n\n // Also push directly into the vector store so `searchVectors`\n // can surface episodic entries in RAG-style queries.\n try {\n const { addVector, isVectorSearchAvailable } = await import('./vectors.js');\n if (isVectorSearchAvailable()) {\n await addVector(`episode:${ep.id}`, ep.summary, 'episodic', {\n kind: ep.kind,\n at: ep.at,\n tags: ep.tags,\n attribution: ep.attribution,\n } as Record<string, unknown>);\n }\n } catch (err) {\n logger.debug(COMPONENT, `vector add skipped: ${(err as Error).message}`);\n }\n}\n\n/**\n * Semantic similarity recall. Returns episodes whose embedded summary\n * is close to the query, filtered by kind + time window. When vector\n * search is unavailable (Ollama down / not configured), falls back to\n * a substring filter so the feature never dark-fails.\n */\nexport async function recallSimilarEpisodes(query: string, opts: {\n limit?: number;\n kinds?: EpisodeKind[];\n windowDays?: number;\n minScore?: number;\n} = {}): Promise<Array<Episode & { score?: number }>> {\n const limit = opts.limit ?? 10;\n const windowMs = (opts.windowDays ?? 30) * 86_400_000;\n const cutoff = Date.now() - windowMs;\n const kindSet = opts.kinds ? new Set(opts.kinds) : null;\n\n // All the records in-memory (we're bounded at ≤20k lines, cheap).\n const all = loadAll().filter(e =>\n new Date(e.at).getTime() >= cutoff\n && (!kindSet || kindSet.has(e.kind)),\n );\n if (all.length === 0) return [];\n\n // Try semantic recall via the vectors store.\n try {\n const { searchVectors, isVectorSearchAvailable } = await import('./vectors.js');\n if (isVectorSearchAvailable()) {\n const hits = await searchVectors(query, limit * 2, 'episodic', opts.minScore ?? 0.35);\n const byId = new Map(all.map(e => [`episode:${e.id}`, e]));\n const out: Array<Episode & { score?: number }> = [];\n for (const h of hits) {\n const ep = byId.get(h.id);\n if (ep) out.push({ ...ep, score: h.score });\n if (out.length >= limit) break;\n }\n if (out.length > 0) return out;\n }\n } catch (err) {\n logger.debug(COMPONENT, `vector recall skipped: ${(err as Error).message}`);\n }\n\n // Fallback: lexical substring match. Ranked by recency.\n const q = query.toLowerCase();\n const matches = all.filter(e =>\n e.summary.toLowerCase().includes(q)\n || (e.detail && e.detail.toLowerCase().includes(q)),\n );\n matches.sort((a, b) => new Date(b.at).getTime() - new Date(a.at).getTime());\n return matches.slice(0, limit);\n}\n\n/**\n * Recent episodes without a semantic query. For when you want a time-\n * window snapshot — e.g. \"what happened in the last 24 hours.\"\n */\nexport function recallRecent(opts: {\n limit?: number;\n kinds?: EpisodeKind[];\n windowHours?: number;\n} = {}): Episode[] {\n const limit = opts.limit ?? 50;\n const windowMs = (opts.windowHours ?? 24) * 3_600_000;\n const cutoff = Date.now() - windowMs;\n const kindSet = opts.kinds ? new Set(opts.kinds) : null;\n return loadAll()\n .filter(e =>\n new Date(e.at).getTime() >= cutoff\n && (!kindSet || kindSet.has(e.kind)),\n )\n .sort((a, b) => new Date(b.at).getTime() - new Date(a.at).getTime())\n .slice(0, limit);\n}\n\n/**\n * Render a prompt-ready block of recent episodes. goalProposer uses\n * this to tell the proposing LLM \"here's what TITAN has done recently;\n * don't re-propose things covered here.\"\n */\nexport function renderRecallBlock(opts: {\n limit?: number;\n kinds?: EpisodeKind[];\n windowHours?: number;\n} = {}): string {\n const recent = recallRecent({ limit: opts.limit ?? 15, kinds: opts.kinds, windowHours: opts.windowHours ?? 72 });\n if (recent.length === 0) return '';\n const lines: string[] = ['## Recent episodic memory (do not re-propose work already covered here)'];\n for (const e of recent) {\n const driveTag = e.attribution?.drive ? ` [${e.attribution.drive}]` : '';\n const goalTag = e.attribution?.goalId ? ` goal=${e.attribution.goalId.slice(0, 8)}` : '';\n lines.push(`- ${e.at.slice(0, 16)} [${e.kind}]${driveTag}${goalTag}: ${e.summary}`);\n }\n return lines.join('\\n');\n}\n\n/**\n * Count of episodes by kind over a window. Used by the self-repair\n * daemon + Safety drive input to notice anomalies (e.g. \"we've had 10\n * goal_failed episodes in 24h — press Safety\").\n */\nexport function getEpisodicStats(windowHours = 24): {\n total: number;\n byKind: Record<string, number>;\n} {\n const cutoff = Date.now() - windowHours * 3_600_000;\n const recent = loadAll().filter(e => new Date(e.at).getTime() >= cutoff);\n const byKind: Record<string, number> = {};\n for (const e of recent) byKind[e.kind] = (byKind[e.kind] ?? 0) + 1;\n return { total: recent.length, byKind };\n}\n\n/** Test-only: wipe the jsonl. */\nexport function _resetEpisodicForTests(): void {\n try { if (existsSync(EPISODIC_PATH)) atomicWriteFileSync(EPISODIC_PATH, ''); } catch { /* ok */ }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/memory/episodic.ts"],"sourcesContent":["/**\n * TITAN — Episodic Memory (v4.9.0+, local hard-takeoff)\n *\n * The \"what did I do and when\" layer. Thin but strategic — it pulls\n * together pieces that already exist:\n * - src/memory/graph.ts — episodes + entities + vector embedding\n * - src/memory/vectors.ts — Ollama-backed semantic search\n * - src/memory/experiments.ts — attempt/outcome/lesson tracker\n * - src/agent/trajectoryLogger.ts — per-session tool trajectories\n *\n * Public API:\n * recordEpisode(event) — append a high-signal event (drive-fired\n * proposal, goal completion, kill, self-mod PR,\n * identity drift resolution, etc.) to the\n * episodic store. Attaches vector embedding\n * so future recall is semantic, not lexical.\n *\n * recallSimilarEpisodes(query, opts) — semantic search across past\n * events, filtered by time window + kind.\n *\n * recallRecent(opts) — time-window recall without a semantic query.\n *\n * renderRecallBlock(...) — formatted recall output for goalProposer\n * context blocks. Used to tell Soma's proposer\n * \"here's what TITAN has done recently —\n * don't re-propose things in this list.\"\n *\n * Why this matters:\n * Curiosity drive kept firing \"Explore Novel Stimuli\" daily. TITAN\n * built three ant colony sims from scratch because it had no episodic\n * \"I already did that yesterday\" recall. The experiment tracker\n * landed a lexical Jaccard version of this; episodic memory upgrades\n * to semantic recall + broadens the event surface beyond experiments.\n */\nimport { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'fs';\nimport { atomicWriteFileSync } from '../utils/helpers.js';\nimport { dirname, join } from 'path';\nimport { randomUUID } from 'crypto';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Episodic';\nconst EPISODIC_PATH = join(TITAN_HOME, 'episodic.jsonl');\nconst EPISODIC_MAX_LINES = 20_000;\n\n// ── Types ────────────────────────────────────────────────────────\n\nexport type EpisodeKind =\n | 'goal_completed'\n | 'goal_failed'\n | 'goal_paused'\n | 'goal_abandoned'\n | 'drive_fired'\n | 'proposal_approved'\n | 'proposal_rejected'\n | 'self_mod_pr_merged'\n | 'self_mod_pr_rejected'\n | 'experiment_succeeded'\n | 'experiment_failed'\n | 'kill_switch_fired'\n | 'kill_switch_resumed'\n | 'fix_oscillation'\n | 'identity_drift'\n | 'canary_degradation'\n | 'significant_learning'\n | 'note';\n\nexport interface Episode {\n id: string;\n at: string;\n kind: EpisodeKind;\n /** Short headline — ≤140 chars. Used as the embedding target. */\n summary: string;\n /** Optional long-form detail — not embedded, just stored. */\n detail?: string;\n /** Which drive / goal / session / specialist triggered this. */\n attribution?: {\n drive?: string;\n goalId?: string;\n sessionId?: string;\n agentId?: string;\n approvalId?: string;\n experimentId?: string;\n };\n /** Free-form tags for filtering (e.g. 'bio-sim', 'node-contract'). */\n tags: string[];\n /** Content hash — lets us detect silent edits to the jsonl file. */\n contentHash?: string;\n}\n\n// ── Storage (append-only jsonl, bounded) ─────────────────────────\n\nfunction mkdirIfNotExists(): void {\n try { mkdirSync(dirname(EPISODIC_PATH), { recursive: true }); } catch { /* ok */ }\n}\n\nfunction loadAll(): Episode[] {\n if (!existsSync(EPISODIC_PATH)) return [];\n try {\n const raw = readFileSync(EPISODIC_PATH, 'utf-8');\n const out: Episode[] = [];\n for (const line of raw.split('\\n')) {\n if (!line.trim()) continue;\n try {\n const ep = JSON.parse(line) as Episode;\n if (ep.id && ep.at && ep.kind) out.push(ep);\n } catch { /* skip malformed */ }\n }\n return out;\n } catch (err) {\n logger.warn(COMPONENT, `episodic.jsonl parse failed: ${(err as Error).message}`);\n return [];\n }\n}\n\nfunction appendLine(ep: Episode): void {\n mkdirIfNotExists();\n const line = JSON.stringify(ep) + '\\n';\n try {\n // Append for efficiency; bounded file rewriting handled below.\n // v4.10.0-local fix: use ESM import, not require() — the bundle is\n // ESM and `require('fs')` throws \"Dynamic require not supported\".\n appendFileSync(EPISODIC_PATH, line, 'utf-8');\n } catch (err) {\n logger.warn(COMPONENT, `append failed: ${(err as Error).message}`);\n }\n}\n\n/**\n * Keep the jsonl bounded — periodic tail-rewrite. Called only when the\n * line count is likely over the max, to avoid rewriting every record.\n */\nfunction trimIfNeeded(): void {\n try {\n if (!existsSync(EPISODIC_PATH)) return;\n const raw = readFileSync(EPISODIC_PATH, 'utf-8');\n const lines = raw.split('\\n').filter(Boolean);\n if (lines.length > EPISODIC_MAX_LINES) {\n const tail = lines.slice(-EPISODIC_MAX_LINES).join('\\n') + '\\n';\n atomicWriteFileSync(EPISODIC_PATH, tail);\n logger.info(COMPONENT, `Trimmed episodic.jsonl to last ${EPISODIC_MAX_LINES} episodes`);\n }\n } catch { /* ok */ }\n}\n\n// ── Public API ───────────────────────────────────────────────────\n\n/**\n * Record an episode. Fire-and-forget — never throws. Kicks off an\n * async vector embedding via the existing graph.ts addEpisode pipeline\n * so future semantic recall has the record indexed.\n */\nexport function recordEpisode(opts: {\n kind: EpisodeKind;\n summary: string;\n detail?: string;\n attribution?: Episode['attribution'];\n tags?: string[];\n provenanceSource?: import('./provenance.js').ProvenanceSource;\n provenanceConfidence?: number;\n provenanceWrittenBy?: string;\n}): Episode {\n const ep: Episode = {\n id: randomUUID(),\n at: new Date().toISOString(),\n kind: opts.kind,\n summary: opts.summary.slice(0, 280),\n detail: opts.detail,\n attribution: opts.attribution,\n tags: opts.tags ?? [],\n };\n appendLine(ep);\n\n // Record provenance for this episode if source info is provided.\n if (opts.provenanceSource) {\n void (async () => {\n try {\n const { recordProvenance } = await import('./provenance.js');\n recordProvenance({\n memoryId: ep.id,\n memoryType: 'episode',\n source: opts.provenanceSource!,\n confidence: opts.provenanceConfidence,\n writtenBy: opts.provenanceWrittenBy,\n content: ep.summary,\n });\n } catch { /* ok */ }\n })();\n }\n\n // Opportunistically trim — cheap (just a linecount check) at scale\n // via bounded max. Not per-insert; only every ~100 inserts via\n // randomness so we don't hammer disk.\n if (Math.random() < 0.01) trimIfNeeded();\n\n // Async: push into the graph so the existing vector embedding\n // pipeline indexes the summary. Fire-and-forget; never blocks.\n void indexEpisodeForRecall(ep);\n return ep;\n}\n\nasync function indexEpisodeForRecall(ep: Episode): Promise<void> {\n try {\n const { addEpisode: graphAddEpisode } = await import('./graph.js');\n // Format the summary + kind so retrieved episodes still carry\n // their type when they come back via graph search.\n const content = `[${ep.kind}] ${ep.summary}`;\n await graphAddEpisode(content, `episodic:${ep.id}`);\n } catch (err) {\n logger.debug(COMPONENT, `graph indexing skipped: ${(err as Error).message}`);\n }\n\n // Also push directly into the vector store so `searchVectors`\n // can surface episodic entries in RAG-style queries.\n try {\n const { addVector, isVectorSearchAvailable } = await import('./vectors.js');\n if (isVectorSearchAvailable()) {\n await addVector(`episode:${ep.id}`, ep.summary, 'episodic', {\n kind: ep.kind,\n at: ep.at,\n tags: ep.tags,\n attribution: ep.attribution,\n } as Record<string, unknown>);\n }\n } catch (err) {\n logger.debug(COMPONENT, `vector add skipped: ${(err as Error).message}`);\n }\n}\n\n/**\n * Semantic similarity recall. Returns episodes whose embedded summary\n * is close to the query, filtered by kind + time window. When vector\n * search is unavailable (Ollama down / not configured), falls back to\n * a substring filter so the feature never dark-fails.\n */\nexport async function recallSimilarEpisodes(query: string, opts: {\n limit?: number;\n kinds?: EpisodeKind[];\n windowDays?: number;\n minScore?: number;\n} = {}): Promise<Array<Episode & { score?: number }>> {\n const limit = opts.limit ?? 10;\n const windowMs = (opts.windowDays ?? 30) * 86_400_000;\n const cutoff = Date.now() - windowMs;\n const kindSet = opts.kinds ? new Set(opts.kinds) : null;\n\n // All the records in-memory (we're bounded at ≤20k lines, cheap).\n const all = loadAll().filter(e =>\n new Date(e.at).getTime() >= cutoff\n && (!kindSet || kindSet.has(e.kind)),\n );\n if (all.length === 0) return [];\n\n // Try semantic recall via the vectors store.\n try {\n const { searchVectors, isVectorSearchAvailable } = await import('./vectors.js');\n if (isVectorSearchAvailable()) {\n const hits = await searchVectors(query, limit * 2, 'episodic', opts.minScore ?? 0.35);\n const byId = new Map(all.map(e => [`episode:${e.id}`, e]));\n const out: Array<Episode & { score?: number }> = [];\n for (const h of hits) {\n const ep = byId.get(h.id);\n if (ep) out.push({ ...ep, score: h.score });\n if (out.length >= limit) break;\n }\n if (out.length > 0) return out;\n }\n } catch (err) {\n logger.debug(COMPONENT, `vector recall skipped: ${(err as Error).message}`);\n }\n\n // Fallback: lexical substring match. Ranked by recency.\n const q = query.toLowerCase();\n const matches = all.filter(e =>\n e.summary.toLowerCase().includes(q)\n || (e.detail && e.detail.toLowerCase().includes(q)),\n );\n matches.sort((a, b) => new Date(b.at).getTime() - new Date(a.at).getTime());\n return matches.slice(0, limit);\n}\n\n/**\n * Recent episodes without a semantic query. For when you want a time-\n * window snapshot — e.g. \"what happened in the last 24 hours.\"\n */\nexport function recallRecent(opts: {\n limit?: number;\n kinds?: EpisodeKind[];\n windowHours?: number;\n} = {}): Episode[] {\n const limit = opts.limit ?? 50;\n const windowMs = (opts.windowHours ?? 24) * 3_600_000;\n const cutoff = Date.now() - windowMs;\n const kindSet = opts.kinds ? new Set(opts.kinds) : null;\n return loadAll()\n .filter(e =>\n new Date(e.at).getTime() >= cutoff\n && (!kindSet || kindSet.has(e.kind)),\n )\n .sort((a, b) => new Date(b.at).getTime() - new Date(a.at).getTime())\n .slice(0, limit);\n}\n\n/**\n * Render a prompt-ready block of recent episodes. goalProposer uses\n * this to tell the proposing LLM \"here's what TITAN has done recently;\n * don't re-propose things covered here.\"\n */\nexport function renderRecallBlock(opts: {\n limit?: number;\n kinds?: EpisodeKind[];\n windowHours?: number;\n} = {}): string {\n const recent = recallRecent({ limit: opts.limit ?? 15, kinds: opts.kinds, windowHours: opts.windowHours ?? 72 });\n if (recent.length === 0) return '';\n const lines: string[] = ['## Recent episodic memory (do not re-propose work already covered here)'];\n for (const e of recent) {\n const driveTag = e.attribution?.drive ? ` [${e.attribution.drive}]` : '';\n const goalTag = e.attribution?.goalId ? ` goal=${e.attribution.goalId.slice(0, 8)}` : '';\n lines.push(`- ${e.at.slice(0, 16)} [${e.kind}]${driveTag}${goalTag}: ${e.summary}`);\n }\n return lines.join('\\n');\n}\n\n/**\n * Count of episodes by kind over a window. Used by the self-repair\n * daemon + Safety drive input to notice anomalies (e.g. \"we've had 10\n * goal_failed episodes in 24h — press Safety\").\n */\nexport function getEpisodicStats(windowHours = 24): {\n total: number;\n byKind: Record<string, number>;\n} {\n const cutoff = Date.now() - windowHours * 3_600_000;\n const recent = loadAll().filter(e => new Date(e.at).getTime() >= cutoff);\n const byKind: Record<string, number> = {};\n for (const e of recent) byKind[e.kind] = (byKind[e.kind] ?? 0) + 1;\n return { total: recent.length, byKind };\n}\n\n/** Test-only: wipe the jsonl. */\nexport function _resetEpisodicForTests(): void {\n try { if (existsSync(EPISODIC_PATH)) atomicWriteFileSync(EPISODIC_PATH, ''); } catch { /* ok */ }\n}\n"],"mappings":"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","names":[]}
@@ -2,7 +2,7 @@
2
2
  import { existsSync, readFileSync, writeFileSync, renameSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { TITAN_HOME } from "../utils/constants.js";
5
- import { ensureDir } from "../utils/helpers.js";
5
+ import { mkdirIfNotExists } from "../utils/helpers.js";
6
6
  import logger from "../utils/logger.js";
7
7
  const COMPONENT = "Learning";
8
8
  const KNOWLEDGE_FILE = join(TITAN_HOME, "knowledge.json");
@@ -25,7 +25,7 @@ function getPatternWords(pattern) {
25
25
  }
26
26
  function loadKnowledgeBase() {
27
27
  if (kb) return kb;
28
- ensureDir(TITAN_HOME);
28
+ mkdirIfNotExists(TITAN_HOME);
29
29
  if (existsSync(KNOWLEDGE_FILE)) {
30
30
  try {
31
31
  kb = JSON.parse(readFileSync(KNOWLEDGE_FILE, "utf-8"));
@@ -57,7 +57,7 @@ function createEmptyKB() {
57
57
  }
58
58
  function doSave() {
59
59
  if (!kb) return;
60
- ensureDir(TITAN_HOME);
60
+ mkdirIfNotExists(TITAN_HOME);
61
61
  try {
62
62
  const tmpFile = KNOWLEDGE_FILE + ".tmp";
63
63
  writeFileSync(tmpFile, JSON.stringify(kb, null, 2), "utf-8");
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/memory/learning.ts"],"sourcesContent":["/**\n * TITAN — Learning Engine\n * Continuous self-improvement: learns from interactions, tracks patterns,\n * builds a knowledge base, and improves tool selection over time.\n */\nimport { existsSync, readFileSync, writeFileSync, renameSync } from 'fs';\nimport { join } from 'path';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport { ensureDir } from '../utils/helpers.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Learning';\nconst KNOWLEDGE_FILE = join(TITAN_HOME, 'knowledge.json');\n\nexport interface LearningEntry {\n id: string;\n type: 'skill_usage' | 'error_pattern' | 'user_correction' | 'successful_pattern' | 'fact' | 'preference';\n category: string;\n content: string;\n context?: string;\n score: number; // confidence/usefulness score (0-1)\n accessCount: number; // how often this knowledge was retrieved\n createdAt: string;\n updatedAt: string;\n}\n\n/** Tool preference by task type (Phase 4 — Active Learning) */\ninterface ToolPreference {\n tool: string;\n successRate: number;\n totalUses: number;\n}\n\n/** Strategy memory — records what approaches worked for task patterns */\ninterface StrategyEntry {\n pattern: string; // Task pattern description\n toolsUsed: string[]; // Deduplicated set\n toolSequence?: string[]; // Ordered sequence of tool calls (preserves order + repeats)\n taskType?: string; // Classified task type (coding, research, etc.)\n roundCount: number;\n success: boolean;\n successCount?: number; // How many times this sequence has succeeded\n failCount?: number; // How many times this strategy failed after being applied\n lastValidated?: string; // ISO timestamp of last successful validation\n timestamp: string;\n}\n\ninterface KnowledgeBase {\n entries: LearningEntry[];\n toolSuccessRates: Record<string, { success: number; fail: number; total: number }>;\n /** Tool success rates segmented by task type (coding, research, analysis, etc.) */\n toolPreferencesByType: Record<string, Record<string, { success: number; total: number }>>;\n /** Strategy memory — top 50 strategies, evicted by age + success */\n strategies: StrategyEntry[];\n errorPatterns: Record<string, { count: number; lastSeen: string; resolution?: string }>;\n userCorrections: Array<{ original: string; correction: string; timestamp: string }>;\n conversationInsights: Array<{ topic: string; outcome: string; toolsUsed: string[]; timestamp: string }>;\n}\n\nlet kb: KnowledgeBase | null = null;\nlet dirty = false;\n\n// ── Strategy hint caches ──────────────────────────────────────────\nlet successfulStrategiesCache: StrategyEntry[] | null = null;\nlet lastDecayRun = 0;\nconst DECAY_INTERVAL_MS = 3600_000; // 1 hour\nconst patternWordsCache = new Map<string, Set<string>>();\n\nfunction invalidateStrategyCaches(): void {\n successfulStrategiesCache = null;\n}\n\nfunction getPatternWords(pattern: string): Set<string> {\n let words = patternWordsCache.get(pattern);\n if (!words) {\n words = new Set(pattern.toLowerCase().split(/\\s+/).filter(w => w.length > 3));\n patternWordsCache.set(pattern, words);\n }\n return words;\n}\n\n// NOTE: Sync I/O is intentional — runs only once at cold start, then cached in-memory.\nfunction loadKnowledgeBase(): KnowledgeBase {\n if (kb) return kb;\n ensureDir(TITAN_HOME);\n if (existsSync(KNOWLEDGE_FILE)) {\n try {\n kb = JSON.parse(readFileSync(KNOWLEDGE_FILE, 'utf-8'));\n // Ensure fields exist\n kb!.entries = kb!.entries || [];\n kb!.toolSuccessRates = kb!.toolSuccessRates || {};\n kb!.toolPreferencesByType = kb!.toolPreferencesByType || {};\n kb!.strategies = kb!.strategies || [];\n kb!.errorPatterns = kb!.errorPatterns || {};\n kb!.userCorrections = kb!.userCorrections || [];\n kb!.conversationInsights = kb!.conversationInsights || [];\n } catch {\n kb = createEmptyKB();\n }\n } else {\n kb = createEmptyKB();\n }\n return kb!;\n}\n\nfunction createEmptyKB(): KnowledgeBase {\n return {\n entries: [],\n toolSuccessRates: {},\n toolPreferencesByType: {},\n strategies: [],\n errorPatterns: {},\n userCorrections: [],\n conversationInsights: [],\n };\n}\n\nfunction doSave(): void {\n if (!kb) return;\n ensureDir(TITAN_HOME);\n try {\n const tmpFile = KNOWLEDGE_FILE + '.tmp';\n writeFileSync(tmpFile, JSON.stringify(kb, null, 2), 'utf-8');\n renameSync(tmpFile, KNOWLEDGE_FILE);\n dirty = false;\n } catch (err) {\n dirty = true;\n logger.error(COMPONENT, `Failed to save knowledge base: ${(err as Error).message}`);\n }\n}\n\nlet saveTimeout: ReturnType<typeof setTimeout> | null = null;\nfunction debouncedSave(): void {\n if (dirty) { doSave(); return; }\n if (saveTimeout) clearTimeout(saveTimeout);\n saveTimeout = setTimeout(doSave, 2000);\n saveTimeout.unref();\n}\n\n/** Initialize the learning engine */\nexport function initLearning(): void {\n loadKnowledgeBase();\n logger.info(COMPONENT, `Learning engine initialized (${kb?.entries.length ?? 0} knowledge entries)`);\n}\n\n/**\n * v4.9.0-local.9: detect \"error\" strings that are actually file content\n * echoed back as context (common when build tools dump failing source).\n * The pre-.9 behavior was to slice(0, 200) and dedupe on that prefix,\n * which made the same file show up many times and inflated Curiosity's\n * \"unresolved patterns\" count by ~5-10x. Observed on 2026-04-18: 327\n * total patterns, of which >50% were just slices of titan-saas source.\n *\n * Heuristic: if the string looks like source code being printed back,\n * don't record it as a distinct error pattern. Specifically:\n * - starts with \"File: /path/to/...\" (compiler output convention)\n * - contains \"--- 1:\" or \"--- 2:\" etc. (line-numbered code dumps)\n * - is >1200 chars (real error strings are usually short; anything\n * longer is almost always dumped source)\n * - is an entire TypeScript/JS import block (≥3 `import ` lines)\n *\n * When detected, we record a GENERIC rollup pattern (\"build-dumped-source:\n * <basename>\") instead of the raw content, so Curiosity sees one entry per\n * file instead of N copies of the same file's content.\n */\nfunction classifyErrorPattern(error: string): { pattern: string; isFileDump: boolean } {\n const trimmed = error.trim();\n const basename = (m: RegExpMatchArray | null): string => {\n const path = m?.[1] ?? '';\n return path.split('/').pop() ?? 'unknown';\n };\n\n // \"File: /path/to/foo.ts (N lines) --- ...\"\n const fileHeaderMatch = trimmed.match(/^File:\\s+(\\S+)\\s+\\(\\d+\\s+lines\\)/);\n if (fileHeaderMatch) {\n return { pattern: `build-dumped-source:${basename(fileHeaderMatch)}`, isFileDump: true };\n }\n\n // Multi-line numbered code dump (\"--- 1: ... --- 2: ...\")\n if (/---\\s*\\d+:/.test(trimmed) && /---\\s*2:/.test(trimmed)) {\n return { pattern: 'build-dumped-source:numbered-code-block', isFileDump: true };\n }\n\n // Import blocks (TypeScript/JS) — ≥3 import lines → probably source\n const importLines = (trimmed.match(/^import\\s+/gm) || []).length;\n if (importLines >= 3) {\n return { pattern: 'build-dumped-source:import-block', isFileDump: true };\n }\n\n // Too long to be a useful signature\n if (trimmed.length > 1200) {\n return { pattern: 'oversized-error:' + trimmed.slice(0, 60).replace(/\\s+/g, ' '), isFileDump: true };\n }\n\n // Normal short error — keep the original 200-char slice behavior\n return { pattern: trimmed.slice(0, 200), isFileDump: false };\n}\n\n/** Record a tool execution result for learning */\nexport function recordToolResult(toolName: string, success: boolean, context?: string, error?: string): void {\n const k = loadKnowledgeBase();\n\n // Update tool success rates\n if (!k.toolSuccessRates[toolName]) {\n k.toolSuccessRates[toolName] = { success: 0, fail: 0, total: 0 };\n }\n k.toolSuccessRates[toolName].total++;\n if (success) {\n k.toolSuccessRates[toolName].success++;\n } else {\n k.toolSuccessRates[toolName].fail++;\n // Track error patterns — with file-content detection (v4.9.0-local.9)\n if (error) {\n const { pattern } = classifyErrorPattern(error);\n if (!k.errorPatterns[pattern]) {\n k.errorPatterns[pattern] = { count: 0, lastSeen: '' };\n }\n k.errorPatterns[pattern].count++;\n k.errorPatterns[pattern].lastSeen = new Date().toISOString();\n }\n }\n\n debouncedSave();\n}\n\n// Exposed for tests + one-time migration tooling\nexport { classifyErrorPattern };\n\n/** Record a successful interaction pattern */\nexport function recordSuccessPattern(pattern: {\n topic: string;\n toolsUsed: string[];\n outcome: string;\n}): void {\n const k = loadKnowledgeBase();\n k.conversationInsights.push({\n ...pattern,\n timestamp: new Date().toISOString(),\n });\n // Keep last 500 insights\n if (k.conversationInsights.length > 500) {\n k.conversationInsights = k.conversationInsights.slice(-500);\n }\n debouncedSave();\n}\n\n/** Record a user correction to learn from mistakes */\nexport function recordUserCorrection(original: string, correction: string): void {\n const k = loadKnowledgeBase();\n k.userCorrections.push({\n original,\n correction,\n timestamp: new Date().toISOString(),\n });\n if (k.userCorrections.length > 200) {\n k.userCorrections = k.userCorrections.slice(-200);\n }\n debouncedSave();\n}\n\n/** Learn a new fact or update an existing one */\nexport function learnFact(category: string, content: string, context?: string): void {\n const k = loadKnowledgeBase();\n const id = `${category}:${content.slice(0, 50)}`;\n const existing = k.entries.findIndex((e) => e.id === id);\n\n if (existing >= 0) {\n k.entries[existing].score = Math.min(k.entries[existing].score + 0.1, 1.0);\n k.entries[existing].accessCount++;\n k.entries[existing].updatedAt = new Date().toISOString();\n } else {\n k.entries.push({\n id,\n type: 'fact',\n category,\n content,\n context,\n score: 0.5,\n accessCount: 0,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n });\n }\n // Keep entries under 2000\n if (k.entries.length > 2000) {\n // Remove lowest-scored entries\n k.entries.sort((a, b) => b.score - a.score);\n k.entries = k.entries.slice(0, 1500);\n }\n debouncedSave();\n}\n\n/** Query the knowledge base for relevant information */\nexport function queryKnowledge(query: string, category?: string, limit: number = 10): LearningEntry[] {\n const k = loadKnowledgeBase();\n const q = query.toLowerCase();\n\n const results = k.entries.filter((e) => {\n const matchesQuery = e.content.toLowerCase().includes(q) ||\n e.category.toLowerCase().includes(q) ||\n (e.context && e.context.toLowerCase().includes(q));\n const matchesCategory = !category || e.category === category;\n return matchesQuery && matchesCategory;\n });\n\n // Sort by score (most useful first), then by recency\n results.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score;\n return b.updatedAt.localeCompare(a.updatedAt);\n });\n\n // Boost access count for retrieved entries\n for (const entry of results.slice(0, limit)) {\n entry.accessCount++;\n entry.score = Math.min(entry.score + 0.01, 1.0);\n }\n\n debouncedSave();\n return results.slice(0, limit);\n}\n\n/** Get tool recommendations based on historical success rates */\nexport function getToolRecommendations(): Record<string, number> {\n const k = loadKnowledgeBase();\n const recommendations: Record<string, number> = {};\n\n for (const [tool, stats] of Object.entries(k.toolSuccessRates)) {\n if (stats.total > 0) {\n recommendations[tool] = stats.success / stats.total;\n }\n }\n\n return recommendations;\n}\n\n/**\n * TITAN pattern: Memory staleness verification\n * Before acting on learned knowledge, verify it's still current.\n * Strategies unvalidated for 30+ days lose 20% successCount.\n * Knowledge entries older than 60 days get flagged as potentially stale.\n */\nexport function verifyMemoryStaleness(): { pruned: number; decayed: number } {\n const k = loadKnowledgeBase();\n const now = Date.now();\n const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000;\n const SIXTY_DAYS = 60 * 24 * 60 * 60 * 1000;\n let pruned = 0;\n let decayed = 0;\n\n // Decay strategies unvalidated for 30+ days\n for (const strategy of k.strategies) {\n const lastValidated = strategy.lastValidated ? new Date(strategy.lastValidated).getTime() : new Date(strategy.timestamp).getTime();\n if (now - lastValidated > THIRTY_DAYS && (strategy.successCount || 0) > 0) {\n strategy.successCount = Math.floor((strategy.successCount || 0) * 0.8);\n decayed++;\n }\n }\n\n // Remove knowledge entries older than 60 days with low scores\n const before = k.entries.length;\n k.entries = k.entries.filter(e => {\n const age = now - new Date(e.createdAt).getTime();\n return age < SIXTY_DAYS || e.score > 0.7;\n });\n pruned = before - k.entries.length;\n\n // Remove error patterns not seen in 30+ days\n for (const [pattern, info] of Object.entries(k.errorPatterns)) {\n if (now - new Date(info.lastSeen).getTime() > THIRTY_DAYS) {\n delete k.errorPatterns[pattern];\n pruned++;\n }\n }\n\n if (pruned > 0 || decayed > 0) {\n doSave();\n logger.info(COMPONENT, `Memory staleness check: ${pruned} pruned, ${decayed} strategies decayed`);\n }\n\n return { pruned, decayed };\n}\n\n/**\n * v4.9.0-local.9: one-time cleanup of \"error patterns\" that are actually\n * file content dumps. Retroactively applies the same classifyErrorPattern\n * logic that now runs on record. Collapses dozens of sliced file-content\n * entries into a single rollup per file.\n *\n * Returns { removed, collapsedInto } where `removed` is the number of raw\n * entries deleted and `collapsedInto` is how many rollup entries were\n * produced (usually much smaller).\n */\nexport function pruneFileContentErrorPatterns(): { removed: number; collapsedInto: number } {\n const k = loadKnowledgeBase();\n const rollup = new Map<string, { count: number; lastSeen: string; resolution?: string }>();\n const keep: Record<string, { count: number; lastSeen: string; resolution?: string }> = {};\n let removed = 0;\n\n for (const [pattern, info] of Object.entries(k.errorPatterns)) {\n const { pattern: canonical, isFileDump } = classifyErrorPattern(pattern);\n if (isFileDump) {\n // Collapse into the rollup bucket\n const existing = rollup.get(canonical);\n if (existing) {\n existing.count += info.count;\n if (info.lastSeen > existing.lastSeen) existing.lastSeen = info.lastSeen;\n if (!existing.resolution && info.resolution) existing.resolution = info.resolution;\n } else {\n rollup.set(canonical, { count: info.count, lastSeen: info.lastSeen, resolution: info.resolution });\n }\n removed++;\n } else {\n keep[pattern] = info;\n }\n }\n // Merge rollups back in (they may collide with real entries — count-sum if so)\n for (const [canonical, info] of rollup) {\n if (keep[canonical]) {\n keep[canonical].count += info.count;\n if (info.lastSeen > keep[canonical].lastSeen) keep[canonical].lastSeen = info.lastSeen;\n } else {\n keep[canonical] = info;\n }\n }\n k.errorPatterns = keep;\n doSave();\n logger.info(COMPONENT, `[PrunePatterns] removed ${removed} file-content entries, collapsed into ${rollup.size} rollup entries`);\n return { removed, collapsedInto: rollup.size };\n}\n\n/** Get learning summary for the system prompt */\nexport function getLearningContext(): string {\n const k = loadKnowledgeBase();\n const parts: string[] = [];\n\n // Recent high-score knowledge\n const topEntries = k.entries\n .filter((e) => e.score > 0.6)\n .sort((a, b) => b.score - a.score)\n .slice(0, 10);\n\n if (topEntries.length > 0) {\n parts.push('Key learned facts:');\n for (const e of topEntries) {\n parts.push(`- [${e.category}] ${e.content}`);\n }\n }\n\n // Tool recommendations\n const toolRecs = getToolRecommendations();\n const bestTools = Object.entries(toolRecs)\n .filter(([_, rate]) => rate > 0.8)\n .sort((a, b) => b[1] - a[1])\n .slice(0, 5);\n\n if (bestTools.length > 0) {\n parts.push('\\nMost reliable tools:');\n for (const [tool, rate] of bestTools) {\n parts.push(`- ${tool}: ${Math.round(rate * 100)}% success rate`);\n }\n }\n\n // Common error patterns to avoid\n const frequentErrors = Object.entries(k.errorPatterns)\n .filter(([_, info]) => info.count > 2)\n .sort((a, b) => b[1].count - a[1].count)\n .slice(0, 3);\n\n if (frequentErrors.length > 0) {\n parts.push('\\nCommon errors to avoid:');\n for (const [pattern, info] of frequentErrors) {\n parts.push(`- ${pattern.slice(0, 100)} (seen ${info.count}x)${info.resolution ? ` → Fix: ${info.resolution}` : ''}`);\n }\n }\n\n return parts.join('\\n');\n}\n\n/** Get per-tool reliability warnings/boosts for tool description injection */\nexport function getToolWarnings(): Record<string, string> {\n const k = loadKnowledgeBase();\n const warnings: Record<string, string> = {};\n\n for (const [tool, stats] of Object.entries(k.toolSuccessRates)) {\n if (stats.total < 10) continue; // Need enough data to be meaningful\n const rate = stats.success / stats.total;\n if (rate < 0.3) {\n warnings[tool] = `[LOW RELIABILITY: ${Math.round(rate * 100)}% success rate over ${stats.total} uses]`;\n } else if (rate > 0.9) {\n warnings[tool] = `[HIGHLY RELIABLE: ${Math.round(rate * 100)}% success rate]`;\n }\n }\n\n return warnings;\n}\n\n/** Record when a tool failure is resolved by using a different tool */\nexport function recordErrorResolution(errorPattern: string, resolution: string): void {\n const k = loadKnowledgeBase();\n const pattern = errorPattern.slice(0, 200);\n if (k.errorPatterns[pattern]) {\n k.errorPatterns[pattern].resolution = resolution;\n } else {\n k.errorPatterns[pattern] = { count: 1, lastSeen: new Date().toISOString(), resolution };\n }\n debouncedSave();\n}\n\n/** Get stats about the learning system */\nexport function getLearningStats(): {\n knowledgeEntries: number;\n toolsTracked: number;\n errorPatterns: number;\n /**\n * v4.10.0-local fix: true count of UNRESOLVED patterns, used by the\n * curiosity drive. Prior behavior was to report `errorPatterns` (total)\n * as if it were unresolved — meaning marking patterns as resolved did\n * nothing to the drive signal. Now separate field.\n */\n unresolvedErrorPatterns: number;\n corrections: number;\n insights: number;\n strategies: number;\n taskTypes: number;\n} {\n const k = loadKnowledgeBase();\n const unresolvedErrorPatterns = Object.values(k.errorPatterns).filter(v => !v.resolution).length;\n return {\n knowledgeEntries: k.entries.length,\n toolsTracked: Object.keys(k.toolSuccessRates).length,\n errorPatterns: Object.keys(k.errorPatterns).length,\n unresolvedErrorPatterns,\n corrections: k.userCorrections.length,\n insights: k.conversationInsights.length,\n strategies: k.strategies.length,\n taskTypes: Object.keys(k.toolPreferencesByType).length,\n };\n}\n\n// ── Phase 4: Active Learning ──────────────────────────────────────\n\n/** Classify a message into a task type for preference tracking */\nexport function classifyTaskType(message: string): string {\n const lower = message.toLowerCase();\n if (/\\b(code|function|class|typescript|python|script|debug|compile|build)\\b/.test(lower)) return 'coding';\n if (/\\b(search|research|find|look up|investigate|compare)\\b/.test(lower)) return 'research';\n if (/\\b(analy[sz]e|data|csv|chart|graph|statistics|metrics)\\b/.test(lower)) return 'analysis';\n if (/\\b(write|draft|blog|article|email|message|story)\\b/.test(lower)) return 'writing';\n if (/\\b(deploy|server|docker|kubernetes|ci|cd|infrastructure)\\b/.test(lower)) return 'devops';\n if (/\\b(file|folder|directory|rename|move|copy|delete)\\b/.test(lower)) return 'filesystem';\n if (/\\b(schedule|cron|automat|workflow|remind)\\b/.test(lower)) return 'automation';\n return 'general';\n}\n\n/** Record a tool result against a specific task type */\nexport function recordToolPreference(toolName: string, taskType: string, success: boolean): void {\n const k = loadKnowledgeBase();\n if (!k.toolPreferencesByType[taskType]) {\n k.toolPreferencesByType[taskType] = {};\n }\n if (!k.toolPreferencesByType[taskType][toolName]) {\n k.toolPreferencesByType[taskType][toolName] = { success: 0, total: 0 };\n }\n\n k.toolPreferencesByType[taskType][toolName].total++;\n if (success) k.toolPreferencesByType[taskType][toolName].success++;\n debouncedSave();\n}\n\n/** Get ranked tool preferences for a task type */\nexport function getToolPreferences(taskType: string): ToolPreference[] {\n const k = loadKnowledgeBase();\n const prefs = k.toolPreferencesByType[taskType];\n if (!prefs) return [];\n\n return Object.entries(prefs)\n .filter(([, stats]) => stats.total >= 3) // Minimum sample size\n .map(([tool, stats]) => ({\n tool,\n successRate: stats.success / stats.total,\n totalUses: stats.total,\n }))\n .sort((a, b) => b.successRate - a.successRate);\n}\n\n/** Generate human-readable preference hints from collected tool-preference data */\nexport function getLearnedPreferenceHints(taskType: string): string | null {\n const prefs = getToolPreferences(taskType);\n if (prefs.length < 2) return null;\n\n const lines: string[] = [];\n const FILE_TOOLS = ['read_file', 'write_file', 'edit_file', 'list_dir'];\n const shellPref = prefs.find(p => p.tool === 'shell');\n\n // Find cases where shell has lower success than dedicated tools\n for (const dt of prefs.filter(p => FILE_TOOLS.includes(p.tool))) {\n if (shellPref && dt.successRate > shellPref.successRate && dt.totalUses >= 3) {\n lines.push(\n `prefer ${dt.tool} (${Math.round(dt.successRate * 100)}% success) ` +\n `over shell (${Math.round(shellPref.successRate * 100)}% success)`,\n );\n }\n }\n\n // Surface top tools for this task type if no shell comparison available\n if (lines.length === 0) {\n const top3 = prefs.slice(0, 3);\n if (top3.length >= 2) {\n lines.push(`best tools: ${top3.map(p => `${p.tool} (${Math.round(p.successRate * 100)}%)`).join(', ')}`);\n }\n }\n\n return lines.length > 0 ? `For ${taskType} tasks: ${lines.join('; ')}` : null;\n}\n\n/** Record a successful strategy for future reference */\nexport function recordStrategy(\n message: string,\n toolsUsed: string[],\n roundCount: number,\n success: boolean,\n toolSequence?: string[],\n): void {\n const k = loadKnowledgeBase();\n const taskType = classifyTaskType(message);\n\n // Check if a similar sequence already exists — merge instead of duplicating\n if (success && toolSequence && toolSequence.length > 0) {\n const seqKey = toolSequence.join('→');\n const existing = k.strategies.find(\n s => s.success && s.taskType === taskType && s.toolSequence?.join('→') === seqKey,\n );\n if (existing) {\n existing.successCount = (existing.successCount || 1) + 1;\n existing.timestamp = new Date().toISOString();\n debouncedSave();\n return;\n }\n }\n\n const entry: StrategyEntry = {\n pattern: message.slice(0, 200),\n toolsUsed: [...new Set(toolsUsed)],\n toolSequence: toolSequence?.slice(0, 20), // Cap sequence length\n taskType,\n roundCount,\n success,\n successCount: success ? 1 : 0,\n timestamp: new Date().toISOString(),\n };\n\n k.strategies.push(entry);\n invalidateStrategyCaches();\n\n // Evict old/failed strategies to keep at 200 max\n if (k.strategies.length > 200) {\n // Sort: keep high-success + recent, evict failed + old + low-success\n k.strategies.sort((a, b) => {\n if (a.success !== b.success) return a.success ? -1 : 1;\n const aCount = a.successCount || 1;\n const bCount = b.successCount || 1;\n if (aCount !== bCount) return bCount - aCount;\n return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime();\n });\n k.strategies = k.strategies.slice(0, 200);\n }\n\n debouncedSave();\n}\n\n/** Record outcome of an applied strategy (feedback loop) */\nexport function recordStrategyOutcome(\n taskType: string,\n toolSequence: string[],\n succeeded: boolean,\n): void {\n const k = loadKnowledgeBase();\n const seqKey = toolSequence.join('→');\n const match = k.strategies.find(\n s => s.taskType === taskType && s.toolSequence?.join('→') === seqKey,\n );\n if (!match) return;\n\n if (succeeded) {\n match.successCount = (match.successCount || 1) + 1;\n match.lastValidated = new Date().toISOString();\n } else {\n match.failCount = (match.failCount || 0) + 1;\n }\n\n // High-fail strategies get marked as unsuccessful\n const becameUnsuccessful = (match.failCount || 0) > (match.successCount || 1) && match.success;\n if (becameUnsuccessful) {\n match.success = false;\n }\n\n if (becameUnsuccessful) invalidateStrategyCaches();\n debouncedSave();\n}\n\n/** Decay unvalidated strategies — called on every getStrategyHints */\nfunction decayStrategies(strategies: StrategyEntry[]): boolean {\n const now = Date.now();\n let changed = false;\n\n for (const s of strategies) {\n const lastValidated = s.lastValidated\n ? new Date(s.lastValidated).getTime()\n : new Date(s.timestamp).getTime();\n const daysSinceValidated = (now - lastValidated) / 86400000;\n\n // Strategies not validated in 30 days: reduce successCount by 20%\n if (daysSinceValidated > 30 && (s.successCount || 1) > 1) {\n s.successCount = Math.max(1, Math.floor((s.successCount || 1) * 0.8));\n changed = true;\n }\n\n // High-fail strategies get excluded\n if ((s.failCount || 0) > (s.successCount || 1) && s.success) {\n s.success = false;\n changed = true;\n }\n }\n\n return changed;\n}\n\n/** Get strategy hints for a similar task */\nexport function getStrategyHints(message: string): string | null {\n const k = loadKnowledgeBase();\n\n // Apply decay to stale strategies\n const now = Date.now();\n if (now - lastDecayRun > DECAY_INTERVAL_MS) {\n lastDecayRun = now;\n if (decayStrategies(k.strategies)) {\n debouncedSave();\n invalidateStrategyCaches();\n }\n }\n if (k.strategies.length === 0) return null;\n\n const taskType = classifyTaskType(message);\n const words = new Set(message.toLowerCase().split(/\\s+/).filter(w => w.length > 3));\n if (words.size === 0) return null;\n\n const successfulStrategies = successfulStrategiesCache ?? (successfulStrategiesCache = k.strategies.filter(s => s.success));\n if (successfulStrategies.length === 0) return null;\n\n // Score strategies by: task type match + keyword overlap + success count\n let bestMatch: StrategyEntry | null = null;\n let bestScore = 0;\n\n for (const strategy of successfulStrategies) {\n const patternWords = getPatternWords(strategy.pattern);\n let overlap = 0;\n for (const w of words) {\n if (patternWords.has(w)) overlap++;\n }\n const keywordScore = overlap / Math.max(words.size, patternWords.size);\n\n // Task type match bonus (0.2)\n const typeBonus = strategy.taskType === taskType ? 0.2 : 0;\n\n // Success count bonus (normalized, max 0.15)\n const countBonus = Math.min((strategy.successCount || 1) / 10, 0.15);\n\n const totalScore = keywordScore + typeBonus + countBonus;\n if (totalScore > bestScore && keywordScore > 0.15) {\n bestScore = totalScore;\n bestMatch = strategy;\n }\n }\n\n if (!bestMatch) return null;\n\n // Prefer showing the ordered sequence if available\n const toolInfo = bestMatch.toolSequence && bestMatch.toolSequence.length > 0\n ? bestMatch.toolSequence.join(' → ')\n : bestMatch.toolsUsed.join(', ');\n\n const countInfo = (bestMatch.successCount || 1) > 1\n ? ` (succeeded ${bestMatch.successCount}x)`\n : '';\n\n return `For similar ${bestMatch.taskType || 'general'} tasks, a proven tool sequence: ${toolInfo} (${bestMatch.roundCount} rounds)${countInfo}.`;\n}\n\n/** Get error resolution if a known pattern matches */\nexport function getErrorResolution(error: string): string | null {\n const k = loadKnowledgeBase();\n const errorLower = error.toLowerCase();\n\n for (const [pattern, info] of Object.entries(k.errorPatterns)) {\n if (info.resolution && errorLower.includes(pattern.toLowerCase().slice(0, 50))) {\n return info.resolution;\n }\n }\n return null;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/memory/learning.ts"],"sourcesContent":["/**\n * TITAN — Learning Engine\n * Continuous self-improvement: learns from interactions, tracks patterns,\n * builds a knowledge base, and improves tool selection over time.\n */\nimport { existsSync, readFileSync, writeFileSync, renameSync } from 'fs';\nimport { join } from 'path';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport { mkdirIfNotExists } from '../utils/helpers.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Learning';\nconst KNOWLEDGE_FILE = join(TITAN_HOME, 'knowledge.json');\n\nexport interface LearningEntry {\n id: string;\n type: 'skill_usage' | 'error_pattern' | 'user_correction' | 'successful_pattern' | 'fact' | 'preference';\n category: string;\n content: string;\n context?: string;\n score: number; // confidence/usefulness score (0-1)\n accessCount: number; // how often this knowledge was retrieved\n createdAt: string;\n updatedAt: string;\n}\n\n/** Tool preference by task type (Phase 4 — Active Learning) */\ninterface ToolPreference {\n tool: string;\n successRate: number;\n totalUses: number;\n}\n\n/** Strategy memory — records what approaches worked for task patterns */\ninterface StrategyEntry {\n pattern: string; // Task pattern description\n toolsUsed: string[]; // Deduplicated set\n toolSequence?: string[]; // Ordered sequence of tool calls (preserves order + repeats)\n taskType?: string; // Classified task type (coding, research, etc.)\n roundCount: number;\n success: boolean;\n successCount?: number; // How many times this sequence has succeeded\n failCount?: number; // How many times this strategy failed after being applied\n lastValidated?: string; // ISO timestamp of last successful validation\n timestamp: string;\n}\n\ninterface KnowledgeBase {\n entries: LearningEntry[];\n toolSuccessRates: Record<string, { success: number; fail: number; total: number }>;\n /** Tool success rates segmented by task type (coding, research, analysis, etc.) */\n toolPreferencesByType: Record<string, Record<string, { success: number; total: number }>>;\n /** Strategy memory — top 50 strategies, evicted by age + success */\n strategies: StrategyEntry[];\n errorPatterns: Record<string, { count: number; lastSeen: string; resolution?: string }>;\n userCorrections: Array<{ original: string; correction: string; timestamp: string }>;\n conversationInsights: Array<{ topic: string; outcome: string; toolsUsed: string[]; timestamp: string }>;\n}\n\nlet kb: KnowledgeBase | null = null;\nlet dirty = false;\n\n// ── Strategy hint caches ──────────────────────────────────────────\nlet successfulStrategiesCache: StrategyEntry[] | null = null;\nlet lastDecayRun = 0;\nconst DECAY_INTERVAL_MS = 3600_000; // 1 hour\nconst patternWordsCache = new Map<string, Set<string>>();\n\nfunction invalidateStrategyCaches(): void {\n successfulStrategiesCache = null;\n}\n\nfunction getPatternWords(pattern: string): Set<string> {\n let words = patternWordsCache.get(pattern);\n if (!words) {\n words = new Set(pattern.toLowerCase().split(/\\s+/).filter(w => w.length > 3));\n patternWordsCache.set(pattern, words);\n }\n return words;\n}\n\n// NOTE: Sync I/O is intentional — runs only once at cold start, then cached in-memory.\nfunction loadKnowledgeBase(): KnowledgeBase {\n if (kb) return kb;\n mkdirIfNotExists(TITAN_HOME);\n if (existsSync(KNOWLEDGE_FILE)) {\n try {\n kb = JSON.parse(readFileSync(KNOWLEDGE_FILE, 'utf-8'));\n // Ensure fields exist\n kb!.entries = kb!.entries || [];\n kb!.toolSuccessRates = kb!.toolSuccessRates || {};\n kb!.toolPreferencesByType = kb!.toolPreferencesByType || {};\n kb!.strategies = kb!.strategies || [];\n kb!.errorPatterns = kb!.errorPatterns || {};\n kb!.userCorrections = kb!.userCorrections || [];\n kb!.conversationInsights = kb!.conversationInsights || [];\n } catch {\n kb = createEmptyKB();\n }\n } else {\n kb = createEmptyKB();\n }\n return kb!;\n}\n\nfunction createEmptyKB(): KnowledgeBase {\n return {\n entries: [],\n toolSuccessRates: {},\n toolPreferencesByType: {},\n strategies: [],\n errorPatterns: {},\n userCorrections: [],\n conversationInsights: [],\n };\n}\n\nfunction doSave(): void {\n if (!kb) return;\n mkdirIfNotExists(TITAN_HOME);\n try {\n const tmpFile = KNOWLEDGE_FILE + '.tmp';\n writeFileSync(tmpFile, JSON.stringify(kb, null, 2), 'utf-8');\n renameSync(tmpFile, KNOWLEDGE_FILE);\n dirty = false;\n } catch (err) {\n dirty = true;\n logger.error(COMPONENT, `Failed to save knowledge base: ${(err as Error).message}`);\n }\n}\n\nlet saveTimeout: ReturnType<typeof setTimeout> | null = null;\nfunction debouncedSave(): void {\n if (dirty) { doSave(); return; }\n if (saveTimeout) clearTimeout(saveTimeout);\n saveTimeout = setTimeout(doSave, 2000);\n saveTimeout.unref();\n}\n\n/** Initialize the learning engine */\nexport function initLearning(): void {\n loadKnowledgeBase();\n logger.info(COMPONENT, `Learning engine initialized (${kb?.entries.length ?? 0} knowledge entries)`);\n}\n\n/**\n * v4.9.0-local.9: detect \"error\" strings that are actually file content\n * echoed back as context (common when build tools dump failing source).\n * The pre-.9 behavior was to slice(0, 200) and dedupe on that prefix,\n * which made the same file show up many times and inflated Curiosity's\n * \"unresolved patterns\" count by ~5-10x. Observed on 2026-04-18: 327\n * total patterns, of which >50% were just slices of titan-saas source.\n *\n * Heuristic: if the string looks like source code being printed back,\n * don't record it as a distinct error pattern. Specifically:\n * - starts with \"File: /path/to/...\" (compiler output convention)\n * - contains \"--- 1:\" or \"--- 2:\" etc. (line-numbered code dumps)\n * - is >1200 chars (real error strings are usually short; anything\n * longer is almost always dumped source)\n * - is an entire TypeScript/JS import block (≥3 `import ` lines)\n *\n * When detected, we record a GENERIC rollup pattern (\"build-dumped-source:\n * <basename>\") instead of the raw content, so Curiosity sees one entry per\n * file instead of N copies of the same file's content.\n */\nfunction classifyErrorPattern(error: string): { pattern: string; isFileDump: boolean } {\n const trimmed = error.trim();\n const basename = (m: RegExpMatchArray | null): string => {\n const path = m?.[1] ?? '';\n return path.split('/').pop() ?? 'unknown';\n };\n\n // \"File: /path/to/foo.ts (N lines) --- ...\"\n const fileHeaderMatch = trimmed.match(/^File:\\s+(\\S+)\\s+\\(\\d+\\s+lines\\)/);\n if (fileHeaderMatch) {\n return { pattern: `build-dumped-source:${basename(fileHeaderMatch)}`, isFileDump: true };\n }\n\n // Multi-line numbered code dump (\"--- 1: ... --- 2: ...\")\n if (/---\\s*\\d+:/.test(trimmed) && /---\\s*2:/.test(trimmed)) {\n return { pattern: 'build-dumped-source:numbered-code-block', isFileDump: true };\n }\n\n // Import blocks (TypeScript/JS) — ≥3 import lines → probably source\n const importLines = (trimmed.match(/^import\\s+/gm) || []).length;\n if (importLines >= 3) {\n return { pattern: 'build-dumped-source:import-block', isFileDump: true };\n }\n\n // Too long to be a useful signature\n if (trimmed.length > 1200) {\n return { pattern: 'oversized-error:' + trimmed.slice(0, 60).replace(/\\s+/g, ' '), isFileDump: true };\n }\n\n // Normal short error — keep the original 200-char slice behavior\n return { pattern: trimmed.slice(0, 200), isFileDump: false };\n}\n\n/** Record a tool execution result for learning */\nexport function recordToolResult(toolName: string, success: boolean, context?: string, error?: string): void {\n const k = loadKnowledgeBase();\n\n // Update tool success rates\n if (!k.toolSuccessRates[toolName]) {\n k.toolSuccessRates[toolName] = { success: 0, fail: 0, total: 0 };\n }\n k.toolSuccessRates[toolName].total++;\n if (success) {\n k.toolSuccessRates[toolName].success++;\n } else {\n k.toolSuccessRates[toolName].fail++;\n // Track error patterns — with file-content detection (v4.9.0-local.9)\n if (error) {\n const { pattern } = classifyErrorPattern(error);\n if (!k.errorPatterns[pattern]) {\n k.errorPatterns[pattern] = { count: 0, lastSeen: '' };\n }\n k.errorPatterns[pattern].count++;\n k.errorPatterns[pattern].lastSeen = new Date().toISOString();\n }\n }\n\n debouncedSave();\n}\n\n// Exposed for tests + one-time migration tooling\nexport { classifyErrorPattern };\n\n/** Record a successful interaction pattern */\nexport function recordSuccessPattern(pattern: {\n topic: string;\n toolsUsed: string[];\n outcome: string;\n}): void {\n const k = loadKnowledgeBase();\n k.conversationInsights.push({\n ...pattern,\n timestamp: new Date().toISOString(),\n });\n // Keep last 500 insights\n if (k.conversationInsights.length > 500) {\n k.conversationInsights = k.conversationInsights.slice(-500);\n }\n debouncedSave();\n}\n\n/** Record a user correction to learn from mistakes */\nexport function recordUserCorrection(original: string, correction: string): void {\n const k = loadKnowledgeBase();\n k.userCorrections.push({\n original,\n correction,\n timestamp: new Date().toISOString(),\n });\n if (k.userCorrections.length > 200) {\n k.userCorrections = k.userCorrections.slice(-200);\n }\n debouncedSave();\n}\n\n/** Learn a new fact or update an existing one */\nexport function learnFact(category: string, content: string, context?: string): void {\n const k = loadKnowledgeBase();\n const id = `${category}:${content.slice(0, 50)}`;\n const existing = k.entries.findIndex((e) => e.id === id);\n\n if (existing >= 0) {\n k.entries[existing].score = Math.min(k.entries[existing].score + 0.1, 1.0);\n k.entries[existing].accessCount++;\n k.entries[existing].updatedAt = new Date().toISOString();\n } else {\n k.entries.push({\n id,\n type: 'fact',\n category,\n content,\n context,\n score: 0.5,\n accessCount: 0,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n });\n }\n // Keep entries under 2000\n if (k.entries.length > 2000) {\n // Remove lowest-scored entries\n k.entries.sort((a, b) => b.score - a.score);\n k.entries = k.entries.slice(0, 1500);\n }\n debouncedSave();\n}\n\n/** Query the knowledge base for relevant information */\nexport function queryKnowledge(query: string, category?: string, limit: number = 10): LearningEntry[] {\n const k = loadKnowledgeBase();\n const q = query.toLowerCase();\n\n const results = k.entries.filter((e) => {\n const matchesQuery = e.content.toLowerCase().includes(q) ||\n e.category.toLowerCase().includes(q) ||\n (e.context && e.context.toLowerCase().includes(q));\n const matchesCategory = !category || e.category === category;\n return matchesQuery && matchesCategory;\n });\n\n // Sort by score (most useful first), then by recency\n results.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score;\n return b.updatedAt.localeCompare(a.updatedAt);\n });\n\n // Boost access count for retrieved entries\n for (const entry of results.slice(0, limit)) {\n entry.accessCount++;\n entry.score = Math.min(entry.score + 0.01, 1.0);\n }\n\n debouncedSave();\n return results.slice(0, limit);\n}\n\n/** Get tool recommendations based on historical success rates */\nexport function getToolRecommendations(): Record<string, number> {\n const k = loadKnowledgeBase();\n const recommendations: Record<string, number> = {};\n\n for (const [tool, stats] of Object.entries(k.toolSuccessRates)) {\n if (stats.total > 0) {\n recommendations[tool] = stats.success / stats.total;\n }\n }\n\n return recommendations;\n}\n\n/**\n * TITAN pattern: Memory staleness verification\n * Before acting on learned knowledge, verify it's still current.\n * Strategies unvalidated for 30+ days lose 20% successCount.\n * Knowledge entries older than 60 days get flagged as potentially stale.\n */\nexport function verifyMemoryStaleness(): { pruned: number; decayed: number } {\n const k = loadKnowledgeBase();\n const now = Date.now();\n const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000;\n const SIXTY_DAYS = 60 * 24 * 60 * 60 * 1000;\n let pruned = 0;\n let decayed = 0;\n\n // Decay strategies unvalidated for 30+ days\n for (const strategy of k.strategies) {\n const lastValidated = strategy.lastValidated ? new Date(strategy.lastValidated).getTime() : new Date(strategy.timestamp).getTime();\n if (now - lastValidated > THIRTY_DAYS && (strategy.successCount || 0) > 0) {\n strategy.successCount = Math.floor((strategy.successCount || 0) * 0.8);\n decayed++;\n }\n }\n\n // Remove knowledge entries older than 60 days with low scores\n const before = k.entries.length;\n k.entries = k.entries.filter(e => {\n const age = now - new Date(e.createdAt).getTime();\n return age < SIXTY_DAYS || e.score > 0.7;\n });\n pruned = before - k.entries.length;\n\n // Remove error patterns not seen in 30+ days\n for (const [pattern, info] of Object.entries(k.errorPatterns)) {\n if (now - new Date(info.lastSeen).getTime() > THIRTY_DAYS) {\n delete k.errorPatterns[pattern];\n pruned++;\n }\n }\n\n if (pruned > 0 || decayed > 0) {\n doSave();\n logger.info(COMPONENT, `Memory staleness check: ${pruned} pruned, ${decayed} strategies decayed`);\n }\n\n return { pruned, decayed };\n}\n\n/**\n * v4.9.0-local.9: one-time cleanup of \"error patterns\" that are actually\n * file content dumps. Retroactively applies the same classifyErrorPattern\n * logic that now runs on record. Collapses dozens of sliced file-content\n * entries into a single rollup per file.\n *\n * Returns { removed, collapsedInto } where `removed` is the number of raw\n * entries deleted and `collapsedInto` is how many rollup entries were\n * produced (usually much smaller).\n */\nexport function pruneFileContentErrorPatterns(): { removed: number; collapsedInto: number } {\n const k = loadKnowledgeBase();\n const rollup = new Map<string, { count: number; lastSeen: string; resolution?: string }>();\n const keep: Record<string, { count: number; lastSeen: string; resolution?: string }> = {};\n let removed = 0;\n\n for (const [pattern, info] of Object.entries(k.errorPatterns)) {\n const { pattern: canonical, isFileDump } = classifyErrorPattern(pattern);\n if (isFileDump) {\n // Collapse into the rollup bucket\n const existing = rollup.get(canonical);\n if (existing) {\n existing.count += info.count;\n if (info.lastSeen > existing.lastSeen) existing.lastSeen = info.lastSeen;\n if (!existing.resolution && info.resolution) existing.resolution = info.resolution;\n } else {\n rollup.set(canonical, { count: info.count, lastSeen: info.lastSeen, resolution: info.resolution });\n }\n removed++;\n } else {\n keep[pattern] = info;\n }\n }\n // Merge rollups back in (they may collide with real entries — count-sum if so)\n for (const [canonical, info] of rollup) {\n if (keep[canonical]) {\n keep[canonical].count += info.count;\n if (info.lastSeen > keep[canonical].lastSeen) keep[canonical].lastSeen = info.lastSeen;\n } else {\n keep[canonical] = info;\n }\n }\n k.errorPatterns = keep;\n doSave();\n logger.info(COMPONENT, `[PrunePatterns] removed ${removed} file-content entries, collapsed into ${rollup.size} rollup entries`);\n return { removed, collapsedInto: rollup.size };\n}\n\n/** Get learning summary for the system prompt */\nexport function getLearningContext(): string {\n const k = loadKnowledgeBase();\n const parts: string[] = [];\n\n // Recent high-score knowledge\n const topEntries = k.entries\n .filter((e) => e.score > 0.6)\n .sort((a, b) => b.score - a.score)\n .slice(0, 10);\n\n if (topEntries.length > 0) {\n parts.push('Key learned facts:');\n for (const e of topEntries) {\n parts.push(`- [${e.category}] ${e.content}`);\n }\n }\n\n // Tool recommendations\n const toolRecs = getToolRecommendations();\n const bestTools = Object.entries(toolRecs)\n .filter(([_, rate]) => rate > 0.8)\n .sort((a, b) => b[1] - a[1])\n .slice(0, 5);\n\n if (bestTools.length > 0) {\n parts.push('\\nMost reliable tools:');\n for (const [tool, rate] of bestTools) {\n parts.push(`- ${tool}: ${Math.round(rate * 100)}% success rate`);\n }\n }\n\n // Common error patterns to avoid\n const frequentErrors = Object.entries(k.errorPatterns)\n .filter(([_, info]) => info.count > 2)\n .sort((a, b) => b[1].count - a[1].count)\n .slice(0, 3);\n\n if (frequentErrors.length > 0) {\n parts.push('\\nCommon errors to avoid:');\n for (const [pattern, info] of frequentErrors) {\n parts.push(`- ${pattern.slice(0, 100)} (seen ${info.count}x)${info.resolution ? ` → Fix: ${info.resolution}` : ''}`);\n }\n }\n\n return parts.join('\\n');\n}\n\n/** Get per-tool reliability warnings/boosts for tool description injection */\nexport function getToolWarnings(): Record<string, string> {\n const k = loadKnowledgeBase();\n const warnings: Record<string, string> = {};\n\n for (const [tool, stats] of Object.entries(k.toolSuccessRates)) {\n if (stats.total < 10) continue; // Need enough data to be meaningful\n const rate = stats.success / stats.total;\n if (rate < 0.3) {\n warnings[tool] = `[LOW RELIABILITY: ${Math.round(rate * 100)}% success rate over ${stats.total} uses]`;\n } else if (rate > 0.9) {\n warnings[tool] = `[HIGHLY RELIABLE: ${Math.round(rate * 100)}% success rate]`;\n }\n }\n\n return warnings;\n}\n\n/** Record when a tool failure is resolved by using a different tool */\nexport function recordErrorResolution(errorPattern: string, resolution: string): void {\n const k = loadKnowledgeBase();\n const pattern = errorPattern.slice(0, 200);\n if (k.errorPatterns[pattern]) {\n k.errorPatterns[pattern].resolution = resolution;\n } else {\n k.errorPatterns[pattern] = { count: 1, lastSeen: new Date().toISOString(), resolution };\n }\n debouncedSave();\n}\n\n/** Get stats about the learning system */\nexport function getLearningStats(): {\n knowledgeEntries: number;\n toolsTracked: number;\n errorPatterns: number;\n /**\n * v4.10.0-local fix: true count of UNRESOLVED patterns, used by the\n * curiosity drive. Prior behavior was to report `errorPatterns` (total)\n * as if it were unresolved — meaning marking patterns as resolved did\n * nothing to the drive signal. Now separate field.\n */\n unresolvedErrorPatterns: number;\n corrections: number;\n insights: number;\n strategies: number;\n taskTypes: number;\n} {\n const k = loadKnowledgeBase();\n const unresolvedErrorPatterns = Object.values(k.errorPatterns).filter(v => !v.resolution).length;\n return {\n knowledgeEntries: k.entries.length,\n toolsTracked: Object.keys(k.toolSuccessRates).length,\n errorPatterns: Object.keys(k.errorPatterns).length,\n unresolvedErrorPatterns,\n corrections: k.userCorrections.length,\n insights: k.conversationInsights.length,\n strategies: k.strategies.length,\n taskTypes: Object.keys(k.toolPreferencesByType).length,\n };\n}\n\n// ── Phase 4: Active Learning ──────────────────────────────────────\n\n/** Classify a message into a task type for preference tracking */\nexport function classifyTaskType(message: string): string {\n const lower = message.toLowerCase();\n if (/\\b(code|function|class|typescript|python|script|debug|compile|build)\\b/.test(lower)) return 'coding';\n if (/\\b(search|research|find|look up|investigate|compare)\\b/.test(lower)) return 'research';\n if (/\\b(analy[sz]e|data|csv|chart|graph|statistics|metrics)\\b/.test(lower)) return 'analysis';\n if (/\\b(write|draft|blog|article|email|message|story)\\b/.test(lower)) return 'writing';\n if (/\\b(deploy|server|docker|kubernetes|ci|cd|infrastructure)\\b/.test(lower)) return 'devops';\n if (/\\b(file|folder|directory|rename|move|copy|delete)\\b/.test(lower)) return 'filesystem';\n if (/\\b(schedule|cron|automat|workflow|remind)\\b/.test(lower)) return 'automation';\n return 'general';\n}\n\n/** Record a tool result against a specific task type */\nexport function recordToolPreference(toolName: string, taskType: string, success: boolean): void {\n const k = loadKnowledgeBase();\n if (!k.toolPreferencesByType[taskType]) {\n k.toolPreferencesByType[taskType] = {};\n }\n if (!k.toolPreferencesByType[taskType][toolName]) {\n k.toolPreferencesByType[taskType][toolName] = { success: 0, total: 0 };\n }\n\n k.toolPreferencesByType[taskType][toolName].total++;\n if (success) k.toolPreferencesByType[taskType][toolName].success++;\n debouncedSave();\n}\n\n/** Get ranked tool preferences for a task type */\nexport function getToolPreferences(taskType: string): ToolPreference[] {\n const k = loadKnowledgeBase();\n const prefs = k.toolPreferencesByType[taskType];\n if (!prefs) return [];\n\n return Object.entries(prefs)\n .filter(([, stats]) => stats.total >= 3) // Minimum sample size\n .map(([tool, stats]) => ({\n tool,\n successRate: stats.success / stats.total,\n totalUses: stats.total,\n }))\n .sort((a, b) => b.successRate - a.successRate);\n}\n\n/** Generate human-readable preference hints from collected tool-preference data */\nexport function getLearnedPreferenceHints(taskType: string): string | null {\n const prefs = getToolPreferences(taskType);\n if (prefs.length < 2) return null;\n\n const lines: string[] = [];\n const FILE_TOOLS = ['read_file', 'write_file', 'edit_file', 'list_dir'];\n const shellPref = prefs.find(p => p.tool === 'shell');\n\n // Find cases where shell has lower success than dedicated tools\n for (const dt of prefs.filter(p => FILE_TOOLS.includes(p.tool))) {\n if (shellPref && dt.successRate > shellPref.successRate && dt.totalUses >= 3) {\n lines.push(\n `prefer ${dt.tool} (${Math.round(dt.successRate * 100)}% success) ` +\n `over shell (${Math.round(shellPref.successRate * 100)}% success)`,\n );\n }\n }\n\n // Surface top tools for this task type if no shell comparison available\n if (lines.length === 0) {\n const top3 = prefs.slice(0, 3);\n if (top3.length >= 2) {\n lines.push(`best tools: ${top3.map(p => `${p.tool} (${Math.round(p.successRate * 100)}%)`).join(', ')}`);\n }\n }\n\n return lines.length > 0 ? `For ${taskType} tasks: ${lines.join('; ')}` : null;\n}\n\n/** Record a successful strategy for future reference */\nexport function recordStrategy(\n message: string,\n toolsUsed: string[],\n roundCount: number,\n success: boolean,\n toolSequence?: string[],\n): void {\n const k = loadKnowledgeBase();\n const taskType = classifyTaskType(message);\n\n // Check if a similar sequence already exists — merge instead of duplicating\n if (success && toolSequence && toolSequence.length > 0) {\n const seqKey = toolSequence.join('→');\n const existing = k.strategies.find(\n s => s.success && s.taskType === taskType && s.toolSequence?.join('→') === seqKey,\n );\n if (existing) {\n existing.successCount = (existing.successCount || 1) + 1;\n existing.timestamp = new Date().toISOString();\n debouncedSave();\n return;\n }\n }\n\n const entry: StrategyEntry = {\n pattern: message.slice(0, 200),\n toolsUsed: [...new Set(toolsUsed)],\n toolSequence: toolSequence?.slice(0, 20), // Cap sequence length\n taskType,\n roundCount,\n success,\n successCount: success ? 1 : 0,\n timestamp: new Date().toISOString(),\n };\n\n k.strategies.push(entry);\n invalidateStrategyCaches();\n\n // Evict old/failed strategies to keep at 200 max\n if (k.strategies.length > 200) {\n // Sort: keep high-success + recent, evict failed + old + low-success\n k.strategies.sort((a, b) => {\n if (a.success !== b.success) return a.success ? -1 : 1;\n const aCount = a.successCount || 1;\n const bCount = b.successCount || 1;\n if (aCount !== bCount) return bCount - aCount;\n return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime();\n });\n k.strategies = k.strategies.slice(0, 200);\n }\n\n debouncedSave();\n}\n\n/** Record outcome of an applied strategy (feedback loop) */\nexport function recordStrategyOutcome(\n taskType: string,\n toolSequence: string[],\n succeeded: boolean,\n): void {\n const k = loadKnowledgeBase();\n const seqKey = toolSequence.join('→');\n const match = k.strategies.find(\n s => s.taskType === taskType && s.toolSequence?.join('→') === seqKey,\n );\n if (!match) return;\n\n if (succeeded) {\n match.successCount = (match.successCount || 1) + 1;\n match.lastValidated = new Date().toISOString();\n } else {\n match.failCount = (match.failCount || 0) + 1;\n }\n\n // High-fail strategies get marked as unsuccessful\n const becameUnsuccessful = (match.failCount || 0) > (match.successCount || 1) && match.success;\n if (becameUnsuccessful) {\n match.success = false;\n }\n\n if (becameUnsuccessful) invalidateStrategyCaches();\n debouncedSave();\n}\n\n/** Decay unvalidated strategies — called on every getStrategyHints */\nfunction decayStrategies(strategies: StrategyEntry[]): boolean {\n const now = Date.now();\n let changed = false;\n\n for (const s of strategies) {\n const lastValidated = s.lastValidated\n ? new Date(s.lastValidated).getTime()\n : new Date(s.timestamp).getTime();\n const daysSinceValidated = (now - lastValidated) / 86400000;\n\n // Strategies not validated in 30 days: reduce successCount by 20%\n if (daysSinceValidated > 30 && (s.successCount || 1) > 1) {\n s.successCount = Math.max(1, Math.floor((s.successCount || 1) * 0.8));\n changed = true;\n }\n\n // High-fail strategies get excluded\n if ((s.failCount || 0) > (s.successCount || 1) && s.success) {\n s.success = false;\n changed = true;\n }\n }\n\n return changed;\n}\n\n/** Get strategy hints for a similar task */\nexport function getStrategyHints(message: string): string | null {\n const k = loadKnowledgeBase();\n\n // Apply decay to stale strategies\n const now = Date.now();\n if (now - lastDecayRun > DECAY_INTERVAL_MS) {\n lastDecayRun = now;\n if (decayStrategies(k.strategies)) {\n debouncedSave();\n invalidateStrategyCaches();\n }\n }\n if (k.strategies.length === 0) return null;\n\n const taskType = classifyTaskType(message);\n const words = new Set(message.toLowerCase().split(/\\s+/).filter(w => w.length > 3));\n if (words.size === 0) return null;\n\n const successfulStrategies = successfulStrategiesCache ?? (successfulStrategiesCache = k.strategies.filter(s => s.success));\n if (successfulStrategies.length === 0) return null;\n\n // Score strategies by: task type match + keyword overlap + success count\n let bestMatch: StrategyEntry | null = null;\n let bestScore = 0;\n\n for (const strategy of successfulStrategies) {\n const patternWords = getPatternWords(strategy.pattern);\n let overlap = 0;\n for (const w of words) {\n if (patternWords.has(w)) overlap++;\n }\n const keywordScore = overlap / Math.max(words.size, patternWords.size);\n\n // Task type match bonus (0.2)\n const typeBonus = strategy.taskType === taskType ? 0.2 : 0;\n\n // Success count bonus (normalized, max 0.15)\n const countBonus = Math.min((strategy.successCount || 1) / 10, 0.15);\n\n const totalScore = keywordScore + typeBonus + countBonus;\n if (totalScore > bestScore && keywordScore > 0.15) {\n bestScore = totalScore;\n bestMatch = strategy;\n }\n }\n\n if (!bestMatch) return null;\n\n // Prefer showing the ordered sequence if available\n const toolInfo = bestMatch.toolSequence && bestMatch.toolSequence.length > 0\n ? bestMatch.toolSequence.join(' → ')\n : bestMatch.toolsUsed.join(', ');\n\n const countInfo = (bestMatch.successCount || 1) > 1\n ? ` (succeeded ${bestMatch.successCount}x)`\n : '';\n\n return `For similar ${bestMatch.taskType || 'general'} tasks, a proven tool sequence: ${toolInfo} (${bestMatch.roundCount} rounds)${countInfo}.`;\n}\n\n/** Get error resolution if a known pattern matches */\nexport function getErrorResolution(error: string): string | null {\n const k = loadKnowledgeBase();\n const errorLower = error.toLowerCase();\n\n for (const [pattern, info] of Object.entries(k.errorPatterns)) {\n if (info.resolution && errorLower.includes(pattern.toLowerCase().slice(0, 50))) {\n return info.resolution;\n }\n }\n return null;\n}\n"],"mappings":"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","names":[]}
@@ -2,7 +2,7 @@
2
2
  import { existsSync, readFileSync, writeFileSync, renameSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { TITAN_HOME } from "../utils/constants.js";
5
- import { ensureDir } from "../utils/helpers.js";
5
+ import { mkdirIfNotExists } from "../utils/helpers.js";
6
6
  import logger from "../utils/logger.js";
7
7
  import { encrypt, decrypt } from "../security/encryption.js";
8
8
  import { isVectorSearchAvailable, searchVectors, addVector } from "./vectors.js";
@@ -23,7 +23,7 @@ function getDefaultStore() {
23
23
  }
24
24
  function loadStore() {
25
25
  if (store) return store;
26
- ensureDir(TITAN_HOME);
26
+ mkdirIfNotExists(TITAN_HOME);
27
27
  if (existsSync(DB_FILE)) {
28
28
  try {
29
29
  const raw = readFileSync(DB_FILE, "utf-8");
@@ -45,7 +45,7 @@ function loadStore() {
45
45
  }
46
46
  function saveStore() {
47
47
  if (!store || isShuttingDown) return;
48
- ensureDir(TITAN_HOME);
48
+ mkdirIfNotExists(TITAN_HOME);
49
49
  try {
50
50
  const tmpFile = DB_FILE + ".tmp";
51
51
  writeFileSync(tmpFile, JSON.stringify(store, null, 2), "utf-8");
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/memory/memory.ts"],"sourcesContent":["/**\n * TITAN — Memory / Persistence System\n * JSON-file-backed persistent memory for conversations, facts, preferences, and usage.\n * Uses no native dependencies — pure Node.js for maximum portability.\n */\nimport { existsSync, readFileSync, writeFileSync, renameSync } from 'fs';\nimport { join } from 'path';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport { ensureDir } from '../utils/helpers.js';\nimport logger from '../utils/logger.js';\nimport { encrypt, decrypt, type EncryptedPayload } from '../security/encryption.js';\nimport { isVectorSearchAvailable, searchVectors, addVector } from './vectors.js';\n\nconst COMPONENT = 'Memory';\n\n// ─── Data Store ──────────────────────────────────────────────────\n\ninterface DataStore {\n conversations: ConversationMessage[];\n memories: MemoryEntry[];\n sessions: SessionRecord[];\n usageStats: UsageRecord[];\n cronJobs: CronRecord[];\n skillsInstalled: SkillRecord[];\n}\n\ninterface MemoryEntry {\n id: string;\n category: string;\n key: string;\n value: string;\n metadata?: string;\n createdAt: string;\n updatedAt: string;\n}\n\ninterface SessionRecord {\n id: string;\n channel: string;\n user_id: string;\n agent_id: string;\n status: string;\n message_count: number;\n created_at: string;\n last_active: string;\n name?: string;\n last_message?: string;\n // D3: Persisted session overrides (survive session recovery after timeout/restart)\n model_override?: string;\n thinking_override?: string;\n // Hunt Finding #19 (2026-04-14): true when this session was created via an\n // explicit sessionId (getOrCreateSessionById). Named sessions MUST NOT be\n // returned by the default-slot lookup in getOrCreateSession — otherwise\n // subsequent no-sessionId requests from the same channel+user+agent will\n // inherit the most recent named session's history, causing privacy bleed\n // between API callers.\n is_named?: boolean;\n}\n\ninterface UsageRecord {\n id: number;\n session_id: string;\n provider: string;\n model: string;\n prompt_tokens: number;\n completion_tokens: number;\n total_tokens: number;\n created_at: string;\n}\n\ninterface CronRecord {\n id: string;\n name: string;\n schedule: string;\n command: string;\n mode?: 'shell' | 'tool'; // Execution mode (default: shell for backward compat)\n allowedTools?: string[]; // Tool allowlist for tool-mode jobs\n enabled: boolean;\n last_run?: string;\n next_run?: string;\n created_at: string;\n}\n\ninterface SkillRecord {\n name: string;\n version: string;\n source: string;\n enabled: boolean;\n installed_at: string;\n}\n\nconst DB_FILE = join(TITAN_HOME, 'titan-data.json');\n\nlet store: DataStore | null = null;\nlet dirty = false;\nlet isShuttingDown = false;\n\nfunction getDefaultStore(): DataStore {\n return {\n conversations: [],\n memories: [],\n sessions: [],\n usageStats: [],\n cronJobs: [],\n skillsInstalled: [],\n };\n}\n\n// NOTE: Sync I/O is intentional — runs only once at cold start, then cached in-memory.\nfunction loadStore(): DataStore {\n if (store) return store;\n ensureDir(TITAN_HOME);\n if (existsSync(DB_FILE)) {\n try {\n const raw = readFileSync(DB_FILE, 'utf-8');\n store = JSON.parse(raw) as DataStore;\n // Ensure all fields exist\n store.conversations = store.conversations || [];\n store.memories = store.memories || [];\n store.sessions = store.sessions || [];\n store.usageStats = store.usageStats || [];\n store.cronJobs = store.cronJobs || [];\n store.skillsInstalled = store.skillsInstalled || [];\n } catch {\n logger.warn(COMPONENT, 'Could not load data store, creating fresh one');\n store = getDefaultStore();\n }\n } else {\n store = getDefaultStore();\n }\n return store;\n}\n\nfunction saveStore(): void {\n if (!store || isShuttingDown) return;\n ensureDir(TITAN_HOME);\n try {\n const tmpFile = DB_FILE + '.tmp';\n writeFileSync(tmpFile, JSON.stringify(store, null, 2), 'utf-8');\n renameSync(tmpFile, DB_FILE);\n dirty = false;\n } catch (e) {\n dirty = true;\n logger.error(COMPONENT, `Failed to save data: ${(e as Error).message}`);\n }\n}\n\n// Auto-save periodically\nlet saveTimeout: ReturnType<typeof setTimeout> | null = null;\nexport function debouncedSave(): void {\n if (dirty) { saveStore(); return; }\n if (saveTimeout) clearTimeout(saveTimeout);\n saveTimeout = setTimeout(saveStore, 1000);\n saveTimeout.unref();\n}\n\n/** Initialize the memory system */\nexport function initMemory(): void {\n loadStore();\n logger.info(COMPONENT, 'Memory system initialized');\n}\n\n/** Close / flush the memory system */\nexport function closeMemory(): void {\n if (saveTimeout) { clearTimeout(saveTimeout); saveTimeout = null; }\n saveStore();\n if (dirty) {\n logger.error(COMPONENT, 'DATA MAY BE LOST — failed to flush memory store on shutdown');\n }\n isShuttingDown = true;\n}\n\n/** Get internal store (for skills like cron that need direct access) */\nexport function getDb(): DataStore {\n return loadStore();\n}\n\n// ─── Conversation History ────────────────────────────────────────\n\nexport interface ConversationMessage {\n id: string;\n sessionId: string;\n role: string;\n content: string;\n toolCalls?: string;\n toolCallId?: string;\n model?: string;\n tokenCount: number;\n createdAt: string;\n isEncrypted?: boolean;\n}\n\n/** Save a message to conversation history */\nexport function saveMessage(message: Omit<ConversationMessage, 'createdAt'>, e2eKey?: string): void {\n const s = loadStore();\n\n let content = message.content;\n let isEncrypted = false;\n\n if (e2eKey) {\n try {\n const payload = encrypt(message.content, Buffer.from(e2eKey, 'base64'));\n content = JSON.stringify(payload);\n isEncrypted = true;\n } catch {\n logger.error(COMPONENT, `Failed to encrypt message for storage`);\n content = \"[ENCRYPTION FAILED] \" + content; // Fallback, though we should probably throw in strict environments\n }\n }\n\n s.conversations.push({\n ...message,\n content,\n isEncrypted,\n createdAt: new Date().toISOString(),\n });\n // Keep only last 5000 messages total to prevent unbounded growth\n if (s.conversations.length > 5000) {\n s.conversations = s.conversations.slice(-5000);\n }\n debouncedSave();\n}\n\n/** Get conversation history for a session */\nexport function getHistory(sessionId: string, limit: number = 50, e2eKey?: string): ConversationMessage[] {\n const s = loadStore();\n const rawHistory = s.conversations\n .filter((m) => m.sessionId === sessionId)\n .slice(-limit);\n\n if (!e2eKey) {\n // If no key is provided, we just return the raw payload. \n // If it's encrypted, it'll just show the JSON string of the EncryptedPayload.\n return rawHistory;\n }\n\n // Decrypt the ones that were encrypted\n return rawHistory.map(m => {\n if (m.isEncrypted) {\n try {\n const payload = JSON.parse(m.content) as EncryptedPayload;\n return {\n ...m,\n content: decrypt(payload, Buffer.from(e2eKey, 'base64'))\n };\n } catch {\n logger.error(COMPONENT, `Failed to decrypt message ${m.id}`);\n return { ...m, content: \"[DECRYPTION FAILED]\" };\n }\n }\n return m;\n });\n}\n\n/** Update session name and/or last message snippet */\nexport function updateSessionMeta(sessionId: string, meta: { name?: string; last_message?: string; model_override?: string; thinking_override?: string }): void {\n const s = loadStore();\n const rec = s.sessions.find(ses => ses.id === sessionId);\n if (!rec) return;\n if (meta.name !== undefined) rec.name = meta.name;\n if (meta.last_message !== undefined) rec.last_message = meta.last_message;\n // D3: Persist session overrides to database so they survive timeout/restart\n if (meta.model_override !== undefined) rec.model_override = meta.model_override;\n if (meta.thinking_override !== undefined) rec.thinking_override = meta.thinking_override;\n debouncedSave();\n}\n\n/** Clear conversation history for a session */\nexport function clearHistory(sessionId: string): void {\n const s = loadStore();\n s.conversations = s.conversations.filter((m) => m.sessionId !== sessionId);\n debouncedSave();\n}\n\n// ─── Persistent Memory (Facts / Preferences) ─────────────────────\n\n/** Store a memory (key-value with category) */\nexport function rememberFact(category: string, key: string, value: string, metadata?: Record<string, unknown>): void {\n const s = loadStore();\n const id = `${category}:${key}`;\n const existingIdx = s.memories.findIndex((m) => m.id === id);\n const now = new Date().toISOString();\n\n if (existingIdx >= 0) {\n s.memories[existingIdx].value = value;\n s.memories[existingIdx].metadata = metadata ? JSON.stringify(metadata) : undefined;\n s.memories[existingIdx].updatedAt = now;\n } else {\n s.memories.push({\n id,\n category,\n key,\n value,\n metadata: metadata ? JSON.stringify(metadata) : undefined,\n createdAt: now,\n updatedAt: now,\n });\n }\n debouncedSave();\n\n // Index to vector store (fire-and-forget)\n if (isVectorSearchAvailable()) {\n addVector(id, `${category}: ${key} = ${value}`, 'memory', { category, key }).catch(e => logger.debug(COMPONENT, `Background vector indexing failed: ${(e as Error).message}`));\n }\n}\n\n/** Recall a specific memory */\nexport function recallFact(category: string, key: string): string | null {\n const s = loadStore();\n const entry = s.memories.find((m) => m.category === category && m.key === key);\n return entry?.value || null;\n}\n\n/** Search memories by category — hybrid keyword + vector search */\nexport async function searchMemories(category?: string, query?: string): Promise<Array<{ key: string; value: string; category: string; score?: number }>> {\n const s = loadStore();\n let results = s.memories;\n\n if (category) {\n results = results.filter((m) => m.category === category);\n }\n if (query) {\n const q = query.toLowerCase();\n // Word-boundary match to avoid false positives (\"use\" matching \"user\", \"reuse\")\n const qRegex = new RegExp('\\\\b' + q.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&') + '\\\\b', 'i');\n results = results.filter((m) =>\n qRegex.test(m.key) || qRegex.test(m.value)\n );\n }\n\n // Keyword scoring\n const scored = results.map(m => {\n let score = 0;\n if (query) {\n const q = query.toLowerCase();\n const keyLower = m.key.toLowerCase();\n const valLower = m.value.toLowerCase();\n // Exact key match scores highest\n if (keyLower === q) score += 5;\n else if (keyLower.includes(q)) score += 2;\n if (valLower.includes(q)) score += 1;\n // BM25-style: boost for multiple keyword matches\n const terms = q.split(/\\s+/).filter(Boolean);\n for (const term of terms) {\n if (keyLower.includes(term)) score += 1;\n if (valLower.includes(term)) score += 0.5;\n }\n }\n return { key: m.key, value: m.value, category: m.category, id: m.id, score };\n });\n\n // Vector search augmentation (hybrid mode)\n if (query && isVectorSearchAvailable()) {\n try {\n const vectorResults = await searchVectors(query, 20, 'memory', 0.4);\n for (const vr of vectorResults) {\n // Skip stale vector IDs that no longer exist in the store\n const memEntry = s.memories.find(m => m.id === vr.id);\n if (!memEntry) continue;\n const existing = scored.find(s => s.id === vr.id);\n if (existing) {\n // Boost keyword results that also match semantically\n existing.score += vr.score * 3;\n } else {\n // Add vector-only results (semantically similar but no keyword match)\n const entry = s.memories.find(m => m.id === vr.id);\n if (entry && (!category || entry.category === category)) {\n scored.push({\n key: entry.key,\n value: entry.value,\n category: entry.category,\n id: entry.id,\n score: vr.score * 2,\n });\n }\n }\n }\n } catch {\n // Vector search failure is non-fatal\n }\n }\n\n scored.sort((a, b) => b.score - a.score);\n // Deduplicate by ID (vector + keyword can match the same entry)\n const seen = new Set<string>();\n const unique = scored.filter(m => { if (seen.has(m.id)) return false; seen.add(m.id); return true; });\n return unique.slice(0, 50).map(m => ({ key: m.key, value: m.value, category: m.category, score: m.score }));\n}\n\n// ─── Usage Tracking ──────────────────────────────────────────────\n\n/** Record usage statistics */\nexport function recordUsage(sessionId: string, provider: string, model: string, promptTokens: number, completionTokens: number): void {\n const s = loadStore();\n s.usageStats.push({\n id: Date.now(),\n session_id: sessionId,\n provider,\n model,\n prompt_tokens: promptTokens,\n completion_tokens: completionTokens,\n total_tokens: promptTokens + completionTokens,\n created_at: new Date().toISOString(),\n });\n // Keep only last 10000 records\n if (s.usageStats.length > 10000) {\n s.usageStats = s.usageStats.slice(-10000);\n }\n debouncedSave();\n}\n\n/** Get total usage statistics */\nexport function getUsageStats(): { totalTokens: number; totalRequests: number; byProvider: Record<string, number> } {\n const s = loadStore();\n let totalTokens = 0;\n const byProvider: Record<string, number> = {};\n\n for (const rec of s.usageStats) {\n totalTokens += rec.total_tokens;\n byProvider[rec.provider] = (byProvider[rec.provider] || 0) + rec.total_tokens;\n }\n\n return {\n totalTokens,\n totalRequests: s.usageStats.length,\n byProvider,\n };\n}\n"],"mappings":"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","names":["s"]}
1
+ {"version":3,"sources":["../../src/memory/memory.ts"],"sourcesContent":["/**\n * TITAN — Memory / Persistence System\n * JSON-file-backed persistent memory for conversations, facts, preferences, and usage.\n * Uses no native dependencies — pure Node.js for maximum portability.\n */\nimport { existsSync, readFileSync, writeFileSync, renameSync } from 'fs';\nimport { join } from 'path';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport { mkdirIfNotExists } from '../utils/helpers.js';\nimport logger from '../utils/logger.js';\nimport { encrypt, decrypt, type EncryptedPayload } from '../security/encryption.js';\nimport { isVectorSearchAvailable, searchVectors, addVector } from './vectors.js';\n\nconst COMPONENT = 'Memory';\n\n// ─── Data Store ──────────────────────────────────────────────────\n\ninterface DataStore {\n conversations: ConversationMessage[];\n memories: MemoryEntry[];\n sessions: SessionRecord[];\n usageStats: UsageRecord[];\n cronJobs: CronRecord[];\n skillsInstalled: SkillRecord[];\n}\n\ninterface MemoryEntry {\n id: string;\n category: string;\n key: string;\n value: string;\n metadata?: string;\n createdAt: string;\n updatedAt: string;\n}\n\ninterface SessionRecord {\n id: string;\n channel: string;\n user_id: string;\n agent_id: string;\n status: string;\n message_count: number;\n created_at: string;\n last_active: string;\n name?: string;\n last_message?: string;\n // D3: Persisted session overrides (survive session recovery after timeout/restart)\n model_override?: string;\n thinking_override?: string;\n // Hunt Finding #19 (2026-04-14): true when this session was created via an\n // explicit sessionId (getOrCreateSessionById). Named sessions MUST NOT be\n // returned by the default-slot lookup in getOrCreateSession — otherwise\n // subsequent no-sessionId requests from the same channel+user+agent will\n // inherit the most recent named session's history, causing privacy bleed\n // between API callers.\n is_named?: boolean;\n}\n\ninterface UsageRecord {\n id: number;\n session_id: string;\n provider: string;\n model: string;\n prompt_tokens: number;\n completion_tokens: number;\n total_tokens: number;\n created_at: string;\n}\n\ninterface CronRecord {\n id: string;\n name: string;\n schedule: string;\n command: string;\n mode?: 'shell' | 'tool'; // Execution mode (default: shell for backward compat)\n allowedTools?: string[]; // Tool allowlist for tool-mode jobs\n enabled: boolean;\n last_run?: string;\n next_run?: string;\n created_at: string;\n}\n\ninterface SkillRecord {\n name: string;\n version: string;\n source: string;\n enabled: boolean;\n installed_at: string;\n}\n\nconst DB_FILE = join(TITAN_HOME, 'titan-data.json');\n\nlet store: DataStore | null = null;\nlet dirty = false;\nlet isShuttingDown = false;\n\nfunction getDefaultStore(): DataStore {\n return {\n conversations: [],\n memories: [],\n sessions: [],\n usageStats: [],\n cronJobs: [],\n skillsInstalled: [],\n };\n}\n\n// NOTE: Sync I/O is intentional — runs only once at cold start, then cached in-memory.\nfunction loadStore(): DataStore {\n if (store) return store;\n mkdirIfNotExists(TITAN_HOME);\n if (existsSync(DB_FILE)) {\n try {\n const raw = readFileSync(DB_FILE, 'utf-8');\n store = JSON.parse(raw) as DataStore;\n // Ensure all fields exist\n store.conversations = store.conversations || [];\n store.memories = store.memories || [];\n store.sessions = store.sessions || [];\n store.usageStats = store.usageStats || [];\n store.cronJobs = store.cronJobs || [];\n store.skillsInstalled = store.skillsInstalled || [];\n } catch {\n logger.warn(COMPONENT, 'Could not load data store, creating fresh one');\n store = getDefaultStore();\n }\n } else {\n store = getDefaultStore();\n }\n return store;\n}\n\nfunction saveStore(): void {\n if (!store || isShuttingDown) return;\n mkdirIfNotExists(TITAN_HOME);\n try {\n const tmpFile = DB_FILE + '.tmp';\n writeFileSync(tmpFile, JSON.stringify(store, null, 2), 'utf-8');\n renameSync(tmpFile, DB_FILE);\n dirty = false;\n } catch (e) {\n dirty = true;\n logger.error(COMPONENT, `Failed to save data: ${(e as Error).message}`);\n }\n}\n\n// Auto-save periodically\nlet saveTimeout: ReturnType<typeof setTimeout> | null = null;\nexport function debouncedSave(): void {\n if (dirty) { saveStore(); return; }\n if (saveTimeout) clearTimeout(saveTimeout);\n saveTimeout = setTimeout(saveStore, 1000);\n saveTimeout.unref();\n}\n\n/** Initialize the memory system */\nexport function initMemory(): void {\n loadStore();\n logger.info(COMPONENT, 'Memory system initialized');\n}\n\n/** Close / flush the memory system */\nexport function closeMemory(): void {\n if (saveTimeout) { clearTimeout(saveTimeout); saveTimeout = null; }\n saveStore();\n if (dirty) {\n logger.error(COMPONENT, 'DATA MAY BE LOST — failed to flush memory store on shutdown');\n }\n isShuttingDown = true;\n}\n\n/** Get internal store (for skills like cron that need direct access) */\nexport function getDb(): DataStore {\n return loadStore();\n}\n\n// ─── Conversation History ────────────────────────────────────────\n\nexport interface ConversationMessage {\n id: string;\n sessionId: string;\n role: string;\n content: string;\n toolCalls?: string;\n toolCallId?: string;\n model?: string;\n tokenCount: number;\n createdAt: string;\n isEncrypted?: boolean;\n}\n\n/** Save a message to conversation history */\nexport function saveMessage(message: Omit<ConversationMessage, 'createdAt'>, e2eKey?: string): void {\n const s = loadStore();\n\n let content = message.content;\n let isEncrypted = false;\n\n if (e2eKey) {\n try {\n const payload = encrypt(message.content, Buffer.from(e2eKey, 'base64'));\n content = JSON.stringify(payload);\n isEncrypted = true;\n } catch {\n logger.error(COMPONENT, `Failed to encrypt message for storage`);\n content = \"[ENCRYPTION FAILED] \" + content; // Fallback, though we should probably throw in strict environments\n }\n }\n\n s.conversations.push({\n ...message,\n content,\n isEncrypted,\n createdAt: new Date().toISOString(),\n });\n // Keep only last 5000 messages total to prevent unbounded growth\n if (s.conversations.length > 5000) {\n s.conversations = s.conversations.slice(-5000);\n }\n debouncedSave();\n}\n\n/** Get conversation history for a session */\nexport function getHistory(sessionId: string, limit: number = 50, e2eKey?: string): ConversationMessage[] {\n const s = loadStore();\n const rawHistory = s.conversations\n .filter((m) => m.sessionId === sessionId)\n .slice(-limit);\n\n if (!e2eKey) {\n // If no key is provided, we just return the raw payload. \n // If it's encrypted, it'll just show the JSON string of the EncryptedPayload.\n return rawHistory;\n }\n\n // Decrypt the ones that were encrypted\n return rawHistory.map(m => {\n if (m.isEncrypted) {\n try {\n const payload = JSON.parse(m.content) as EncryptedPayload;\n return {\n ...m,\n content: decrypt(payload, Buffer.from(e2eKey, 'base64'))\n };\n } catch {\n logger.error(COMPONENT, `Failed to decrypt message ${m.id}`);\n return { ...m, content: \"[DECRYPTION FAILED]\" };\n }\n }\n return m;\n });\n}\n\n/** Update session name and/or last message snippet */\nexport function updateSessionMeta(sessionId: string, meta: { name?: string; last_message?: string; model_override?: string; thinking_override?: string }): void {\n const s = loadStore();\n const rec = s.sessions.find(ses => ses.id === sessionId);\n if (!rec) return;\n if (meta.name !== undefined) rec.name = meta.name;\n if (meta.last_message !== undefined) rec.last_message = meta.last_message;\n // D3: Persist session overrides to database so they survive timeout/restart\n if (meta.model_override !== undefined) rec.model_override = meta.model_override;\n if (meta.thinking_override !== undefined) rec.thinking_override = meta.thinking_override;\n debouncedSave();\n}\n\n/** Clear conversation history for a session */\nexport function clearHistory(sessionId: string): void {\n const s = loadStore();\n s.conversations = s.conversations.filter((m) => m.sessionId !== sessionId);\n debouncedSave();\n}\n\n// ─── Persistent Memory (Facts / Preferences) ─────────────────────\n\n/** Store a memory (key-value with category) */\nexport function rememberFact(category: string, key: string, value: string, metadata?: Record<string, unknown>): void {\n const s = loadStore();\n const id = `${category}:${key}`;\n const existingIdx = s.memories.findIndex((m) => m.id === id);\n const now = new Date().toISOString();\n\n if (existingIdx >= 0) {\n s.memories[existingIdx].value = value;\n s.memories[existingIdx].metadata = metadata ? JSON.stringify(metadata) : undefined;\n s.memories[existingIdx].updatedAt = now;\n } else {\n s.memories.push({\n id,\n category,\n key,\n value,\n metadata: metadata ? JSON.stringify(metadata) : undefined,\n createdAt: now,\n updatedAt: now,\n });\n }\n debouncedSave();\n\n // Index to vector store (fire-and-forget)\n if (isVectorSearchAvailable()) {\n addVector(id, `${category}: ${key} = ${value}`, 'memory', { category, key }).catch(e => logger.debug(COMPONENT, `Background vector indexing failed: ${(e as Error).message}`));\n }\n}\n\n/** Recall a specific memory */\nexport function recallFact(category: string, key: string): string | null {\n const s = loadStore();\n const entry = s.memories.find((m) => m.category === category && m.key === key);\n return entry?.value || null;\n}\n\n/** Search memories by category — hybrid keyword + vector search */\nexport async function searchMemories(category?: string, query?: string): Promise<Array<{ key: string; value: string; category: string; score?: number }>> {\n const s = loadStore();\n let results = s.memories;\n\n if (category) {\n results = results.filter((m) => m.category === category);\n }\n if (query) {\n const q = query.toLowerCase();\n // Word-boundary match to avoid false positives (\"use\" matching \"user\", \"reuse\")\n const qRegex = new RegExp('\\\\b' + q.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&') + '\\\\b', 'i');\n results = results.filter((m) =>\n qRegex.test(m.key) || qRegex.test(m.value)\n );\n }\n\n // Keyword scoring\n const scored = results.map(m => {\n let score = 0;\n if (query) {\n const q = query.toLowerCase();\n const keyLower = m.key.toLowerCase();\n const valLower = m.value.toLowerCase();\n // Exact key match scores highest\n if (keyLower === q) score += 5;\n else if (keyLower.includes(q)) score += 2;\n if (valLower.includes(q)) score += 1;\n // BM25-style: boost for multiple keyword matches\n const terms = q.split(/\\s+/).filter(Boolean);\n for (const term of terms) {\n if (keyLower.includes(term)) score += 1;\n if (valLower.includes(term)) score += 0.5;\n }\n }\n return { key: m.key, value: m.value, category: m.category, id: m.id, score };\n });\n\n // Vector search augmentation (hybrid mode)\n if (query && isVectorSearchAvailable()) {\n try {\n const vectorResults = await searchVectors(query, 20, 'memory', 0.4);\n for (const vr of vectorResults) {\n // Skip stale vector IDs that no longer exist in the store\n const memEntry = s.memories.find(m => m.id === vr.id);\n if (!memEntry) continue;\n const existing = scored.find(s => s.id === vr.id);\n if (existing) {\n // Boost keyword results that also match semantically\n existing.score += vr.score * 3;\n } else {\n // Add vector-only results (semantically similar but no keyword match)\n const entry = s.memories.find(m => m.id === vr.id);\n if (entry && (!category || entry.category === category)) {\n scored.push({\n key: entry.key,\n value: entry.value,\n category: entry.category,\n id: entry.id,\n score: vr.score * 2,\n });\n }\n }\n }\n } catch {\n // Vector search failure is non-fatal\n }\n }\n\n scored.sort((a, b) => b.score - a.score);\n // Deduplicate by ID (vector + keyword can match the same entry)\n const seen = new Set<string>();\n const unique = scored.filter(m => { if (seen.has(m.id)) return false; seen.add(m.id); return true; });\n return unique.slice(0, 50).map(m => ({ key: m.key, value: m.value, category: m.category, score: m.score }));\n}\n\n// ─── Usage Tracking ──────────────────────────────────────────────\n\n/** Record usage statistics */\nexport function recordUsage(sessionId: string, provider: string, model: string, promptTokens: number, completionTokens: number): void {\n const s = loadStore();\n s.usageStats.push({\n id: Date.now(),\n session_id: sessionId,\n provider,\n model,\n prompt_tokens: promptTokens,\n completion_tokens: completionTokens,\n total_tokens: promptTokens + completionTokens,\n created_at: new Date().toISOString(),\n });\n // Keep only last 10000 records\n if (s.usageStats.length > 10000) {\n s.usageStats = s.usageStats.slice(-10000);\n }\n debouncedSave();\n}\n\n/** Get total usage statistics */\nexport function getUsageStats(): { totalTokens: number; totalRequests: number; byProvider: Record<string, number> } {\n const s = loadStore();\n let totalTokens = 0;\n const byProvider: Record<string, number> = {};\n\n for (const rec of s.usageStats) {\n totalTokens += rec.total_tokens;\n byProvider[rec.provider] = (byProvider[rec.provider] || 0) + rec.total_tokens;\n }\n\n return {\n totalTokens,\n totalRequests: s.usageStats.length,\n byProvider,\n };\n}\n"],"mappings":"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","names":["s"]}
@@ -2,7 +2,7 @@
2
2
  import { existsSync, readFileSync, writeFileSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { TITAN_HOME } from "../utils/constants.js";
5
- import { ensureDir } from "../utils/helpers.js";
5
+ import { mkdirIfNotExists } from "../utils/helpers.js";
6
6
  import { listGoals, getReadyTasks } from "../agent/goals.js";
7
7
  import { getRegisteredAgents, getBudgetPolicies, listRuns } from "../agent/commandPost.js";
8
8
  import { getRecentTrajectories } from "../agent/trajectoryLogger.js";
@@ -380,7 +380,7 @@ function loadDriveHistory() {
380
380
  }
381
381
  function saveDriveTick(tick) {
382
382
  try {
383
- ensureDir(TITAN_HOME);
383
+ mkdirIfNotExists(TITAN_HOME);
384
384
  const existing = loadDriveHistory();
385
385
  const satisfactions = {};
386
386
  for (const d of tick.drives) satisfactions[d.id] = d.satisfaction;