titan-agent 6.3.5 → 6.4.0

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 (174) hide show
  1. package/THIRD_PARTY_NOTICES.md +3 -0
  2. package/dist/agent/agent.js +6 -2
  3. package/dist/agent/agent.js.map +1 -1
  4. package/dist/agent/autoSkillGen.js +55 -17
  5. package/dist/agent/autoSkillGen.js.map +1 -1
  6. package/dist/agent/learningCurator.js +383 -0
  7. package/dist/agent/learningCurator.js.map +1 -0
  8. package/dist/agent/trajectoryLogger.js +2 -2
  9. package/dist/agent/trajectoryLogger.js.map +1 -1
  10. package/dist/config/config.js +18 -2
  11. package/dist/config/config.js.map +1 -1
  12. package/dist/config/schema.js +22 -0
  13. package/dist/config/schema.js.map +1 -1
  14. package/dist/gateway/server.js +6 -1
  15. package/dist/gateway/server.js.map +1 -1
  16. package/dist/providers/defaultModel.js +2 -2
  17. package/dist/providers/defaultModel.js.map +1 -1
  18. package/dist/providers/ollama.js +17 -1
  19. package/dist/providers/ollama.js.map +1 -1
  20. package/dist/providers/router.js +7 -3
  21. package/dist/providers/router.js.map +1 -1
  22. package/dist/providers/stub.js +1 -6
  23. package/dist/providers/stub.js.map +1 -1
  24. package/dist/utils/constants.js +1 -1
  25. package/dist/utils/constants.js.map +1 -1
  26. package/package.json +1 -1
  27. package/ui/dist/assets/{AuditPanel-C7rrUHBM.js → AuditPanel-CLc6IVvA.js} +1 -1
  28. package/ui/dist/assets/{AutonomyPanel-CcOxsveF.js → AutonomyPanel-E6arcbAb.js} +2 -2
  29. package/ui/dist/assets/{AutopilotPanel-CjrYEF2s.js → AutopilotPanel-AhNEPP_2.js} +1 -1
  30. package/ui/dist/assets/{AutoresearchPanel-CBdGRAdZ.js → AutoresearchPanel-BJRXdUNa.js} +1 -1
  31. package/ui/dist/assets/{BackupPanel-BhU-utCn.js → BackupPanel-CGQAxeWf.js} +1 -1
  32. package/ui/dist/assets/{BrowserPanel-Dr-B5Kva.js → BrowserPanel-BylOyNpQ.js} +1 -1
  33. package/ui/dist/assets/{CPActivity-C5kJWqWc.js → CPActivity-7zS0dXGf.js} +1 -1
  34. package/ui/dist/assets/{CPAgentDetail-BVE3LoLA.js → CPAgentDetail-DUi0ygHI.js} +1 -1
  35. package/ui/dist/assets/{CPAgents-BKBSyXHs.js → CPAgents-BZYynbga.js} +1 -1
  36. package/ui/dist/assets/{CPApprovals-AkRGu05y.js → CPApprovals-CwNTmqzq.js} +1 -1
  37. package/ui/dist/assets/{CPCosts-DC5mPwVm.js → CPCosts-5hylxGvH.js} +1 -1
  38. package/ui/dist/assets/{CPDashboard-C7G-H6HW.js → CPDashboard-5f8qcBKy.js} +1 -1
  39. package/ui/dist/assets/{CPFiles-IhcbsdvY.js → CPFiles-CR9Nn3_p.js} +1 -1
  40. package/ui/dist/assets/{CPGoals-BL0rrvT5.js → CPGoals-CVSxXk1i.js} +1 -1
  41. package/ui/dist/assets/{CPInbox-CBUkqu4i.js → CPInbox-DD4RYSpf.js} +1 -1
  42. package/ui/dist/assets/{CPIssueDetail-HuI9tPu4.js → CPIssueDetail-Dpuc_wTi.js} +1 -1
  43. package/ui/dist/assets/{CPIssues-Do9d-evH.js → CPIssues-DsPZLTGm.js} +1 -1
  44. package/ui/dist/assets/CPLayout-CDkrYE-H.js +2 -0
  45. package/ui/dist/assets/{CPOrg-WHkSBNGU.js → CPOrg-C121ZFWy.js} +1 -1
  46. package/ui/dist/assets/{CPRuns-B4Pz5QkG.js → CPRuns-BUdKn-uk.js} +1 -1
  47. package/ui/dist/assets/{CPSocial-DayQlwZL.js → CPSocial-CriG1P45.js} +1 -1
  48. package/ui/dist/assets/{CPVoice-C2ry-Muk.js → CPVoice-BNhgXUE1.js} +1 -1
  49. package/ui/dist/assets/{ChannelsPanel-CeIEB9Es.js → ChannelsPanel-9SjAkSHg.js} +1 -1
  50. package/ui/dist/assets/{CheckpointsPanel-BkqqvLDL.js → CheckpointsPanel-D2sCPz2u.js} +1 -1
  51. package/ui/dist/assets/{CommandPostHub-Jd5_PAh3.js → CommandPostHub-DHTVn2_E.js} +3 -3
  52. package/ui/dist/assets/{CronPanel-COkBBcTV.js → CronPanel-CLQ0_Z_9.js} +1 -1
  53. package/ui/dist/assets/{DataTable-YQ9l5Xab.js → DataTable-CrBYCT2O.js} +1 -1
  54. package/ui/dist/assets/{DreamPanel-B5M--_sB.js → DreamPanel-BAkiDqQc.js} +1 -1
  55. package/ui/dist/assets/EmptyState-DSuZC-Tg.js +1 -0
  56. package/ui/dist/assets/{EvalHarnessPanel-DXU9jNmm.js → EvalHarnessPanel-BykaelQd.js} +1 -1
  57. package/ui/dist/assets/{EvalPanel-CY1kmmoG.js → EvalPanel-BlFByCOv.js} +1 -1
  58. package/ui/dist/assets/{FileViewer-BPLxBXEr.js → FileViewer-B0WedL9k.js} +1 -1
  59. package/ui/dist/assets/{FilesPanel-B5RpGquX.js → FilesPanel-DwUzP4Hv.js} +2 -2
  60. package/ui/dist/assets/{FleetPanel-1g-MrF81.js → FleetPanel-JW9Pk1FM.js} +1 -1
  61. package/ui/dist/assets/{HomelabPanel-CxkWfeeN.js → HomelabPanel-DigWCjaM.js} +1 -1
  62. package/ui/dist/assets/InfraView-0iQPmcD1.js +2 -0
  63. package/ui/dist/assets/{InlineEditableField-BuXVLQiL.js → InlineEditableField-Car7F635.js} +1 -1
  64. package/ui/dist/assets/{Input-C6OxbFDH.js → Input-BGGQDAyP.js} +1 -1
  65. package/ui/dist/assets/{IntegrationsPanel-CPZclot5.js → IntegrationsPanel-BGEkU649.js} +1 -1
  66. package/ui/dist/assets/IntelligenceView-wm2Pw46I.js +2 -0
  67. package/ui/dist/assets/LearningPanel-CGENRFnq.js +1 -0
  68. package/ui/dist/assets/{LogsPanel-XfC4fyQ6.js → LogsPanel-DZ_K2xOL.js} +1 -1
  69. package/ui/dist/assets/{McpPanel-CBN7xizL.js → McpPanel-CNB1BMz-.js} +1 -1
  70. package/ui/dist/assets/MemoryGraphPanel-FQxiv5x5.js +16 -0
  71. package/ui/dist/assets/{MemoryWikiPanel-BNv9ycwr.js → MemoryWikiPanel-C1w7xDNl.js} +2 -7
  72. package/ui/dist/assets/{MeshPanel-GFPkWStS.js → MeshPanel-DjYZh3P3.js} +1 -1
  73. package/ui/dist/assets/{MissionCanvas-BR9tT-Vs.js → MissionCanvas-BvbSfN_4.js} +1 -1
  74. package/ui/dist/assets/{MissionChat-B_fuBUn5.js → MissionChat-Do8j0nvp.js} +1 -1
  75. package/ui/dist/assets/{MissionLibrary-C9fCMdpu.js → MissionLibrary-CTzkJFJ7.js} +1 -1
  76. package/ui/dist/assets/{MissionStart-Bb2fCB2u.js → MissionStart-_-qlVcRK.js} +1 -1
  77. package/ui/dist/assets/{Modal-9_l_CYE2.js → Modal-CNnSfYJm.js} +1 -1
  78. package/ui/dist/assets/{NvidiaPanel-CiB9Nlbq.js → NvidiaPanel-CLoX_gD2.js} +1 -1
  79. package/ui/dist/assets/{OrganismPanel-d6mPFcBq.js → OrganismPanel-DBy9J7Vl.js} +1 -1
  80. package/ui/dist/assets/{OverviewPanel-DF7eBJCF.js → OverviewPanel-B7JFNH2w.js} +1 -1
  81. package/ui/dist/assets/PageHeader-CwcMp_4A.js +1 -0
  82. package/ui/dist/assets/{PersonaProfilesPanel-qAgZyu1B.js → PersonaProfilesPanel-CmZhQC8H.js} +1 -1
  83. package/ui/dist/assets/{PersonasPanel-CxDpp_3j.js → PersonasPanel-DSP0tTKz.js} +1 -1
  84. package/ui/dist/assets/{PhoneDeskPanel-CLp3E_Ry.js → PhoneDeskPanel-DZSs7aPr.js} +1 -1
  85. package/ui/dist/assets/{RecipesPanel-DqMMQnyc.js → RecipesPanel-BW0hW2sW.js} +1 -1
  86. package/ui/dist/assets/{SecurityPanel-BQ3-aMmO.js → SecurityPanel-_Saqrvw7.js} +1 -1
  87. package/ui/dist/assets/{SelfImprovePanel-C-ylXP-k.js → SelfImprovePanel-D5s-1XCU.js} +1 -1
  88. package/ui/dist/assets/{SelfProposalsPanel-d5W-0A3P.js → SelfProposalsPanel-D24kB0uM.js} +2 -2
  89. package/ui/dist/assets/{SessionsPanel-DzbNc8iW.js → SessionsPanel-BCxsCKwr.js} +1 -1
  90. package/ui/dist/assets/{SessionsTab-CuRh8SAw.js → SessionsTab-C5fvnjtl.js} +1 -1
  91. package/ui/dist/assets/{SettingsPanel-C1FbNM3W.js → SettingsPanel-B7jrFmeR.js} +1 -1
  92. package/ui/dist/assets/SettingsView-CQMHjpN8.js +2 -0
  93. package/ui/dist/assets/{SkeletonLoader-CrZUFaIM.js → SkeletonLoader-C42HbL9c.js} +1 -1
  94. package/ui/dist/assets/{SkillsPanel-eyCYqSuP.js → SkillsPanel-DYG8avbV.js} +1 -1
  95. package/ui/dist/assets/{SomaView-CvHD2r-j.js → SomaView-8Rwvw2sJ.js} +1 -1
  96. package/ui/dist/assets/{StatCard-_MlD8k-5.js → StatCard-vp0doZoE.js} +1 -1
  97. package/ui/dist/assets/{StatusBadge-MIQ4EfQR.js → StatusBadge-C4dl4Z7r.js} +1 -1
  98. package/ui/dist/assets/{Tabs-DkooY-W7.js → Tabs-B3hK5PRj.js} +1 -1
  99. package/ui/dist/assets/{TeamsPanel-fa--osWi.js → TeamsPanel-Bgc-EYSL.js} +1 -1
  100. package/ui/dist/assets/{TelemetryPanel-BEEWcoNE.js → TelemetryPanel-DexYjpd5.js} +1 -1
  101. package/ui/dist/assets/{TitanCanvas-LWyPGpf5.js → TitanCanvas-Da6TxxdJ.js} +52 -27
  102. package/ui/dist/assets/{ToolsView-UCDMsvb1.js → ToolsView-DkP9NfCj.js} +2 -2
  103. package/ui/dist/assets/{Tooltip-DrpHNalN.js → Tooltip-Chg08x7x.js} +1 -1
  104. package/ui/dist/assets/{TraceViewer-XsmYbc0e.js → TraceViewer-CojtXASo.js} +1 -1
  105. package/ui/dist/assets/{TrainingPanel-CmJXLn4n.js → TrainingPanel-tgn1BmiH.js} +1 -1
  106. package/ui/dist/assets/{VoiceOverlay-D2N-c76K.js → VoiceOverlay-D_jfyVe9.js} +1 -1
  107. package/ui/dist/assets/{VramPanel-h6zjNx-6.js → VramPanel-DXrdQK3J.js} +1 -1
  108. package/ui/dist/assets/{WatchView-DDNt13KH.js → WatchView-BitQHsCy.js} +1 -1
  109. package/ui/dist/assets/{WorkTab-CLXkjuee.js → WorkTab-6hdXBR7d.js} +1 -1
  110. package/ui/dist/assets/{WorkflowsPanel-BYtLGQVm.js → WorkflowsPanel-BlB2iBik.js} +1 -1
  111. package/ui/dist/assets/{archive-BSupLPZM.js → archive-aaQ0bgj_.js} +1 -1
  112. package/ui/dist/assets/{arrow-left-BaVVZjqf.js → arrow-left-xxx_FceU.js} +1 -1
  113. package/ui/dist/assets/{book-open-CZxMkLs8.js → book-open-B00IpKZA.js} +1 -1
  114. package/ui/dist/assets/{bot-Cv-edYov.js → bot-C4CRICZX.js} +1 -1
  115. package/ui/dist/assets/{chart-column-BSVdQ9RP.js → chart-column-lnilxGq2.js} +1 -1
  116. package/ui/dist/assets/{check-DbgTEg8O.js → check-BJMHM0tu.js} +1 -1
  117. package/ui/dist/assets/{chevron-right-DfToq0aQ.js → chevron-right-CA8HOEP4.js} +1 -1
  118. package/ui/dist/assets/{chevron-up-DMS2Tjip.js → chevron-up-n_3KVmjQ.js} +1 -1
  119. package/ui/dist/assets/{circle-check-big-BBQqeCVt.js → circle-check-big-hiNdH5H3.js} +1 -1
  120. package/ui/dist/assets/{circle-stop-CZNS3Uri.js → circle-stop-BlIwj_3n.js} +1 -1
  121. package/ui/dist/assets/{clipboard-list-CzMXlUCb.js → clipboard-list-BnOtwHQm.js} +1 -1
  122. package/ui/dist/assets/{clock-BvWNugZo.js → clock-BFgCJnqQ.js} +1 -1
  123. package/ui/dist/assets/{dollar-sign-pghzkByE.js → dollar-sign-CBBudQd7.js} +1 -1
  124. package/ui/dist/assets/{download-CupMD2Qi.js → download-C-DdF4cx.js} +1 -1
  125. package/ui/dist/assets/{external-link-8Pnd2MvS.js → external-link-CPAx2HBG.js} +1 -1
  126. package/ui/dist/assets/{file-text-rkojc4Uq.js → file-text-CyC_OxlA.js} +1 -1
  127. package/ui/dist/assets/{flask-conical-BtbE3rIA.js → flask-conical-BKbKUYhz.js} +1 -1
  128. package/ui/dist/assets/{folder-65U1Euur.js → folder-7bWz46dM.js} +1 -1
  129. package/ui/dist/assets/{funnel-D9FpWHn9.js → funnel-C_bR-Dz5.js} +1 -1
  130. package/ui/dist/assets/{git-branch-D6YV_OC1.js → git-branch-DGWdkudk.js} +1 -1
  131. package/ui/dist/assets/{index-BslUQuVl.js → index-CgHHe41p.js} +19 -19
  132. package/ui/dist/assets/index-CtD9JOx2.css +1 -0
  133. package/ui/dist/assets/{layers-rc37Q5yX.js → layers-3BMDaZuk.js} +1 -1
  134. package/ui/dist/assets/{legacy-BydeSW77.js → legacy-VWR8oDEy.js} +1 -1
  135. package/ui/dist/assets/{lightbulb-BEx7BdxD.js → lightbulb-CPhcUxli.js} +1 -1
  136. package/ui/dist/assets/link-CfLpUR24.js +6 -0
  137. package/ui/dist/assets/{list-todo-6N1AZe4i.js → list-todo-DgAmn8ot.js} +1 -1
  138. package/ui/dist/assets/{message-square-BEGY8pHF.js → message-square-WJp1dTaM.js} +1 -1
  139. package/ui/dist/assets/{mic-CGj6cLoV.js → mic-DjXDp1Js.js} +1 -1
  140. package/ui/dist/assets/{missions-BohoEPZo.js → missions-CwDL2FHE.js} +1 -1
  141. package/ui/dist/assets/{network-DxIkmFJR.js → network-C_MOo5My.js} +1 -1
  142. package/ui/dist/assets/{pause-CxvyFedA.js → pause-CovS6Xwq.js} +1 -1
  143. package/ui/dist/assets/{pencil-D5LooNFk.js → pencil-C6SVDEUL.js} +1 -1
  144. package/ui/dist/assets/{play-6W8amqUG.js → play-D3Nbm1Cz.js} +1 -1
  145. package/ui/dist/assets/{plug-C3Ecy-Ji.js → plug-Dpu9RRBi.js} +1 -1
  146. package/ui/dist/assets/{plus-BtmCDNeU.js → plus-CJKkM9fM.js} +1 -1
  147. package/ui/dist/assets/{proxy-BD29vUSb.js → proxy-CaK_YXZW.js} +1 -1
  148. package/ui/dist/assets/{radio-Bl9Tibea.js → radio-BTFO9FV9.js} +1 -1
  149. package/ui/dist/assets/{refresh-cw-BHphkMjv.js → refresh-cw-_C-4Cd_3.js} +1 -1
  150. package/ui/dist/assets/{rotate-ccw-DQzGCkVc.js → rotate-ccw-DWyVAH19.js} +1 -1
  151. package/ui/dist/assets/{save-Bk90yLic.js → save-B02XWwgN.js} +1 -1
  152. package/ui/dist/assets/{search-DxM6zcDJ.js → search-CHteSk-9.js} +1 -1
  153. package/ui/dist/assets/{send-ChZ5Hsne.js → send-DkMQeZAz.js} +1 -1
  154. package/ui/dist/assets/{server-CSOB40Ah.js → server-CvEvPDpX.js} +1 -1
  155. package/ui/dist/assets/{shield-check-uMK1alGs.js → shield-check-DQeYCvwF.js} +1 -1
  156. package/ui/dist/assets/{terminal-lIpm0q0K.js → terminal-BVErXh5C.js} +1 -1
  157. package/ui/dist/assets/{test-tube-DuTInDo7.js → test-tube-JSgqEqJs.js} +1 -1
  158. package/ui/dist/assets/{toggle-right-j4jgj1NQ.js → toggle-right-B3Qz7cQt.js} +1 -1
  159. package/ui/dist/assets/{trash-2-2Qmp8Fyr.js → trash-2-kiHv5iAF.js} +1 -1
  160. package/ui/dist/assets/{trending-up-BydxQces.js → trending-up-xhvUQXWt.js} +1 -1
  161. package/ui/dist/assets/{trophy-B8Fq83wO.js → trophy-G3zNxlQS.js} +1 -1
  162. package/ui/dist/assets/{volume-2-B6qThcBF.js → volume-2-CELMZXsT.js} +1 -1
  163. package/ui/dist/assets/{wifi-off-BX5XYuML.js → wifi-off-DKKYhVWL.js} +1 -1
  164. package/ui/dist/index.html +2 -2
  165. package/ui/dist/sw.js +1 -1
  166. package/ui/dist/assets/CPLayout-DlpdPzBu.js +0 -2
  167. package/ui/dist/assets/EmptyState-DtSIPEVv.js +0 -1
  168. package/ui/dist/assets/InfraView-CYH3ieie.js +0 -2
  169. package/ui/dist/assets/IntelligenceView-DqwlFo91.js +0 -2
  170. package/ui/dist/assets/LearningPanel-DHQSZZGb.js +0 -1
  171. package/ui/dist/assets/MemoryGraphPanel-BBxgK2Lo.js +0 -16
  172. package/ui/dist/assets/PageHeader-BQEb_uAl.js +0 -1
  173. package/ui/dist/assets/SettingsView-ZTklN8Q3.js +0 -2
  174. package/ui/dist/assets/index-DmgXt1oB.css +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/agent/learningCurator.ts"],"sourcesContent":["/**\n * TITAN — Learning Curator\n *\n * Ported from the MIT-licensed Hermes Agent background review / curator pattern:\n * https://github.com/NousResearch/hermes-agent\n *\n * Copyright (c) 2025 Nous Research\n * MIT License. See THIRD_PARTY_NOTICES.md for attribution.\n *\n * Hermes runs a background review pass over recent work and updates memory or\n * skills when durable learning appears. This TITAN-native port keeps the same\n * operational shape but writes into TITAN's existing trajectory and\n * per-agent lesson systems instead of introducing a parallel skill store.\n */\nimport { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'fs';\nimport { dirname, join } from 'path';\nimport logger from '../utils/logger.js';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport { getRecentTrajectories, getSequenceSignature, type TaskTrajectory } from './trajectoryLogger.js';\nimport { recordLesson } from './agentLessons.js';\nimport { loadConfig } from '../config/config.js';\n\n/**\n * v6.4.0 — read curator knobs from `learningCurator` config section, with\n * fallback to constants when config is unavailable (e.g. during unit\n * tests that don't initialize a TitanConfig). The shape mirrors the Zod\n * schema in src/config/schema.ts.\n */\ninterface CuratorConfig {\n enabled: boolean;\n intervalHours: number;\n minTrajectories: number;\n staleAfterDays: number;\n archiveAfterDays: number;\n}\n\nfunction loadCuratorConfig(): CuratorConfig {\n try {\n const cfg = loadConfig() as unknown as { learningCurator?: Partial<CuratorConfig> };\n const lc = cfg.learningCurator ?? {};\n return {\n enabled: lc.enabled ?? true,\n intervalHours: lc.intervalHours ?? DEFAULT_INTERVAL_HOURS,\n minTrajectories: lc.minTrajectories ?? DEFAULT_MIN_TRAJECTORIES,\n staleAfterDays: lc.staleAfterDays ?? DEFAULT_STALE_AFTER_DAYS,\n archiveAfterDays: lc.archiveAfterDays ?? DEFAULT_ARCHIVE_AFTER_DAYS,\n };\n } catch {\n return {\n enabled: true,\n intervalHours: DEFAULT_INTERVAL_HOURS,\n minTrajectories: DEFAULT_MIN_TRAJECTORIES,\n staleAfterDays: DEFAULT_STALE_AFTER_DAYS,\n archiveAfterDays: DEFAULT_ARCHIVE_AFTER_DAYS,\n };\n }\n}\n\nconst COMPONENT = 'LearningCurator';\nconst STATE_PATH = join(TITAN_HOME, 'learning-curator-state.json');\nconst REPORTS_DIR = join(TITAN_HOME, 'logs', 'learning-curator');\nconst DEFAULT_INTERVAL_HOURS = 24;\nconst DEFAULT_MIN_TRAJECTORIES = 3;\nconst DEFAULT_STALE_AFTER_DAYS = 30;\nconst DEFAULT_ARCHIVE_AFTER_DAYS = 90;\n\nexport type LearningPatternLifecycleState = 'active' | 'watch' | 'stale' | 'archived';\n\nexport interface LearningPatternState {\n taskType: string;\n signature: string;\n toolSequence: string[];\n successes: number;\n failures: number;\n transientFailures: number;\n state: LearningPatternLifecycleState;\n firstSeenAt: string;\n lastSeenAt: string;\n lastSuccessAt: string | null;\n lastFailureAt: string | null;\n commonFailureTool: string | null;\n}\n\nexport interface LearningPatternTransitionCounts {\n checked: number;\n markedStale: number;\n archived: number;\n reactivated: number;\n}\n\nexport interface LearningCuratorState {\n lastRunAt: string | null;\n lastRunDurationMs?: number | null;\n lastRunSummary: string | null;\n lastReportPath?: string | null;\n runCount: number;\n paused: boolean;\n patterns: Record<string, LearningPatternState>;\n}\n\nexport interface CuratorReviewResult {\n shouldRun: boolean;\n lessonsRecorded: number;\n summary: string;\n reportPath: string | null;\n transitions: LearningPatternTransitionCounts;\n}\n\nfunction defaultState(): LearningCuratorState {\n return {\n lastRunAt: null,\n lastRunDurationMs: null,\n lastRunSummary: null,\n lastReportPath: null,\n runCount: 0,\n paused: false,\n patterns: {},\n };\n}\n\nexport function loadLearningCuratorState(): LearningCuratorState {\n if (!existsSync(STATE_PATH)) return defaultState();\n try {\n const parsed = JSON.parse(readFileSync(STATE_PATH, 'utf-8')) as Partial<LearningCuratorState>;\n return { ...defaultState(), ...parsed };\n } catch {\n return defaultState();\n }\n}\n\nexport function saveLearningCuratorState(state: LearningCuratorState): void {\n try {\n mkdirSync(dirname(STATE_PATH), { recursive: true });\n atomicWriteText(STATE_PATH, JSON.stringify(state, null, 2));\n } catch (err) {\n logger.warn(COMPONENT, `Failed to save curator state: ${(err as Error).message}`);\n }\n}\n\nfunction atomicWriteText(path: string, content: string): void {\n const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;\n writeFileSync(tmp, content, 'utf-8');\n try {\n renameSync(tmp, path);\n } catch (err) {\n try { unlinkSync(tmp); } catch { /* ignore cleanup */ }\n throw err;\n }\n}\n\nexport function shouldRunLearningReview(now: Date = new Date(), intervalHours = DEFAULT_INTERVAL_HOURS): boolean {\n const state = loadLearningCuratorState();\n if (state.paused) return false;\n\n if (!state.lastRunAt) {\n saveLearningCuratorState({\n ...state,\n lastRunAt: now.toISOString(),\n lastRunDurationMs: 0,\n lastRunSummary: 'Seeded learning curator; first review deferred until interval elapses.',\n });\n return false;\n }\n\n const lastRun = Date.parse(state.lastRunAt);\n if (!Number.isFinite(lastRun)) return true;\n return now.getTime() - lastRun >= intervalHours * 60 * 60 * 1000;\n}\n\nexport function runLearningReview(\n trigger: TaskTrajectory,\n now: Date = new Date(),\n minTrajectories = DEFAULT_MIN_TRAJECTORIES,\n): CuratorReviewResult {\n const started = Date.now();\n const emptyTransitions: LearningPatternTransitionCounts = {\n checked: 0,\n markedStale: 0,\n archived: 0,\n reactivated: 0,\n };\n\n if (!shouldRunLearningReview(now)) {\n return {\n shouldRun: false,\n lessonsRecorded: 0,\n summary: 'Learning review deferred by interval gate.',\n reportPath: null,\n transitions: emptyTransitions,\n };\n }\n\n const recent = getRecentTrajectories(200, { taskType: trigger.taskType });\n if (recent.length < minTrajectories) {\n const summary = `Skipped ${trigger.taskType}: only ${recent.length}/${minTrajectories} trajectories.`;\n const reportPath = writeLearningReviewReport(now, {\n taskType: trigger.taskType,\n summary,\n lessons: [],\n transitions: emptyTransitions,\n recentCount: recent.length,\n durationMs: Date.now() - started,\n });\n persistReview(now, summary, reportPath, Date.now() - started);\n return {\n shouldRun: true,\n lessonsRecorded: 0,\n summary: 'Not enough trajectory history to curate.',\n reportPath,\n transitions: emptyTransitions,\n };\n }\n\n const transitions = applyLearningPatternTransitions(now);\n const lessons = deriveLessons(trigger, recent);\n for (const lesson of lessons) {\n recordLesson('default', {\n kind: lesson.kind,\n topic: lesson.topic,\n sessionId: trigger.sessionId,\n text: lesson.text,\n });\n }\n\n const summary = lessons.length > 0\n ? `Recorded ${lessons.length} ${trigger.taskType} learning lesson(s).`\n : `Reviewed ${recent.length} ${trigger.taskType} trajectories; nothing durable to save.`;\n const durationMs = Date.now() - started;\n const reportPath = writeLearningReviewReport(now, {\n taskType: trigger.taskType,\n summary,\n lessons,\n transitions,\n recentCount: recent.length,\n durationMs,\n });\n persistReview(now, summary, reportPath, durationMs);\n return { shouldRun: true, lessonsRecorded: lessons.length, summary, reportPath, transitions };\n}\n\nfunction persistReview(now: Date, summary: string, reportPath: string | null, durationMs: number): void {\n const state = loadLearningCuratorState();\n saveLearningCuratorState({\n ...state,\n lastRunAt: now.toISOString(),\n lastRunDurationMs: durationMs,\n lastRunSummary: summary,\n lastReportPath: reportPath,\n runCount: state.runCount + 1,\n });\n}\n\ninterface DerivedLesson {\n kind: 'fail' | 'win';\n topic: string;\n text: string;\n}\n\ninterface LearningReviewReportInput {\n taskType: string;\n summary: string;\n lessons: DerivedLesson[];\n transitions: LearningPatternTransitionCounts;\n recentCount: number;\n durationMs: number;\n}\n\nexport function writeLearningReviewReport(now: Date, report: LearningReviewReportInput): string | null {\n try {\n const stamp = now.toISOString().replace(/[-:]/g, '').replace(/\\.\\d{3}Z$/, 'Z');\n const dir = join(REPORTS_DIR, stamp);\n mkdirSync(dir, { recursive: true });\n const jsonPath = join(dir, 'run.json');\n const mdPath = join(dir, 'REPORT.md');\n atomicWriteText(jsonPath, JSON.stringify({\n generatedAt: now.toISOString(),\n taskType: report.taskType,\n summary: report.summary,\n recentCount: report.recentCount,\n lessons: report.lessons,\n transitions: report.transitions,\n durationMs: report.durationMs,\n }, null, 2));\n atomicWriteText(mdPath, renderLearningReviewReport(now, report));\n return mdPath;\n } catch (err) {\n logger.warn(COMPONENT, `Failed to write curator report: ${(err as Error).message}`);\n return null;\n }\n}\n\nfunction renderLearningReviewReport(now: Date, report: LearningReviewReportInput): string {\n const lessons = report.lessons.length > 0\n ? report.lessons.map(lesson => `- ${lesson.kind.toUpperCase()} / ${lesson.topic}: ${lesson.text}`).join('\\n')\n : '- No durable lessons recorded.';\n\n return `# TITAN Learning Curator Report\n\n- Generated: ${now.toISOString()}\n- Task type: ${report.taskType}\n- Recent trajectories checked: ${report.recentCount}\n- Duration: ${report.durationMs}ms\n- Summary: ${report.summary}\n\n## Pattern Transitions\n- Checked: ${report.transitions.checked}\n- Marked stale: ${report.transitions.markedStale}\n- Archived: ${report.transitions.archived}\n- Reactivated: ${report.transitions.reactivated}\n\n## Lessons\n${lessons}\n`;\n}\n\nfunction deriveLessons(trigger: TaskTrajectory, recent: TaskTrajectory[]): DerivedLesson[] {\n const lessons: DerivedLesson[] = [];\n const signature = getSequenceSignature(trigger.toolSequence);\n const sameSequence = recent.filter(t => getSequenceSignature(t.toolSequence) === signature);\n const successes = sameSequence.filter(t => t.success);\n const failures = sameSequence.filter(t => !t.success && isDurableLearningFailure(t));\n\n if (successes.length >= DEFAULT_MIN_TRAJECTORIES) {\n lessons.push({\n kind: 'win',\n topic: trigger.taskType,\n text: `For ${trigger.taskType} tasks, the tool sequence ${signature} is proven across ${successes.length} successful trajectories; prefer it before inventing a new flow, then verify the final artifact before reporting done.`,\n });\n }\n\n if (failures.length >= 2) {\n const failedTool = mostCommonFailureTool(failures);\n lessons.push({\n kind: 'fail',\n topic: trigger.taskType,\n text: `For ${trigger.taskType} tasks, repeated failures happened in the ${signature} flow${failedTool ? ` around ${failedTool}` : ''}; inspect the last tool result and change approach instead of retrying the same sequence blindly.`,\n });\n }\n\n const recentFailure = recent.find(t => !t.success && t.timestamp < trigger.timestamp);\n if (trigger.success && recentFailure && signature !== getSequenceSignature(recentFailure.toolSequence)) {\n lessons.push({\n kind: 'win',\n topic: trigger.taskType,\n text: `When a ${trigger.taskType} attempt stalls or fails, switching to ${signature} has produced a successful recovery path; use this as the first fallback candidate for similar work.`,\n });\n }\n\n return lessons;\n}\n\nexport function ingestTrajectoryLearningSignal(trajectory: TaskTrajectory): LearningPatternState {\n const state = loadLearningCuratorState();\n const now = trajectory.timestamp || new Date().toISOString();\n const signature = getSequenceSignature(trajectory.toolSequence);\n const key = patternKey(trajectory.taskType, signature);\n const existing = state.patterns[key];\n const transientFailure = !trajectory.success && !isDurableLearningFailure(trajectory);\n const failureTool = !trajectory.success && !transientFailure ? mostCommonFailureTool([trajectory]) : '';\n\n const pattern: LearningPatternState = {\n taskType: trajectory.taskType,\n signature,\n toolSequence: trajectory.toolSequence,\n successes: existing?.successes ?? 0,\n failures: existing?.failures ?? 0,\n transientFailures: existing?.transientFailures ?? 0,\n state: existing?.state ?? 'active',\n firstSeenAt: existing?.firstSeenAt ?? now,\n lastSeenAt: now,\n lastSuccessAt: existing?.lastSuccessAt ?? null,\n lastFailureAt: existing?.lastFailureAt ?? null,\n commonFailureTool: existing?.commonFailureTool ?? null,\n };\n\n if (trajectory.success) {\n pattern.successes += 1;\n pattern.lastSuccessAt = now;\n pattern.state = 'active';\n } else if (transientFailure) {\n pattern.transientFailures += 1;\n } else {\n pattern.failures += 1;\n pattern.lastFailureAt = now;\n pattern.commonFailureTool = failureTool || pattern.commonFailureTool;\n if (pattern.failures >= 2) pattern.state = 'watch';\n }\n\n state.patterns[key] = pattern;\n saveLearningCuratorState(state);\n return pattern;\n}\n\nexport function applyLearningPatternTransitions(\n now: Date = new Date(),\n staleAfterDays = DEFAULT_STALE_AFTER_DAYS,\n archiveAfterDays = DEFAULT_ARCHIVE_AFTER_DAYS,\n): LearningPatternTransitionCounts {\n const state = loadLearningCuratorState();\n const counts: LearningPatternTransitionCounts = {\n checked: 0,\n markedStale: 0,\n archived: 0,\n reactivated: 0,\n };\n const staleCutoff = now.getTime() - staleAfterDays * 24 * 60 * 60 * 1000;\n const archiveCutoff = now.getTime() - archiveAfterDays * 24 * 60 * 60 * 1000;\n\n for (const pattern of Object.values(state.patterns)) {\n counts.checked += 1;\n const lastSeen = Date.parse(pattern.lastSeenAt);\n if (!Number.isFinite(lastSeen)) continue;\n\n if (lastSeen <= archiveCutoff && pattern.state !== 'archived') {\n pattern.state = 'archived';\n counts.archived += 1;\n } else if (lastSeen <= staleCutoff && pattern.state === 'active') {\n pattern.state = 'stale';\n counts.markedStale += 1;\n } else if (lastSeen > staleCutoff && pattern.state === 'stale') {\n pattern.state = 'active';\n counts.reactivated += 1;\n }\n }\n\n if (counts.markedStale > 0 || counts.archived > 0 || counts.reactivated > 0) {\n saveLearningCuratorState(state);\n }\n return counts;\n}\n\nexport function renderLearningPatternGuidance(taskType: string, limit = 3): string | null {\n const state = loadLearningCuratorState();\n const patterns = Object.values(state.patterns)\n .filter(pattern => pattern.taskType === taskType && pattern.state !== 'archived')\n .sort((a, b) => patternSortScore(b) - patternSortScore(a))\n .slice(0, limit);\n\n if (patterns.length === 0) return null;\n\n const lines = ['Continuous learning patterns from recent TITAN trajectories:'];\n for (const pattern of patterns) {\n if (pattern.state === 'watch') {\n const failureDetail = pattern.commonFailureTool ? ` around ${pattern.commonFailureTool}` : '';\n lines.push(`- Watch ${pattern.signature}: ${pattern.failures} durable failure(s)${failureDetail}; inspect the failed tool result before repeating this flow.`);\n } else {\n lines.push(`- Prefer ${pattern.signature}: ${pattern.successes} win(s), ${pattern.failures} durable failure(s); verify the final artifact before reporting done.`);\n }\n }\n return lines.join('\\n');\n}\n\nfunction patternSortScore(pattern: LearningPatternState): number {\n const stateBoost = pattern.state === 'watch' ? 1000 : pattern.state === 'active' ? 500 : 0;\n return stateBoost + pattern.successes * 10 - pattern.failures;\n}\n\nfunction patternKey(taskType: string, signature: string): string {\n return `${taskType}:${signature}`;\n}\n\nfunction isDurableLearningFailure(trajectory: TaskTrajectory): boolean {\n if (trajectory.success) return false;\n const text = trajectory.toolDetails\n .map(detail => `${detail.name}\\n${detail.resultSnippet ?? ''}`)\n .join('\\n')\n .toLowerCase();\n\n return ![\n 'command not found',\n 'module not found',\n 'cannot find module',\n 'no such file or directory',\n 'not configured',\n 'missing api key',\n 'missing token',\n 'invalid api key',\n 'credentials are not configured',\n 'authentication required',\n 'unauthorized',\n 'rate limit',\n 'temporarily unavailable',\n 'network error',\n 'econnreset',\n 'etimedout',\n ].some(marker => text.includes(marker));\n}\n\nfunction mostCommonFailureTool(failures: TaskTrajectory[]): string {\n const counts = new Map<string, number>();\n for (const trajectory of failures) {\n for (const detail of trajectory.toolDetails) {\n if (!detail.success) counts.set(detail.name, (counts.get(detail.name) ?? 0) + 1);\n }\n }\n return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? '';\n}\n\nexport function processTrajectoryForLearningReview(trajectory: TaskTrajectory): void {\n try {\n // v6.4.0 — respect the config-level enable flag so operators who\n // turn the curator off in titan.json don't pay any cost per task.\n const cfg = loadCuratorConfig();\n if (!cfg.enabled) return;\n ingestTrajectoryLearningSignal(trajectory);\n const result = runLearningReview(trajectory, new Date(), cfg.minTrajectories);\n if (result.shouldRun) logger.info(COMPONENT, result.summary);\n } catch (err) {\n logger.warn(COMPONENT, `Learning review failed: ${(err as Error).message}`);\n }\n}\n"],"mappings":"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","names":["summary","reportPath"]}
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, statSync } from "fs";
3
3
  import { join } from "path";
4
- import { homedir } from "os";
5
4
  import logger from "../utils/logger.js";
5
+ import { TITAN_HOME } from "../utils/constants.js";
6
6
  const COMPONENT = "TrajectoryLogger";
7
- const TRAJECTORIES_DIR = join(homedir(), ".titan", "trajectories");
7
+ const TRAJECTORIES_DIR = join(TITAN_HOME, "trajectories");
8
8
  const TASK_TRAJECTORIES_FILE = join(TRAJECTORIES_DIR, "task-trajectories.jsonl");
9
9
  const MAX_FILE_SIZE = 50 * 1024 * 1024;
10
10
  function ensureDir() {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/agent/trajectoryLogger.ts"],"sourcesContent":["/**\n * TITAN — Task-Level Trajectory Logger\n *\n * Records task-level trajectories (task, model, tool sequence, success, rounds, duration)\n * to ~/.titan/trajectories/task-trajectories.jsonl for auto-skill generation.\n *\n * Separate from trajectoryCapture.ts which produces ChatML format for LoRA fine-tuning.\n * Inspired by Hermes trajectory.py.\n */\nimport { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, statSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'TrajectoryLogger';\nconst TRAJECTORIES_DIR = join(homedir(), '.titan', 'trajectories');\nconst TASK_TRAJECTORIES_FILE = join(TRAJECTORIES_DIR, 'task-trajectories.jsonl');\nconst MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB\n\n// ── Types ─────────────────────────────────────────────────────────\nexport interface TaskTrajectory {\n id: string;\n timestamp: string;\n /** First 500 chars of the user message */\n task: string;\n taskType: string;\n model: string;\n /** Ordered tool names as executed */\n toolSequence: string[];\n /** Detailed tool call records */\n toolDetails: Array<{\n name: string;\n args: Record<string, unknown>;\n success: boolean;\n resultSnippet: string;\n }>;\n success: boolean;\n rounds: number;\n durationMs: number;\n sessionId: string;\n}\n\n// ── Ensure directory exists ───────────────────────────────────────\nfunction ensureDir(): void {\n if (!existsSync(TRAJECTORIES_DIR)) mkdirSync(TRAJECTORIES_DIR, { recursive: true });\n}\n\n// ── Auto-rotate ───────────────────────────────────────────────────\nfunction rotateIfNeeded(): void {\n if (!existsSync(TASK_TRAJECTORIES_FILE)) return;\n try {\n const stat = statSync(TASK_TRAJECTORIES_FILE);\n if (stat.size > MAX_FILE_SIZE) {\n logger.warn(COMPONENT, `Task trajectories file exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB — rotating`);\n const rotated = `${TASK_TRAJECTORIES_FILE}.${Date.now()}.bak`;\n try { writeFileSync(rotated, readFileSync(TASK_TRAJECTORIES_FILE)); } catch { /* ignore */ }\n writeFileSync(TASK_TRAJECTORIES_FILE, '');\n }\n } catch { /* ignore stat errors */ }\n}\n\n// ── Public API ────────────────────────────────────────────────────\n\n/**\n * Log a task-level trajectory.\n * Fire-and-forget — errors are logged but never thrown.\n */\nexport function logTrajectory(trajectory: TaskTrajectory): void {\n try {\n ensureDir();\n rotateIfNeeded();\n appendFileSync(TASK_TRAJECTORIES_FILE, JSON.stringify(trajectory) + '\\n', 'utf-8');\n logger.debug(COMPONENT, `Logged trajectory: ${trajectory.taskType}, ${trajectory.toolSequence.length} tools, success=${trajectory.success}`);\n } catch (err) {\n logger.warn(COMPONENT, `Failed to log trajectory: ${(err as Error).message}`);\n }\n}\n\n/**\n * Read recent trajectories, optionally filtered.\n */\nexport function getRecentTrajectories(\n limit: number = 50,\n filter?: { taskType?: string; success?: boolean },\n): TaskTrajectory[] {\n if (!existsSync(TASK_TRAJECTORIES_FILE)) return [];\n\n try {\n const content = readFileSync(TASK_TRAJECTORIES_FILE, 'utf-8');\n const lines = content.split('\\n').filter(l => l.trim());\n let trajectories: TaskTrajectory[] = [];\n\n // Read from the end for efficiency\n for (let i = lines.length - 1; i >= 0 && trajectories.length < limit * 2; i--) {\n try {\n trajectories.push(JSON.parse(lines[i]));\n } catch { /* skip malformed lines */ }\n }\n\n // Apply filters\n if (filter?.taskType) {\n trajectories = trajectories.filter(t => t.taskType === filter.taskType);\n }\n if (filter?.success !== undefined) {\n trajectories = trajectories.filter(t => t.success === filter.success);\n }\n\n return trajectories.slice(0, limit);\n } catch {\n return [];\n }\n}\n\n/**\n * Get the tool sequence signature for matching (joined with →).\n */\nexport function getSequenceSignature(toolSequence: string[]): string {\n return toolSequence.join(' → ');\n}\n\n/**\n * Count how many successful trajectories match a given task type and tool sequence.\n */\nexport function countMatchingTrajectories(taskType: string, toolSequence: string[]): number {\n const recent = getRecentTrajectories(200, { taskType, success: true });\n const targetSig = getSequenceSignature(toolSequence);\n return recent.filter(t => getSequenceSignature(t.toolSequence) === targetSig).length;\n}\n"],"mappings":";AASA,SAAS,YAAY,cAAc,eAAe,gBAAgB,WAAW,gBAAgB;AAC7F,SAAS,YAAY;AACrB,SAAS,eAAe;AACxB,OAAO,YAAY;AAEnB,MAAM,YAAY;AAClB,MAAM,mBAAmB,KAAK,QAAQ,GAAG,UAAU,cAAc;AACjE,MAAM,yBAAyB,KAAK,kBAAkB,yBAAyB;AAC/E,MAAM,gBAAgB,KAAK,OAAO;AA0BlC,SAAS,YAAkB;AACvB,MAAI,CAAC,WAAW,gBAAgB,EAAG,WAAU,kBAAkB,EAAE,WAAW,KAAK,CAAC;AACtF;AAGA,SAAS,iBAAuB;AAC5B,MAAI,CAAC,WAAW,sBAAsB,EAAG;AACzC,MAAI;AACA,UAAM,OAAO,SAAS,sBAAsB;AAC5C,QAAI,KAAK,OAAO,eAAe;AAC3B,aAAO,KAAK,WAAW,kCAAkC,gBAAgB,OAAO,IAAI,oBAAe;AACnG,YAAM,UAAU,GAAG,sBAAsB,IAAI,KAAK,IAAI,CAAC;AACvD,UAAI;AAAE,sBAAc,SAAS,aAAa,sBAAsB,CAAC;AAAA,MAAG,QAAQ;AAAA,MAAe;AAC3F,oBAAc,wBAAwB,EAAE;AAAA,IAC5C;AAAA,EACJ,QAAQ;AAAA,EAA2B;AACvC;AAQO,SAAS,cAAc,YAAkC;AAC5D,MAAI;AACA,cAAU;AACV,mBAAe;AACf,mBAAe,wBAAwB,KAAK,UAAU,UAAU,IAAI,MAAM,OAAO;AACjF,WAAO,MAAM,WAAW,sBAAsB,WAAW,QAAQ,KAAK,WAAW,aAAa,MAAM,mBAAmB,WAAW,OAAO,EAAE;AAAA,EAC/I,SAAS,KAAK;AACV,WAAO,KAAK,WAAW,6BAA8B,IAAc,OAAO,EAAE;AAAA,EAChF;AACJ;AAKO,SAAS,sBACZ,QAAgB,IAChB,QACgB;AAChB,MAAI,CAAC,WAAW,sBAAsB,EAAG,QAAO,CAAC;AAEjD,MAAI;AACA,UAAM,UAAU,aAAa,wBAAwB,OAAO;AAC5D,UAAM,QAAQ,QAAQ,MAAM,IAAI,EAAE,OAAO,OAAK,EAAE,KAAK,CAAC;AACtD,QAAI,eAAiC,CAAC;AAGtC,aAAS,IAAI,MAAM,SAAS,GAAG,KAAK,KAAK,aAAa,SAAS,QAAQ,GAAG,KAAK;AAC3E,UAAI;AACA,qBAAa,KAAK,KAAK,MAAM,MAAM,CAAC,CAAC,CAAC;AAAA,MAC1C,QAAQ;AAAA,MAA6B;AAAA,IACzC;AAGA,QAAI,QAAQ,UAAU;AAClB,qBAAe,aAAa,OAAO,OAAK,EAAE,aAAa,OAAO,QAAQ;AAAA,IAC1E;AACA,QAAI,QAAQ,YAAY,QAAW;AAC/B,qBAAe,aAAa,OAAO,OAAK,EAAE,YAAY,OAAO,OAAO;AAAA,IACxE;AAEA,WAAO,aAAa,MAAM,GAAG,KAAK;AAAA,EACtC,QAAQ;AACJ,WAAO,CAAC;AAAA,EACZ;AACJ;AAKO,SAAS,qBAAqB,cAAgC;AACjE,SAAO,aAAa,KAAK,UAAK;AAClC;AAKO,SAAS,0BAA0B,UAAkB,cAAgC;AACxF,QAAM,SAAS,sBAAsB,KAAK,EAAE,UAAU,SAAS,KAAK,CAAC;AACrE,QAAM,YAAY,qBAAqB,YAAY;AACnD,SAAO,OAAO,OAAO,OAAK,qBAAqB,EAAE,YAAY,MAAM,SAAS,EAAE;AAClF;","names":[]}
1
+ {"version":3,"sources":["../../src/agent/trajectoryLogger.ts"],"sourcesContent":["/**\n * TITAN — Task-Level Trajectory Logger\n *\n * Records task-level trajectories (task, model, tool sequence, success, rounds, duration)\n * to ~/.titan/trajectories/task-trajectories.jsonl for auto-skill generation.\n *\n * Separate from trajectoryCapture.ts which produces ChatML format for LoRA fine-tuning.\n * Inspired by Hermes trajectory.py.\n */\nimport { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, statSync } from 'fs';\nimport { join } from 'path';\nimport logger from '../utils/logger.js';\nimport { TITAN_HOME } from '../utils/constants.js';\n\nconst COMPONENT = 'TrajectoryLogger';\nconst TRAJECTORIES_DIR = join(TITAN_HOME, 'trajectories');\nconst TASK_TRAJECTORIES_FILE = join(TRAJECTORIES_DIR, 'task-trajectories.jsonl');\nconst MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB\n\n// ── Types ─────────────────────────────────────────────────────────\nexport interface TaskTrajectory {\n id: string;\n timestamp: string;\n /** First 500 chars of the user message */\n task: string;\n taskType: string;\n model: string;\n /** Ordered tool names as executed */\n toolSequence: string[];\n /** Detailed tool call records */\n toolDetails: Array<{\n name: string;\n args: Record<string, unknown>;\n success: boolean;\n resultSnippet: string;\n }>;\n success: boolean;\n rounds: number;\n durationMs: number;\n sessionId: string;\n}\n\n// ── Ensure directory exists ───────────────────────────────────────\nfunction ensureDir(): void {\n if (!existsSync(TRAJECTORIES_DIR)) mkdirSync(TRAJECTORIES_DIR, { recursive: true });\n}\n\n// ── Auto-rotate ───────────────────────────────────────────────────\nfunction rotateIfNeeded(): void {\n if (!existsSync(TASK_TRAJECTORIES_FILE)) return;\n try {\n const stat = statSync(TASK_TRAJECTORIES_FILE);\n if (stat.size > MAX_FILE_SIZE) {\n logger.warn(COMPONENT, `Task trajectories file exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB — rotating`);\n const rotated = `${TASK_TRAJECTORIES_FILE}.${Date.now()}.bak`;\n try { writeFileSync(rotated, readFileSync(TASK_TRAJECTORIES_FILE)); } catch { /* ignore */ }\n writeFileSync(TASK_TRAJECTORIES_FILE, '');\n }\n } catch { /* ignore stat errors */ }\n}\n\n// ── Public API ────────────────────────────────────────────────────\n\n/**\n * Log a task-level trajectory.\n * Fire-and-forget — errors are logged but never thrown.\n */\nexport function logTrajectory(trajectory: TaskTrajectory): void {\n try {\n ensureDir();\n rotateIfNeeded();\n appendFileSync(TASK_TRAJECTORIES_FILE, JSON.stringify(trajectory) + '\\n', 'utf-8');\n logger.debug(COMPONENT, `Logged trajectory: ${trajectory.taskType}, ${trajectory.toolSequence.length} tools, success=${trajectory.success}`);\n } catch (err) {\n logger.warn(COMPONENT, `Failed to log trajectory: ${(err as Error).message}`);\n }\n}\n\n/**\n * Read recent trajectories, optionally filtered.\n */\nexport function getRecentTrajectories(\n limit: number = 50,\n filter?: { taskType?: string; success?: boolean },\n): TaskTrajectory[] {\n if (!existsSync(TASK_TRAJECTORIES_FILE)) return [];\n\n try {\n const content = readFileSync(TASK_TRAJECTORIES_FILE, 'utf-8');\n const lines = content.split('\\n').filter(l => l.trim());\n let trajectories: TaskTrajectory[] = [];\n\n // Read from the end for efficiency\n for (let i = lines.length - 1; i >= 0 && trajectories.length < limit * 2; i--) {\n try {\n trajectories.push(JSON.parse(lines[i]));\n } catch { /* skip malformed lines */ }\n }\n\n // Apply filters\n if (filter?.taskType) {\n trajectories = trajectories.filter(t => t.taskType === filter.taskType);\n }\n if (filter?.success !== undefined) {\n trajectories = trajectories.filter(t => t.success === filter.success);\n }\n\n return trajectories.slice(0, limit);\n } catch {\n return [];\n }\n}\n\n/**\n * Get the tool sequence signature for matching (joined with →).\n */\nexport function getSequenceSignature(toolSequence: string[]): string {\n return toolSequence.join(' → ');\n}\n\n/**\n * Count how many successful trajectories match a given task type and tool sequence.\n */\nexport function countMatchingTrajectories(taskType: string, toolSequence: string[]): number {\n const recent = getRecentTrajectories(200, { taskType, success: true });\n const targetSig = getSequenceSignature(toolSequence);\n return recent.filter(t => getSequenceSignature(t.toolSequence) === targetSig).length;\n}\n"],"mappings":";AASA,SAAS,YAAY,cAAc,eAAe,gBAAgB,WAAW,gBAAgB;AAC7F,SAAS,YAAY;AACrB,OAAO,YAAY;AACnB,SAAS,kBAAkB;AAE3B,MAAM,YAAY;AAClB,MAAM,mBAAmB,KAAK,YAAY,cAAc;AACxD,MAAM,yBAAyB,KAAK,kBAAkB,yBAAyB;AAC/E,MAAM,gBAAgB,KAAK,OAAO;AA0BlC,SAAS,YAAkB;AACvB,MAAI,CAAC,WAAW,gBAAgB,EAAG,WAAU,kBAAkB,EAAE,WAAW,KAAK,CAAC;AACtF;AAGA,SAAS,iBAAuB;AAC5B,MAAI,CAAC,WAAW,sBAAsB,EAAG;AACzC,MAAI;AACA,UAAM,OAAO,SAAS,sBAAsB;AAC5C,QAAI,KAAK,OAAO,eAAe;AAC3B,aAAO,KAAK,WAAW,kCAAkC,gBAAgB,OAAO,IAAI,oBAAe;AACnG,YAAM,UAAU,GAAG,sBAAsB,IAAI,KAAK,IAAI,CAAC;AACvD,UAAI;AAAE,sBAAc,SAAS,aAAa,sBAAsB,CAAC;AAAA,MAAG,QAAQ;AAAA,MAAe;AAC3F,oBAAc,wBAAwB,EAAE;AAAA,IAC5C;AAAA,EACJ,QAAQ;AAAA,EAA2B;AACvC;AAQO,SAAS,cAAc,YAAkC;AAC5D,MAAI;AACA,cAAU;AACV,mBAAe;AACf,mBAAe,wBAAwB,KAAK,UAAU,UAAU,IAAI,MAAM,OAAO;AACjF,WAAO,MAAM,WAAW,sBAAsB,WAAW,QAAQ,KAAK,WAAW,aAAa,MAAM,mBAAmB,WAAW,OAAO,EAAE;AAAA,EAC/I,SAAS,KAAK;AACV,WAAO,KAAK,WAAW,6BAA8B,IAAc,OAAO,EAAE;AAAA,EAChF;AACJ;AAKO,SAAS,sBACZ,QAAgB,IAChB,QACgB;AAChB,MAAI,CAAC,WAAW,sBAAsB,EAAG,QAAO,CAAC;AAEjD,MAAI;AACA,UAAM,UAAU,aAAa,wBAAwB,OAAO;AAC5D,UAAM,QAAQ,QAAQ,MAAM,IAAI,EAAE,OAAO,OAAK,EAAE,KAAK,CAAC;AACtD,QAAI,eAAiC,CAAC;AAGtC,aAAS,IAAI,MAAM,SAAS,GAAG,KAAK,KAAK,aAAa,SAAS,QAAQ,GAAG,KAAK;AAC3E,UAAI;AACA,qBAAa,KAAK,KAAK,MAAM,MAAM,CAAC,CAAC,CAAC;AAAA,MAC1C,QAAQ;AAAA,MAA6B;AAAA,IACzC;AAGA,QAAI,QAAQ,UAAU;AAClB,qBAAe,aAAa,OAAO,OAAK,EAAE,aAAa,OAAO,QAAQ;AAAA,IAC1E;AACA,QAAI,QAAQ,YAAY,QAAW;AAC/B,qBAAe,aAAa,OAAO,OAAK,EAAE,YAAY,OAAO,OAAO;AAAA,IACxE;AAEA,WAAO,aAAa,MAAM,GAAG,KAAK;AAAA,EACtC,QAAQ;AACJ,WAAO,CAAC;AAAA,EACZ;AACJ;AAKO,SAAS,qBAAqB,cAAgC;AACjE,SAAO,aAAa,KAAK,UAAK;AAClC;AAKO,SAAS,0BAA0B,UAAkB,cAAgC;AACxF,QAAM,SAAS,sBAAsB,KAAK,EAAE,UAAU,SAAS,KAAK,CAAC;AACrE,QAAM,YAAY,qBAAqB,YAAY;AACnD,SAAO,OAAO,OAAO,OAAK,qBAAqB,EAAE,YAAY,MAAM,SAAS,EAAE;AAClF;","names":[]}
@@ -158,7 +158,7 @@ async function hasUsableProvider() {
158
158
  return { ok: true, details: `${key} is set in environment` };
159
159
  }
160
160
  }
161
- const ollamaUrl = providers.ollama?.baseUrl || process.env.OLLAMA_BASE_URL || "http://localhost:11434";
161
+ const ollamaUrl = normalizeOllamaBaseUrl(providers.ollama?.baseUrl || process.env.OLLAMA_BASE_URL || process.env.OLLAMA_HOST || "http://localhost:11434");
162
162
  try {
163
163
  const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(3e3) });
164
164
  if (res.ok) {
@@ -182,7 +182,8 @@ function applyEnvOverrides(config) {
182
182
  ANTHROPIC_API_KEY: (val) => setNested(config, "providers.anthropic.apiKey", val),
183
183
  OPENAI_API_KEY: (val) => setNested(config, "providers.openai.apiKey", val),
184
184
  GOOGLE_API_KEY: (val) => setNested(config, "providers.google.apiKey", val),
185
- OLLAMA_BASE_URL: (val) => setNested(config, "providers.ollama.baseUrl", val),
185
+ OLLAMA_HOST: (val) => setNested(config, "providers.ollama.baseUrl", normalizeOllamaBaseUrl(val)),
186
+ OLLAMA_BASE_URL: (val) => setNested(config, "providers.ollama.baseUrl", normalizeOllamaBaseUrl(val)),
186
187
  DISCORD_TOKEN: (val) => setNested(config, "channels.discord.token", val),
187
188
  TELEGRAM_TOKEN: (val) => setNested(config, "channels.telegram.token", val),
188
189
  SLACK_TOKEN: (val) => setNested(config, "channels.slack.token", val),
@@ -212,6 +213,21 @@ function isProviderDisabled(config, providerName) {
212
213
  const provider = providers?.[providerName];
213
214
  return provider?.enabled === false;
214
215
  }
216
+ function normalizeOllamaBaseUrl(value) {
217
+ let raw = value.trim().replace(/\/+$/, "");
218
+ if (!/^https?:\/\//i.test(raw)) {
219
+ raw = `http://${raw}`;
220
+ }
221
+ try {
222
+ const url = new URL(raw);
223
+ if (url.hostname === "0.0.0.0") {
224
+ url.hostname = "localhost";
225
+ }
226
+ return url.toString().replace(/\/+$/, "");
227
+ } catch {
228
+ return raw;
229
+ }
230
+ }
215
231
  function findDroppedKeys(raw, parsed, pathPrefix, depth = 0) {
216
232
  if (depth > 8) return [];
217
233
  if (!isPlainObject(raw)) return [];
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/config/config.ts"],"sourcesContent":["/**\n * TITAN Configuration Manager\n * Loads, validates, and persists configuration from ~/.titan/titan.json\n */\nimport { existsSync } from 'fs';\nimport { TITAN_CONFIG_PATH, TITAN_HOME } from '../utils/constants.js';\nimport { readJsonFile, writeJsonFile, mkdirIfNotExists, deepMerge } from '../utils/helpers.js';\nimport { TitanConfigSchema, type TitanConfig } from './schema.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Config';\n\nlet cachedConfig: TitanConfig | null = null;\n\n/** Get the default configuration */\nexport function getDefaultConfig(): TitanConfig {\n return TitanConfigSchema.parse({});\n}\n\n/** Load configuration from disk, merging with defaults */\nexport function loadConfig(): TitanConfig {\n if (cachedConfig) return cachedConfig;\n\n mkdirIfNotExists(TITAN_HOME);\n\n let rawConfig: Record<string, unknown> = {};\n\n if (existsSync(TITAN_CONFIG_PATH)) {\n const loaded = readJsonFile<Record<string, unknown>>(TITAN_CONFIG_PATH);\n if (loaded) {\n rawConfig = loaded;\n logger.debug(COMPONENT, `Loaded config from ${TITAN_CONFIG_PATH}`);\n } else {\n logger.warn(COMPONENT, `Failed to parse config at ${TITAN_CONFIG_PATH}, using defaults`);\n }\n } else {\n logger.info(COMPONENT, 'No config file found, using defaults');\n }\n\n // Apply environment variables\n applyEnvOverrides(rawConfig);\n\n // v4.8.4: migrate a top-level `auth` block to `gateway.auth`. The\n // documented path has always been `gateway.auth`, but users (and\n // Claude) naturally try `auth` at the root. Rather than strip it\n // silently and warn, move it to the canonical location and continue.\n if (rawConfig && typeof rawConfig === 'object' && 'auth' in rawConfig) {\n const raw = rawConfig as Record<string, unknown>;\n const topAuth = raw.auth as Record<string, unknown> | undefined;\n if (topAuth && typeof topAuth === 'object') {\n const gateway = (raw.gateway as Record<string, unknown> | undefined) ?? {};\n const gatewayAuth = (gateway.auth as Record<string, unknown> | undefined) ?? {};\n // gateway.auth wins if both are set — explicit nested wins\n // over migrated top-level.\n raw.gateway = { ...gateway, auth: { ...topAuth, ...gatewayAuth } };\n delete raw.auth;\n logger.info(COMPONENT, 'Migrated top-level `auth` → `gateway.auth`. Update titan.json to nest it under `gateway` to silence this notice.');\n }\n }\n\n // Detect unknown keys at every nesting depth BEFORE Zod silently strips\n // them. Hunt Finding #1 (2026-04-14) covered the top-level case\n // (`facebook: {...}` ignored). This expanded check also catches nested\n // typos like `providers.anthropic.unknownField` so users learn about\n // dropped keys immediately rather than chasing \"why doesn't my setting\n // do anything\" days later.\n //\n // The diff is purely informational — invalid keys never block startup\n // (we still feed `rawConfig` through Zod's permissive parse).\n try {\n const parsed = TitanConfigSchema.safeParse(rawConfig);\n if (parsed.success) {\n const droppedPaths = findDroppedKeys(rawConfig, parsed.data as Record<string, unknown>, '');\n for (const path of droppedPaths) {\n logger.warn(\n COMPONENT,\n `Unknown config key: ${path}. Will be ignored. ` +\n `If intentional, extend TitanConfigSchema in src/config/schema.ts.`,\n );\n }\n }\n } catch {\n // Introspection failed — never let a diagnostic warning block load.\n }\n\n // Validate and merge with defaults via Zod.\n // CRITICAL: On validation failure, deep-merge raw config over defaults\n // so that valid user settings (daemon.enabled, autonomy.mode, etc.) survive.\n // Previously this fell back to pure defaults, wiping ALL user config on any error.\n const result = TitanConfigSchema.safeParse(rawConfig);\n if (result.success) {\n cachedConfig = result.data;\n } else {\n const issues = result.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join(', ');\n logger.warn(COMPONENT, `Config validation issues (${issues}) — merging valid fields over defaults`);\n // Deep-merge raw config over defaults so valid sections survive\n const defaults = getDefaultConfig();\n const merged = deepMerge(defaults as Record<string, unknown>, rawConfig) as TitanConfig;\n // Try parsing the merged result — if it still fails, use defaults but log loudly\n const reparse = TitanConfigSchema.safeParse(merged);\n if (reparse.success) {\n cachedConfig = reparse.data;\n } else {\n logger.error(COMPONENT, `Config still invalid after merge — falling back to defaults. Fix your titan.json.`);\n cachedConfig = defaults;\n }\n }\n\n return cachedConfig;\n}\n\n/** Save current configuration to disk */\nexport function saveConfig(config: TitanConfig): void {\n mkdirIfNotExists(TITAN_HOME);\n writeJsonFile(TITAN_CONFIG_PATH, config);\n cachedConfig = config;\n logger.info(COMPONENT, `Config saved to ${TITAN_CONFIG_PATH}`);\n}\n\n/** Update specific fields in the config */\nexport function updateConfig(partial: Partial<TitanConfig>): TitanConfig {\n const current = loadConfig();\n const updated = deepMerge(current as Record<string, unknown>, partial as Record<string, unknown>) as TitanConfig;\n const validated = TitanConfigSchema.parse(updated);\n saveConfig(validated);\n return validated;\n}\n\n/** Reset config cache (useful for testing) */\nexport function resetConfigCache(): void {\n cachedConfig = null;\n}\n\n/** Check if the configuration file exists */\nexport function configExists(): boolean {\n return existsSync(TITAN_CONFIG_PATH);\n}\n\n/**\n * Check if at least one usable AI provider is configured.\n *\n * \"Usable\" means one of:\n * - Any cloud provider has a non-empty `apiKey` set in config\n * - Any *_API_KEY env var is set (Anthropic, OpenAI, Google, Groq, etc.)\n * - Ollama is reachable at the configured baseUrl (returns at least one model)\n *\n * Used by the gateway boot guard and CLI to refuse to start with empty config\n * instead of letting the user hit \"Internal Server Error\" later.\n *\n * Note: Ollama check is async and is the slowest part of this function (~3s timeout).\n * Callers should `await` and only call once at boot.\n */\nexport async function hasUsableProvider(): Promise<{ ok: boolean; details: string }> {\n const config = loadConfig();\n\n // 1. Check config-file API keys (cloud providers)\n const providers = (config.providers as Record<string, unknown> | undefined) || {};\n const cloudProviderNames = [\n 'anthropic', 'openai', 'google', 'groq', 'mistral', 'openrouter',\n 'fireworks', 'xai', 'together', 'deepseek', 'cerebras', 'cohere',\n 'perplexity', 'venice', 'bedrock', 'litellm', 'azure', 'deepinfra',\n 'sambanova', 'kimi', 'huggingface', 'ai21', 'cohere-v2', 'reka',\n 'zhipu', 'yi', 'inflection', 'novita', 'replicate', 'lepton',\n 'anyscale', 'octo', 'nous', 'minimax', 'nvidia',\n ];\n for (const name of cloudProviderNames) {\n const p = providers[name] as { apiKey?: string; enabled?: boolean } | undefined;\n if (p?.enabled === false) continue;\n if (p?.apiKey && p.apiKey.trim().length > 0) {\n return { ok: true, details: `${name} has an API key configured` };\n }\n }\n\n // 2. Check env-var API keys (in case config wasn't reloaded after env var change)\n const envKeys = [\n 'ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GOOGLE_API_KEY', 'GROQ_API_KEY',\n 'MISTRAL_API_KEY', 'OPENROUTER_API_KEY', 'FIREWORKS_API_KEY', 'XAI_API_KEY',\n 'TOGETHER_API_KEY', 'DEEPSEEK_API_KEY', 'CEREBRAS_API_KEY', 'COHERE_API_KEY',\n 'PERPLEXITY_API_KEY', 'AZURE_OPENAI_API_KEY',\n ];\n for (const key of envKeys) {\n if (key === 'ANTHROPIC_API_KEY') {\n const anthropic = providers.anthropic as { enabled?: boolean } | undefined;\n if (anthropic?.enabled === false) continue;\n }\n if (process.env[key] && process.env[key]!.trim().length > 0) {\n return { ok: true, details: `${key} is set in environment` };\n }\n }\n\n // 3. Check Ollama reachability (last resort — slow)\n const ollamaUrl = (providers.ollama as { baseUrl?: string } | undefined)?.baseUrl\n || process.env.OLLAMA_BASE_URL\n || 'http://localhost:11434';\n try {\n const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(3000) });\n if (res.ok) {\n const json = await res.json() as { models?: { name: string }[] };\n const count = (json.models || []).length;\n if (count > 0) {\n return { ok: true, details: `Ollama at ${ollamaUrl} is reachable (${count} models)` };\n }\n return { ok: false, details: `Ollama at ${ollamaUrl} is reachable but has 0 models — run \"ollama pull qwen3.5:4b\"` };\n }\n } catch {\n // Ollama unreachable, fall through to \"no providers\"\n }\n\n return { ok: false, details: 'No API keys configured and Ollama is not running' };\n}\n\n/** Apply environment variable overrides to raw config */\nfunction applyEnvOverrides(config: Record<string, unknown>): void {\n const envMap: Record<string, (val: string) => void> = {\n TITAN_MODEL: (val) => setNested(config, 'agent.model', val),\n TITAN_GATEWAY_PORT: (val) => setNested(config, 'gateway.port', parseInt(val, 10)),\n TITAN_GATEWAY_HOST: (val) => setNested(config, 'gateway.host', val),\n TITAN_LOG_LEVEL: (val) => setNested(config, 'logging.level', val),\n ANTHROPIC_API_KEY: (val) => setNested(config, 'providers.anthropic.apiKey', val),\n OPENAI_API_KEY: (val) => setNested(config, 'providers.openai.apiKey', val),\n GOOGLE_API_KEY: (val) => setNested(config, 'providers.google.apiKey', val),\n OLLAMA_BASE_URL: (val) => setNested(config, 'providers.ollama.baseUrl', val),\n DISCORD_TOKEN: (val) => setNested(config, 'channels.discord.token', val),\n TELEGRAM_TOKEN: (val) => setNested(config, 'channels.telegram.token', val),\n SLACK_TOKEN: (val) => setNested(config, 'channels.slack.token', val),\n GOOGLE_OAUTH_CLIENT_ID: (val) => setNested(config, 'oauth.google.clientId', val),\n GOOGLE_OAUTH_CLIENT_SECRET: (val) => setNested(config, 'oauth.google.clientSecret', val),\n OPENROUTER_API_KEY: (val) => setNested(config, 'providers.openrouter.apiKey', val),\n };\n\n // Cloud mode: auto-configure OpenRouter to point at SaaS gateway\n if (process.env.TITAN_CLOUD_MODE === 'true' && process.env.TITAN_CLOUD_API) {\n const cloudApi = process.env.TITAN_CLOUD_API;\n setNested(config, 'providers.openrouter.baseUrl', cloudApi + '/api/v1');\n logger.debug(COMPONENT, `Cloud mode: OpenRouter base URL set to ${cloudApi}/api/v1`);\n }\n\n for (const [envKey, setter] of Object.entries(envMap)) {\n const val = process.env[envKey];\n if (val) {\n if (envKey === 'ANTHROPIC_API_KEY' && isProviderDisabled(config, 'anthropic')) {\n logger.debug(COMPONENT, 'Skipped env override: ANTHROPIC_API_KEY because providers.anthropic.enabled=false');\n continue;\n }\n setter(val);\n logger.debug(COMPONENT, `Applied env override: ${envKey}`);\n }\n }\n}\n\nfunction isProviderDisabled(config: Record<string, unknown>, providerName: string): boolean {\n const providers = config.providers as Record<string, unknown> | undefined;\n const provider = providers?.[providerName] as { enabled?: unknown } | undefined;\n return provider?.enabled === false;\n}\n\n/**\n * Recursively diff `raw` against the Zod-parsed `parsed` and return the set\n * of dot-notation paths that exist in raw but were stripped by the schema.\n *\n * Behaviour:\n * - Only walks plain objects (Records). Arrays and primitives are leaf\n * comparisons — if the raw value is an object at a key the schema parsed\n * as something else, we treat the whole subtree as dropped at that path.\n * - Recurses into objects that survive parsing so we catch keys deep inside\n * untyped/permissive subtrees (e.g. `providers.<name>.unknownField`).\n * - Cap recursion depth to defend against pathological self-referential\n * configs; 8 levels covers every legitimate path in TitanConfigSchema.\n */\nfunction findDroppedKeys(\n raw: unknown,\n parsed: unknown,\n pathPrefix: string,\n depth: number = 0,\n): string[] {\n if (depth > 8) return [];\n if (!isPlainObject(raw)) return [];\n const dropped: string[] = [];\n for (const key of Object.keys(raw)) {\n const fullPath = pathPrefix ? `${pathPrefix}.${key}` : key;\n const rawVal = raw[key];\n const parsedVal = isPlainObject(parsed) ? (parsed as Record<string, unknown>)[key] : undefined;\n\n if (parsedVal === undefined) {\n // Skip explicit `undefined` in raw (treats `key: undefined` as not-set\n // rather than a dropped key — JSON config files can't express this\n // anyway, but in-memory configs sometimes do via env merging).\n if (rawVal !== undefined) dropped.push(fullPath);\n continue;\n }\n\n // Recurse into matching subtrees so nested unknown keys are also surfaced.\n if (isPlainObject(rawVal) && isPlainObject(parsedVal)) {\n dropped.push(...findDroppedKeys(rawVal, parsedVal, fullPath, depth + 1));\n }\n }\n return dropped;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\n/** Set a nested property by dot-notation path */\nfunction setNested(obj: Record<string, unknown>, path: string, value: unknown): void {\n const parts = path.split('.');\n let current: Record<string, unknown> = obj;\n for (let i = 0; i < parts.length - 1; i++) {\n if (!current[parts[i]] || typeof current[parts[i]] !== 'object') {\n current[parts[i]] = {};\n }\n current = current[parts[i]] as Record<string, unknown>;\n }\n current[parts[parts.length - 1]] = value;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/config/config.ts"],"sourcesContent":["/**\n * TITAN Configuration Manager\n * Loads, validates, and persists configuration from ~/.titan/titan.json\n */\nimport { existsSync } from 'fs';\nimport { TITAN_CONFIG_PATH, TITAN_HOME } from '../utils/constants.js';\nimport { readJsonFile, writeJsonFile, mkdirIfNotExists, deepMerge } from '../utils/helpers.js';\nimport { TitanConfigSchema, type TitanConfig } from './schema.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Config';\n\nlet cachedConfig: TitanConfig | null = null;\n\n/** Get the default configuration */\nexport function getDefaultConfig(): TitanConfig {\n return TitanConfigSchema.parse({});\n}\n\n/** Load configuration from disk, merging with defaults */\nexport function loadConfig(): TitanConfig {\n if (cachedConfig) return cachedConfig;\n\n mkdirIfNotExists(TITAN_HOME);\n\n let rawConfig: Record<string, unknown> = {};\n\n if (existsSync(TITAN_CONFIG_PATH)) {\n const loaded = readJsonFile<Record<string, unknown>>(TITAN_CONFIG_PATH);\n if (loaded) {\n rawConfig = loaded;\n logger.debug(COMPONENT, `Loaded config from ${TITAN_CONFIG_PATH}`);\n } else {\n logger.warn(COMPONENT, `Failed to parse config at ${TITAN_CONFIG_PATH}, using defaults`);\n }\n } else {\n logger.info(COMPONENT, 'No config file found, using defaults');\n }\n\n // Apply environment variables\n applyEnvOverrides(rawConfig);\n\n // v4.8.4: migrate a top-level `auth` block to `gateway.auth`. The\n // documented path has always been `gateway.auth`, but users (and\n // Claude) naturally try `auth` at the root. Rather than strip it\n // silently and warn, move it to the canonical location and continue.\n if (rawConfig && typeof rawConfig === 'object' && 'auth' in rawConfig) {\n const raw = rawConfig as Record<string, unknown>;\n const topAuth = raw.auth as Record<string, unknown> | undefined;\n if (topAuth && typeof topAuth === 'object') {\n const gateway = (raw.gateway as Record<string, unknown> | undefined) ?? {};\n const gatewayAuth = (gateway.auth as Record<string, unknown> | undefined) ?? {};\n // gateway.auth wins if both are set — explicit nested wins\n // over migrated top-level.\n raw.gateway = { ...gateway, auth: { ...topAuth, ...gatewayAuth } };\n delete raw.auth;\n logger.info(COMPONENT, 'Migrated top-level `auth` → `gateway.auth`. Update titan.json to nest it under `gateway` to silence this notice.');\n }\n }\n\n // Detect unknown keys at every nesting depth BEFORE Zod silently strips\n // them. Hunt Finding #1 (2026-04-14) covered the top-level case\n // (`facebook: {...}` ignored). This expanded check also catches nested\n // typos like `providers.anthropic.unknownField` so users learn about\n // dropped keys immediately rather than chasing \"why doesn't my setting\n // do anything\" days later.\n //\n // The diff is purely informational — invalid keys never block startup\n // (we still feed `rawConfig` through Zod's permissive parse).\n try {\n const parsed = TitanConfigSchema.safeParse(rawConfig);\n if (parsed.success) {\n const droppedPaths = findDroppedKeys(rawConfig, parsed.data as Record<string, unknown>, '');\n for (const path of droppedPaths) {\n logger.warn(\n COMPONENT,\n `Unknown config key: ${path}. Will be ignored. ` +\n `If intentional, extend TitanConfigSchema in src/config/schema.ts.`,\n );\n }\n }\n } catch {\n // Introspection failed — never let a diagnostic warning block load.\n }\n\n // Validate and merge with defaults via Zod.\n // CRITICAL: On validation failure, deep-merge raw config over defaults\n // so that valid user settings (daemon.enabled, autonomy.mode, etc.) survive.\n // Previously this fell back to pure defaults, wiping ALL user config on any error.\n const result = TitanConfigSchema.safeParse(rawConfig);\n if (result.success) {\n cachedConfig = result.data;\n } else {\n const issues = result.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join(', ');\n logger.warn(COMPONENT, `Config validation issues (${issues}) — merging valid fields over defaults`);\n // Deep-merge raw config over defaults so valid sections survive\n const defaults = getDefaultConfig();\n const merged = deepMerge(defaults as Record<string, unknown>, rawConfig) as TitanConfig;\n // Try parsing the merged result — if it still fails, use defaults but log loudly\n const reparse = TitanConfigSchema.safeParse(merged);\n if (reparse.success) {\n cachedConfig = reparse.data;\n } else {\n logger.error(COMPONENT, `Config still invalid after merge — falling back to defaults. Fix your titan.json.`);\n cachedConfig = defaults;\n }\n }\n\n return cachedConfig;\n}\n\n/** Save current configuration to disk */\nexport function saveConfig(config: TitanConfig): void {\n mkdirIfNotExists(TITAN_HOME);\n writeJsonFile(TITAN_CONFIG_PATH, config);\n cachedConfig = config;\n logger.info(COMPONENT, `Config saved to ${TITAN_CONFIG_PATH}`);\n}\n\n/** Update specific fields in the config */\nexport function updateConfig(partial: Partial<TitanConfig>): TitanConfig {\n const current = loadConfig();\n const updated = deepMerge(current as Record<string, unknown>, partial as Record<string, unknown>) as TitanConfig;\n const validated = TitanConfigSchema.parse(updated);\n saveConfig(validated);\n return validated;\n}\n\n/** Reset config cache (useful for testing) */\nexport function resetConfigCache(): void {\n cachedConfig = null;\n}\n\n/** Check if the configuration file exists */\nexport function configExists(): boolean {\n return existsSync(TITAN_CONFIG_PATH);\n}\n\n/**\n * Check if at least one usable AI provider is configured.\n *\n * \"Usable\" means one of:\n * - Any cloud provider has a non-empty `apiKey` set in config\n * - Any *_API_KEY env var is set (Anthropic, OpenAI, Google, Groq, etc.)\n * - Ollama is reachable at the configured baseUrl (returns at least one model)\n *\n * Used by the gateway boot guard and CLI to refuse to start with empty config\n * instead of letting the user hit \"Internal Server Error\" later.\n *\n * Note: Ollama check is async and is the slowest part of this function (~3s timeout).\n * Callers should `await` and only call once at boot.\n */\nexport async function hasUsableProvider(): Promise<{ ok: boolean; details: string }> {\n const config = loadConfig();\n\n // 1. Check config-file API keys (cloud providers)\n const providers = (config.providers as Record<string, unknown> | undefined) || {};\n const cloudProviderNames = [\n 'anthropic', 'openai', 'google', 'groq', 'mistral', 'openrouter',\n 'fireworks', 'xai', 'together', 'deepseek', 'cerebras', 'cohere',\n 'perplexity', 'venice', 'bedrock', 'litellm', 'azure', 'deepinfra',\n 'sambanova', 'kimi', 'huggingface', 'ai21', 'cohere-v2', 'reka',\n 'zhipu', 'yi', 'inflection', 'novita', 'replicate', 'lepton',\n 'anyscale', 'octo', 'nous', 'minimax', 'nvidia',\n ];\n for (const name of cloudProviderNames) {\n const p = providers[name] as { apiKey?: string; enabled?: boolean } | undefined;\n if (p?.enabled === false) continue;\n if (p?.apiKey && p.apiKey.trim().length > 0) {\n return { ok: true, details: `${name} has an API key configured` };\n }\n }\n\n // 2. Check env-var API keys (in case config wasn't reloaded after env var change)\n const envKeys = [\n 'ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GOOGLE_API_KEY', 'GROQ_API_KEY',\n 'MISTRAL_API_KEY', 'OPENROUTER_API_KEY', 'FIREWORKS_API_KEY', 'XAI_API_KEY',\n 'TOGETHER_API_KEY', 'DEEPSEEK_API_KEY', 'CEREBRAS_API_KEY', 'COHERE_API_KEY',\n 'PERPLEXITY_API_KEY', 'AZURE_OPENAI_API_KEY',\n ];\n for (const key of envKeys) {\n if (key === 'ANTHROPIC_API_KEY') {\n const anthropic = providers.anthropic as { enabled?: boolean } | undefined;\n if (anthropic?.enabled === false) continue;\n }\n if (process.env[key] && process.env[key]!.trim().length > 0) {\n return { ok: true, details: `${key} is set in environment` };\n }\n }\n\n // 3. Check Ollama reachability (last resort — slow)\n const ollamaUrl = normalizeOllamaBaseUrl((providers.ollama as { baseUrl?: string } | undefined)?.baseUrl\n || process.env.OLLAMA_BASE_URL\n || process.env.OLLAMA_HOST\n || 'http://localhost:11434');\n try {\n const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(3000) });\n if (res.ok) {\n const json = await res.json() as { models?: { name: string }[] };\n const count = (json.models || []).length;\n if (count > 0) {\n return { ok: true, details: `Ollama at ${ollamaUrl} is reachable (${count} models)` };\n }\n return { ok: false, details: `Ollama at ${ollamaUrl} is reachable but has 0 models — run \"ollama pull qwen3.5:4b\"` };\n }\n } catch {\n // Ollama unreachable, fall through to \"no providers\"\n }\n\n return { ok: false, details: 'No API keys configured and Ollama is not running' };\n}\n\n/** Apply environment variable overrides to raw config */\nfunction applyEnvOverrides(config: Record<string, unknown>): void {\n const envMap: Record<string, (val: string) => void> = {\n TITAN_MODEL: (val) => setNested(config, 'agent.model', val),\n TITAN_GATEWAY_PORT: (val) => setNested(config, 'gateway.port', parseInt(val, 10)),\n TITAN_GATEWAY_HOST: (val) => setNested(config, 'gateway.host', val),\n TITAN_LOG_LEVEL: (val) => setNested(config, 'logging.level', val),\n ANTHROPIC_API_KEY: (val) => setNested(config, 'providers.anthropic.apiKey', val),\n OPENAI_API_KEY: (val) => setNested(config, 'providers.openai.apiKey', val),\n GOOGLE_API_KEY: (val) => setNested(config, 'providers.google.apiKey', val),\n OLLAMA_HOST: (val) => setNested(config, 'providers.ollama.baseUrl', normalizeOllamaBaseUrl(val)),\n OLLAMA_BASE_URL: (val) => setNested(config, 'providers.ollama.baseUrl', normalizeOllamaBaseUrl(val)),\n DISCORD_TOKEN: (val) => setNested(config, 'channels.discord.token', val),\n TELEGRAM_TOKEN: (val) => setNested(config, 'channels.telegram.token', val),\n SLACK_TOKEN: (val) => setNested(config, 'channels.slack.token', val),\n GOOGLE_OAUTH_CLIENT_ID: (val) => setNested(config, 'oauth.google.clientId', val),\n GOOGLE_OAUTH_CLIENT_SECRET: (val) => setNested(config, 'oauth.google.clientSecret', val),\n OPENROUTER_API_KEY: (val) => setNested(config, 'providers.openrouter.apiKey', val),\n };\n\n // Cloud mode: auto-configure OpenRouter to point at SaaS gateway\n if (process.env.TITAN_CLOUD_MODE === 'true' && process.env.TITAN_CLOUD_API) {\n const cloudApi = process.env.TITAN_CLOUD_API;\n setNested(config, 'providers.openrouter.baseUrl', cloudApi + '/api/v1');\n logger.debug(COMPONENT, `Cloud mode: OpenRouter base URL set to ${cloudApi}/api/v1`);\n }\n\n for (const [envKey, setter] of Object.entries(envMap)) {\n const val = process.env[envKey];\n if (val) {\n if (envKey === 'ANTHROPIC_API_KEY' && isProviderDisabled(config, 'anthropic')) {\n logger.debug(COMPONENT, 'Skipped env override: ANTHROPIC_API_KEY because providers.anthropic.enabled=false');\n continue;\n }\n setter(val);\n logger.debug(COMPONENT, `Applied env override: ${envKey}`);\n }\n }\n}\n\nfunction isProviderDisabled(config: Record<string, unknown>, providerName: string): boolean {\n const providers = config.providers as Record<string, unknown> | undefined;\n const provider = providers?.[providerName] as { enabled?: unknown } | undefined;\n return provider?.enabled === false;\n}\n\nfunction normalizeOllamaBaseUrl(value: string): string {\n let raw = value.trim().replace(/\\/+$/, '');\n if (!/^https?:\\/\\//i.test(raw)) {\n raw = `http://${raw}`;\n }\n\n try {\n const url = new URL(raw);\n if (url.hostname === '0.0.0.0') {\n url.hostname = 'localhost';\n }\n return url.toString().replace(/\\/+$/, '');\n } catch {\n return raw;\n }\n}\n\n/**\n * Recursively diff `raw` against the Zod-parsed `parsed` and return the set\n * of dot-notation paths that exist in raw but were stripped by the schema.\n *\n * Behaviour:\n * - Only walks plain objects (Records). Arrays and primitives are leaf\n * comparisons — if the raw value is an object at a key the schema parsed\n * as something else, we treat the whole subtree as dropped at that path.\n * - Recurses into objects that survive parsing so we catch keys deep inside\n * untyped/permissive subtrees (e.g. `providers.<name>.unknownField`).\n * - Cap recursion depth to defend against pathological self-referential\n * configs; 8 levels covers every legitimate path in TitanConfigSchema.\n */\nfunction findDroppedKeys(\n raw: unknown,\n parsed: unknown,\n pathPrefix: string,\n depth: number = 0,\n): string[] {\n if (depth > 8) return [];\n if (!isPlainObject(raw)) return [];\n const dropped: string[] = [];\n for (const key of Object.keys(raw)) {\n const fullPath = pathPrefix ? `${pathPrefix}.${key}` : key;\n const rawVal = raw[key];\n const parsedVal = isPlainObject(parsed) ? (parsed as Record<string, unknown>)[key] : undefined;\n\n if (parsedVal === undefined) {\n // Skip explicit `undefined` in raw (treats `key: undefined` as not-set\n // rather than a dropped key — JSON config files can't express this\n // anyway, but in-memory configs sometimes do via env merging).\n if (rawVal !== undefined) dropped.push(fullPath);\n continue;\n }\n\n // Recurse into matching subtrees so nested unknown keys are also surfaced.\n if (isPlainObject(rawVal) && isPlainObject(parsedVal)) {\n dropped.push(...findDroppedKeys(rawVal, parsedVal, fullPath, depth + 1));\n }\n }\n return dropped;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\n/** Set a nested property by dot-notation path */\nfunction setNested(obj: Record<string, unknown>, path: string, value: unknown): void {\n const parts = path.split('.');\n let current: Record<string, unknown> = obj;\n for (let i = 0; i < parts.length - 1; i++) {\n if (!current[parts[i]] || typeof current[parts[i]] !== 'object') {\n current[parts[i]] = {};\n }\n current = current[parts[i]] as Record<string, unknown>;\n }\n current[parts[parts.length - 1]] = value;\n}\n"],"mappings":"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","names":[]}
@@ -1366,6 +1366,28 @@ const TitanConfigSchema = z.object({
1366
1366
  gratitude: z.number().min(0).max(1).default(0.05)
1367
1367
  }).default({})
1368
1368
  }).default({}),
1369
+ /**
1370
+ * Learning Curator (v6.4.0+) — background review pass that turns
1371
+ * repeated successful tool sequences into durable "win" lessons and
1372
+ * repeated structural failures into "watch" warnings. Lessons are
1373
+ * surfaced in the agent's system prompt at task start; reports land
1374
+ * at ~/.titan/logs/learning-curator/<stamp>/.
1375
+ *
1376
+ * Defaults are conservative: 24h between review runs, requires 3
1377
+ * trajectories of a given task type before extracting lessons,
1378
+ * promotes patterns to "stale" after 30 days idle and archives them
1379
+ * after 90. Set `enabled: false` to skip the curator entirely.
1380
+ *
1381
+ * Implementation lives in src/agent/learningCurator.ts (MIT-licensed
1382
+ * port of the NousResearch/hermes-agent background-review pattern).
1383
+ */
1384
+ learningCurator: z.object({
1385
+ enabled: z.boolean().default(true),
1386
+ intervalHours: z.number().min(1).max(720).default(24),
1387
+ minTrajectories: z.number().min(2).max(50).default(3),
1388
+ staleAfterDays: z.number().min(1).max(365).default(30),
1389
+ archiveAfterDays: z.number().min(1).max(730).default(90)
1390
+ }).default({}),
1369
1391
  /**
1370
1392
  * Persona Profiles (v5.5.24+ — Dad Mode plumbing) — TITAN can run as
1371
1393
  * different personas based on time-of-day, channel, or caller. The