titan-agent 6.1.0-beta.3 → 6.1.0-beta.30

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 (294) hide show
  1. package/LICENSE +1 -29
  2. package/README.md +1 -1
  3. package/dist/agent/agentLessons.js +2 -2
  4. package/dist/agent/agentLessons.js.map +1 -1
  5. package/dist/agent/autoModeClassifier.js +95 -0
  6. package/dist/agent/autoModeClassifier.js.map +1 -0
  7. package/dist/agent/durableJournal.js +2 -2
  8. package/dist/agent/durableJournal.js.map +1 -1
  9. package/dist/agent/goalDriver.js +67 -0
  10. package/dist/agent/goalDriver.js.map +1 -1
  11. package/dist/agent/goalPlanFile.js +3 -3
  12. package/dist/agent/goalPlanFile.js.map +1 -1
  13. package/dist/agent/missionLifecycle.js +2 -2
  14. package/dist/agent/missionLifecycle.js.map +1 -1
  15. package/dist/agent/selfKnowledge.js +251 -0
  16. package/dist/agent/selfKnowledge.js.map +1 -0
  17. package/dist/agent/subAgent.js +41 -0
  18. package/dist/agent/subAgent.js.map +1 -1
  19. package/dist/agent/toolContract.js +49 -0
  20. package/dist/agent/toolContract.js.map +1 -0
  21. package/dist/agent/toolRunner.js +139 -11
  22. package/dist/agent/toolRunner.js.map +1 -1
  23. package/dist/agent/trajectoryRecorder.js +185 -0
  24. package/dist/agent/trajectoryRecorder.js.map +1 -0
  25. package/dist/agent/verification.js +164 -0
  26. package/dist/agent/verification.js.map +1 -0
  27. package/dist/agent/worktreeExecutor.js +207 -0
  28. package/dist/agent/worktreeExecutor.js.map +1 -0
  29. package/dist/cli/index.js +3 -2
  30. package/dist/cli/index.js.map +1 -1
  31. package/dist/config/config.js +14 -0
  32. package/dist/config/config.js.map +1 -1
  33. package/dist/config/schema.js +30 -1
  34. package/dist/config/schema.js.map +1 -1
  35. package/dist/gateway/routes/commandPost.js +4 -1
  36. package/dist/gateway/routes/commandPost.js.map +1 -1
  37. package/dist/gateway/routes/files.js +18 -0
  38. package/dist/gateway/routes/files.js.map +1 -1
  39. package/dist/gateway/routes/mesh.js +13 -0
  40. package/dist/gateway/routes/mesh.js.map +1 -1
  41. package/dist/gateway/routes/systemRouter.js +24 -8
  42. package/dist/gateway/routes/systemRouter.js.map +1 -1
  43. package/dist/gateway/server.js +128 -2
  44. package/dist/gateway/server.js.map +1 -1
  45. package/dist/memory/facade.js +137 -0
  46. package/dist/memory/facade.js.map +1 -0
  47. package/dist/migrations/003-soma-profile-default.js +2 -2
  48. package/dist/migrations/003-soma-profile-default.js.map +1 -1
  49. package/dist/migrations/005-route-redirects.js +3 -1
  50. package/dist/migrations/005-route-redirects.js.map +1 -1
  51. package/dist/migrations/runner.js +2 -2
  52. package/dist/migrations/runner.js.map +1 -1
  53. package/dist/providers/anthropic.js +1 -0
  54. package/dist/providers/anthropic.js.map +1 -1
  55. package/dist/providers/defaultModel.js +14 -0
  56. package/dist/providers/defaultModel.js.map +1 -1
  57. package/dist/providers/router.js +4 -0
  58. package/dist/providers/router.js.map +1 -1
  59. package/dist/providers/stub.js +185 -0
  60. package/dist/providers/stub.js.map +1 -0
  61. package/dist/receipts/mint.js +20 -0
  62. package/dist/receipts/mint.js.map +1 -0
  63. package/dist/receipts/store.js +129 -0
  64. package/dist/receipts/store.js.map +1 -0
  65. package/dist/receipts/types.js +20 -0
  66. package/dist/receipts/types.js.map +1 -0
  67. package/dist/skills/builtin/backup.js +1 -2
  68. package/dist/skills/builtin/backup.js.map +1 -1
  69. package/dist/skills/builtin/contracts.js +254 -0
  70. package/dist/skills/builtin/contracts.js.map +1 -0
  71. package/dist/skills/builtin/download_image.js +9 -0
  72. package/dist/skills/builtin/download_image.js.map +1 -1
  73. package/dist/skills/builtin/fb_autopilot.js +71 -76
  74. package/dist/skills/builtin/fb_autopilot.js.map +1 -1
  75. package/dist/skills/builtin/fb_truth_verifier.js +78 -0
  76. package/dist/skills/builtin/fb_truth_verifier.js.map +1 -0
  77. package/dist/skills/builtin/filesystem.js +70 -20
  78. package/dist/skills/builtin/filesystem.js.map +1 -1
  79. package/dist/skills/registry.js +7 -0
  80. package/dist/skills/registry.js.map +1 -1
  81. package/dist/storage/patterns.js +2 -2
  82. package/dist/storage/patterns.js.map +1 -1
  83. package/dist/storage/personalGallery.js +2 -2
  84. package/dist/storage/personalGallery.js.map +1 -1
  85. package/dist/storage/somaProfile.js +2 -2
  86. package/dist/storage/somaProfile.js.map +1 -1
  87. package/dist/utils/constants.js +1 -1
  88. package/dist/utils/constants.js.map +1 -1
  89. package/package.json +2 -2
  90. package/ui/dist/assets/{AuditPanel-DrDvXZ9u.js → AuditPanel-B3L9HrNo.js} +1 -1
  91. package/ui/dist/assets/{AutonomyPanel-BfhW-PRX.js → AutonomyPanel-BF7S_AkR.js} +2 -2
  92. package/ui/dist/assets/{AutopilotPanel-C3yQBxAp.js → AutopilotPanel-CNx3wUzL.js} +1 -1
  93. package/ui/dist/assets/{AutoresearchPanel-BgpR3EcK.js → AutoresearchPanel-CiztTown.js} +2 -2
  94. package/ui/dist/assets/BackupPanel-m5DyPmuv.js +1 -0
  95. package/ui/dist/assets/{BrowserPanel-BNaKZOnn.js → BrowserPanel-C0a2wUR1.js} +2 -2
  96. package/ui/dist/assets/CPActivity-DqxnChTX.js +1 -0
  97. package/ui/dist/assets/CPAgentDetail-xEGrXK3J.js +1 -0
  98. package/ui/dist/assets/CPAgents-DVUSZ-yT.js +1 -0
  99. package/ui/dist/assets/CPApprovals--h9hlHgp.js +1 -0
  100. package/ui/dist/assets/CPCosts-DmBNSecZ.js +1 -0
  101. package/ui/dist/assets/CPDashboard-BPnbfAg5.js +1 -0
  102. package/ui/dist/assets/CPFiles-Btx98YUS.js +1 -0
  103. package/ui/dist/assets/CPGoals-DSAtQs-0.js +1 -0
  104. package/ui/dist/assets/{CPInbox-DUW_hntB.js → CPInbox-oIQDwZCH.js} +2 -2
  105. package/ui/dist/assets/{CPIssueDetail-DHxqtiuh.js → CPIssueDetail-DBTOrkxM.js} +1 -1
  106. package/ui/dist/assets/CPIssues-BxrLJoW-.js +1 -0
  107. package/ui/dist/assets/CPLayout-BamUXAWv.js +2 -0
  108. package/ui/dist/assets/CPOrg-DeZHvyhy.js +8 -0
  109. package/ui/dist/assets/CPRuns-B1yBjvZ8.js +1 -0
  110. package/ui/dist/assets/{CPSocial-By_FStZ4.js → CPSocial-CdEeC-RT.js} +3 -3
  111. package/ui/dist/assets/CPVoice-xlIvZ1c4.js +1 -0
  112. package/ui/dist/assets/ChannelsPanel-CuWN2IPI.js +1 -0
  113. package/ui/dist/assets/CheckpointsPanel-Dq9TUgG4.js +1 -0
  114. package/ui/dist/assets/CommandPostHub-CrguKPqj.js +24 -0
  115. package/ui/dist/assets/CronPanel-Cgr_KHlR.js +1 -0
  116. package/ui/dist/assets/{DataTable-seaktPzB.js → DataTable-CfAgziO1.js} +1 -1
  117. package/ui/dist/assets/DreamPanel-CqrxPytb.js +15 -0
  118. package/ui/dist/assets/{EmptyState-CmK9fywh.js → EmptyState-C6kbnBYJ.js} +1 -1
  119. package/ui/dist/assets/{EvalHarnessPanel-2RVBKbym.js → EvalHarnessPanel-DMcRi5O7.js} +2 -2
  120. package/ui/dist/assets/EvalPanel-BGXxr3Hf.js +1 -0
  121. package/ui/dist/assets/{FileViewer-D98m-vF5.js → FileViewer-B0sYoDfP.js} +52 -14
  122. package/ui/dist/assets/{FilesPanel-dRQM0apb.js → FilesPanel-Wvs7UZOC.js} +2 -2
  123. package/ui/dist/assets/FleetPanel-BfGq2LXe.js +1 -0
  124. package/ui/dist/assets/{HomelabPanel-DjMUrO2B.js → HomelabPanel-CwU2Pws-.js} +2 -2
  125. package/ui/dist/assets/InfraView-DpBxrt7P.js +2 -0
  126. package/ui/dist/assets/InlineEditableField-CHChe_QG.js +1 -0
  127. package/ui/dist/assets/Input-BWm662Jd.js +1 -0
  128. package/ui/dist/assets/IntegrationsPanel-DpW5ngbn.js +1 -0
  129. package/ui/dist/assets/IntelligenceView-DGa9p0fj.js +2 -0
  130. package/ui/dist/assets/LearningPanel-CHISywto.js +1 -0
  131. package/ui/dist/assets/LogsPanel-BuPs3bDR.js +1 -0
  132. package/ui/dist/assets/McpPanel-BpE_2ucx.js +1 -0
  133. package/ui/dist/assets/{MemoryGraphPanel-F1P9COyY.js → MemoryGraphPanel-D1kTkBtg.js} +1 -1
  134. package/ui/dist/assets/MemoryWikiPanel-CxxpJCOF.js +11 -0
  135. package/ui/dist/assets/{MeshPanel-CgIAh5ZX.js → MeshPanel-1i2Noa-5.js} +1 -1
  136. package/ui/dist/assets/MissionCanvas-BePN84Oj.js +69 -0
  137. package/ui/dist/assets/MissionChat-7U1M7fPh.js +7 -0
  138. package/ui/dist/assets/MissionLibrary-CDrR_RnU.js +5 -0
  139. package/ui/dist/assets/MissionStart-rD3amW38.js +1 -0
  140. package/ui/dist/assets/Modal-DtNJ3LE-.js +1 -0
  141. package/ui/dist/assets/NvidiaPanel-C2DOmB5f.js +1 -0
  142. package/ui/dist/assets/OrganismPanel-PFPjRz8v.js +1 -0
  143. package/ui/dist/assets/OverviewPanel-z8ks3icJ.js +1 -0
  144. package/ui/dist/assets/{PageHeader-llOXO43Z.js → PageHeader-Dn2qj2Kj.js} +1 -1
  145. package/ui/dist/assets/{PersonaProfilesPanel-CfsQTMra.js → PersonaProfilesPanel-Bj_HBhG_.js} +3 -3
  146. package/ui/dist/assets/{PersonasPanel-DIac_kZ3.js → PersonasPanel-vpUMfuDW.js} +1 -1
  147. package/ui/dist/assets/RecipesPanel-D28dnS79.js +1 -0
  148. package/ui/dist/assets/{SecurityPanel-_gqMk8hJ.js → SecurityPanel-Cejjk8By.js} +1 -1
  149. package/ui/dist/assets/SelfImprovePanel-GFTusfru.js +1 -0
  150. package/ui/dist/assets/{SelfProposalsPanel-DuA0zsGH.js → SelfProposalsPanel-D20Gw3e6.js} +2 -2
  151. package/ui/dist/assets/SessionsPanel-CVQobM2d.js +1 -0
  152. package/ui/dist/assets/{SessionsTab-bFYaFvTZ.js → SessionsTab-B5cMFZrG.js} +1 -1
  153. package/ui/dist/assets/SettingsPanel-BoRLaakM.js +1 -0
  154. package/ui/dist/assets/SettingsView-BpON3ZzE.js +2 -0
  155. package/ui/dist/assets/SkeletonLoader-CzvWhRTU.js +1 -0
  156. package/ui/dist/assets/{SkillsPanel-D08WcRGp.js → SkillsPanel-D5lTxK5o.js} +1 -1
  157. package/ui/dist/assets/SomaView-DKcfB6c5.js +5 -0
  158. package/ui/dist/assets/{StatCard-DT4Bx1mY.js → StatCard-Bgvwp3FF.js} +1 -1
  159. package/ui/dist/assets/StatusBadge-qL4_LVRL.js +1 -0
  160. package/ui/dist/assets/Tabs-BFA-5kYV.js +1 -0
  161. package/ui/dist/assets/TeamsPanel-DKXeQCNn.js +1 -0
  162. package/ui/dist/assets/TelemetryPanel-CWJDaG8x.js +1 -0
  163. package/ui/dist/assets/TitanCanvas-DwqtbEQq.js +1092 -0
  164. package/ui/dist/assets/ToolsView-KFXwTbe8.js +2 -0
  165. package/ui/dist/assets/Tooltip-V6euBRzx.js +6 -0
  166. package/ui/dist/assets/TraceViewer-Ck_utHcf.js +1 -0
  167. package/ui/dist/assets/TrainingPanel-DzAyIMX8.js +1 -0
  168. package/ui/dist/assets/VoiceOverlay-BPG2HNab.js +17 -0
  169. package/ui/dist/assets/VramPanel-Dvx-iO-I.js +1 -0
  170. package/ui/dist/assets/{WatchView-CaQMDEAy.js → WatchView-CVLTtUfC.js} +1 -1
  171. package/ui/dist/assets/WorkTab-CVlg5-LK.js +1 -0
  172. package/ui/dist/assets/{WorkflowsPanel-DdY6E93C.js → WorkflowsPanel-BdjlRG1W.js} +3 -3
  173. package/ui/dist/assets/archive-DEOaYyeV.js +6 -0
  174. package/ui/dist/assets/{arrow-left-CHJyrO8S.js → arrow-left-BH3XT14o.js} +1 -1
  175. package/ui/dist/assets/book-open-DapDWgUZ.js +6 -0
  176. package/ui/dist/assets/bot-Du0ax_yn.js +6 -0
  177. package/ui/dist/assets/{chart-column-wFkjV3Ni.js → chart-column-B7X5H-lj.js} +1 -1
  178. package/ui/dist/assets/{check-BcOm5jPO.js → check-B2XIY89r.js} +1 -1
  179. package/ui/dist/assets/{chevron-right-aFDDgFsL.js → chevron-right-BLEvMG0x.js} +1 -1
  180. package/ui/dist/assets/{chevron-up-D6g7FB6i.js → chevron-up-Cj9nIxNn.js} +1 -1
  181. package/ui/dist/assets/{circle-check-big-DPz64kZW.js → circle-check-big-Dn2Azr0U.js} +1 -1
  182. package/ui/dist/assets/circle-stop-CwLCglgw.js +6 -0
  183. package/ui/dist/assets/{clock-BbCCaukN.js → clock-Cdc5yE9P.js} +1 -1
  184. package/ui/dist/assets/clsx-B-dksMZM.js +1 -0
  185. package/ui/dist/assets/{dollar-sign-Dy5-qkg3.js → dollar-sign-D0DQNm0Y.js} +1 -1
  186. package/ui/dist/assets/{download-DjheLEns.js → download-CJS_fyfR.js} +1 -1
  187. package/ui/dist/assets/{external-link-BgxGF7ZQ.js → external-link-Bwj11lf7.js} +1 -1
  188. package/ui/dist/assets/file-text-BUa5yWcU.js +6 -0
  189. package/ui/dist/assets/flask-conical-VdP9FVzz.js +6 -0
  190. package/ui/dist/assets/folder-E4G-W6AS.js +6 -0
  191. package/ui/dist/assets/{funnel-LWmcwhj-.js → funnel-CVvLlDVB.js} +1 -1
  192. package/ui/dist/assets/{git-branch-CPZciK10.js → git-branch-BU_0f26_.js} +1 -1
  193. package/ui/dist/assets/index-B_7lW4Qc.css +1 -0
  194. package/ui/dist/assets/index-D_5iu3SA.js +337 -0
  195. package/ui/dist/assets/{layers-Bn7bgu2Z.js → layers-DE1xsfwd.js} +1 -1
  196. package/ui/dist/assets/{legacy-BVPeht6t.js → legacy-DLp8i1LC.js} +2 -2
  197. package/ui/dist/assets/{lightbulb-tzuCyEQk.js → lightbulb-CH4MF2YL.js} +1 -1
  198. package/ui/dist/assets/{list-todo-say4NbSw.js → list-todo-Bst-LnJM.js} +1 -1
  199. package/ui/dist/assets/message-square-CjdKSzno.js +6 -0
  200. package/ui/dist/assets/mic-CwQoAiLh.js +6 -0
  201. package/ui/dist/assets/missions-BX5xVwDv.js +1 -0
  202. package/ui/dist/assets/{network-v9O7MmZT.js → network-BeRafIEW.js} +1 -1
  203. package/ui/dist/assets/{pause-CZo33QlA.js → pause-DsIB9yI3.js} +1 -1
  204. package/ui/dist/assets/pencil-Fhvy079i.js +6 -0
  205. package/ui/dist/assets/{play-DbCJhc5H.js → play-TAtm8pQC.js} +1 -1
  206. package/ui/dist/assets/{plug-CtpCZg2z.js → plug-BL6QB3oA.js} +1 -1
  207. package/ui/dist/assets/{plus-O61rB_YF.js → plus-DQ_yV8ZG.js} +1 -1
  208. package/ui/dist/assets/{proxy-BAUTNyg6.js → proxy-D-OUMPUu.js} +1 -1
  209. package/ui/dist/assets/radio-BobvLW8x.js +6 -0
  210. package/ui/dist/assets/refresh-cw-B0hhX07m.js +6 -0
  211. package/ui/dist/assets/{rotate-ccw-n5WkWR51.js → rotate-ccw-CLTjdVF1.js} +1 -1
  212. package/ui/dist/assets/{save-D2Qh8KyN.js → save-6H0hs-ZN.js} +1 -1
  213. package/ui/dist/assets/{search-C9SdcZJq.js → search-BTPZbawT.js} +1 -1
  214. package/ui/dist/assets/{send-DZ_5M5QQ.js → send-CDdMryGx.js} +1 -1
  215. package/ui/dist/assets/server-DoiE9766.js +6 -0
  216. package/ui/dist/assets/{shield-check-DyP5PmaY.js → shield-check-CKUjkHCO.js} +1 -1
  217. package/ui/dist/assets/terminal-CwajMMS6.js +6 -0
  218. package/ui/dist/assets/test-tube-CI21vdeK.js +11 -0
  219. package/ui/dist/assets/{toggle-right-CmYQLQRS.js → toggle-right-Bo4wxxIe.js} +1 -1
  220. package/ui/dist/assets/{trash-2-TQcEJEv4.js → trash-2-CbERfSyo.js} +1 -1
  221. package/ui/dist/assets/{trending-up-Cw22bM4J.js → trending-up-Cn3913re.js} +1 -1
  222. package/ui/dist/assets/{trophy-DUF8Bcas.js → trophy-CRk39Jg4.js} +1 -1
  223. package/ui/dist/assets/volume-2-Bmjgb607.js +6 -0
  224. package/ui/dist/assets/wifi-off-CpZ9nZtg.js +6 -0
  225. package/ui/dist/index.html +2 -2
  226. package/ui/dist/sw.js +1 -1
  227. package/ui/dist/assets/BackupPanel-BNxMlNIP.js +0 -1
  228. package/ui/dist/assets/CPActivity-CjIgehOq.js +0 -1
  229. package/ui/dist/assets/CPAgentDetail-oaymYRNI.js +0 -1
  230. package/ui/dist/assets/CPAgents-CWQ6iJfO.js +0 -1
  231. package/ui/dist/assets/CPApprovals-DzATBhtM.js +0 -1
  232. package/ui/dist/assets/CPCosts-xVQR148_.js +0 -1
  233. package/ui/dist/assets/CPDashboard-Dt6Jr7Zw.js +0 -1
  234. package/ui/dist/assets/CPFiles-CUpP5I82.js +0 -1
  235. package/ui/dist/assets/CPGoals-Bt1a3C9T.js +0 -1
  236. package/ui/dist/assets/CPIssues-D0l3fgUs.js +0 -1
  237. package/ui/dist/assets/CPLayout-J0fa-it4.js +0 -17
  238. package/ui/dist/assets/CPOrg-DQ3tbYld.js +0 -8
  239. package/ui/dist/assets/CPRuns-0vncicUW.js +0 -1
  240. package/ui/dist/assets/ChannelsPanel-CiJs4gyw.js +0 -1
  241. package/ui/dist/assets/CheckpointsPanel-DvKUd2VF.js +0 -1
  242. package/ui/dist/assets/CommandPostHub-CQqRYBjf.js +0 -24
  243. package/ui/dist/assets/CronPanel-Brmnnu8_.js +0 -1
  244. package/ui/dist/assets/DreamPanel-CO7iSdjn.js +0 -15
  245. package/ui/dist/assets/EvalPanel-CqAwhFQ5.js +0 -1
  246. package/ui/dist/assets/FleetPanel-Bg-f3_ip.js +0 -1
  247. package/ui/dist/assets/InfraView-CHYaZqMK.js +0 -2
  248. package/ui/dist/assets/InlineEditableField-BOLRKQq1.js +0 -1
  249. package/ui/dist/assets/Input-4pNffAw-.js +0 -1
  250. package/ui/dist/assets/IntegrationsPanel-CFucvr-Q.js +0 -1
  251. package/ui/dist/assets/IntelligenceView-Dh-Wuf13.js +0 -2
  252. package/ui/dist/assets/LearningPanel-CtlougkF.js +0 -1
  253. package/ui/dist/assets/LogsPanel-Bs9IsTCH.js +0 -1
  254. package/ui/dist/assets/McpPanel-BobqpLsh.js +0 -1
  255. package/ui/dist/assets/MemoryWikiPanel-BfYksjoI.js +0 -11
  256. package/ui/dist/assets/MissionCanvas-0V_vtqiu.js +0 -50
  257. package/ui/dist/assets/MissionChat-9hGhc9Zw.js +0 -7
  258. package/ui/dist/assets/MissionLibrary-dFkT8pGp.js +0 -5
  259. package/ui/dist/assets/MissionStart-CZSEie0Q.js +0 -1
  260. package/ui/dist/assets/Modal-V2yNnkKk.js +0 -1
  261. package/ui/dist/assets/NvidiaPanel-qLJysI9M.js +0 -1
  262. package/ui/dist/assets/OrganismPanel-CNZM3eof.js +0 -1
  263. package/ui/dist/assets/OverviewPanel-Be6LukJ_.js +0 -1
  264. package/ui/dist/assets/RecipesPanel-DDYOCx3w.js +0 -1
  265. package/ui/dist/assets/SelfImprovePanel-B8o_fO30.js +0 -1
  266. package/ui/dist/assets/SessionsPanel-DeCBkSgY.js +0 -1
  267. package/ui/dist/assets/SettingsPanel-BlsRDAt7.js +0 -1
  268. package/ui/dist/assets/SettingsView-Dsa7Zr4e.js +0 -2
  269. package/ui/dist/assets/SkeletonLoader-BkQoZllJ.js +0 -1
  270. package/ui/dist/assets/SomaView-CYukE7Fp.js +0 -5
  271. package/ui/dist/assets/StatusBadge-BFEHCdgi.js +0 -1
  272. package/ui/dist/assets/Tabs-g2Gs9sK0.js +0 -1
  273. package/ui/dist/assets/TeamsPanel-DOBe72TK.js +0 -1
  274. package/ui/dist/assets/TelemetryPanel-DXAGuV0R.js +0 -1
  275. package/ui/dist/assets/TitanCanvas-DCl3gxSl.js +0 -1072
  276. package/ui/dist/assets/ToolsView-CnUjq0Js.js +0 -2
  277. package/ui/dist/assets/Tooltip-DJ99YlFF.js +0 -6
  278. package/ui/dist/assets/TraceViewer-0bCz0x_a.js +0 -1
  279. package/ui/dist/assets/TrainingPanel-BLf_KMaE.js +0 -1
  280. package/ui/dist/assets/VoiceOverlay-B08Sw918.js +0 -27
  281. package/ui/dist/assets/VramPanel-CJiic8y4.js +0 -1
  282. package/ui/dist/assets/WorkTab-D5bi2WRi.js +0 -1
  283. package/ui/dist/assets/briefcase-ROIEmiah.js +0 -6
  284. package/ui/dist/assets/chevron-down-CM11GPpR.js +0 -6
  285. package/ui/dist/assets/eye-off-CYR4nH5I.js +0 -6
  286. package/ui/dist/assets/folder-DY4eeMdo.js +0 -11
  287. package/ui/dist/assets/index-PwwKjgT3.css +0 -1
  288. package/ui/dist/assets/index-slWylU2o.js +0 -244
  289. package/ui/dist/assets/loader-circle-E8aLxmMz.js +0 -6
  290. package/ui/dist/assets/missions-DbC8UmMc.js +0 -1
  291. package/ui/dist/assets/target-CWfn705s.js +0 -6
  292. package/ui/dist/assets/terminal-CdhfA0Kr.js +0 -16
  293. package/ui/dist/assets/users-BX_ZaByZ.js +0 -16
  294. package/ui/dist/assets/wrench-Cfp7nYhG.js +0 -11
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Tony Elliott (https://github.com/Djtony707)
3
+ Copyright (c) 2026 Tony Elliott
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -19,31 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
-
23
- ---
24
-
25
- ACKNOWLEDGMENTS
26
-
27
- TITAN's architecture is inspired by OpenClaw (https://github.com/openclaw/openclaw),
28
- an open-source personal AI assistant framework, licensed under the MIT License.
29
- Thank you to the OpenClaw team and all open-source contributors.
30
-
31
- Voice interface adapted from LiveKit Agent Starter React
32
- (https://github.com/livekit-examples/agent-starter-react).
33
- Copyright (c) 2025 LiveKit, Inc. Licensed under the MIT License.
34
- Thank you to the LiveKit team for their excellent real-time WebRTC platform.
35
-
36
- Mission Control v2 frontend built with React 19 (MIT), Vite (MIT),
37
- Tailwind CSS 4 (MIT), React Router v7 (MIT), react-markdown (MIT),
38
- Lucide React (ISC), and Motion (MIT).
39
-
40
- Voice infrastructure powered by faster-whisper (MIT), Kokoro FastAPI (MIT),
41
- Caddy (Apache-2.0), and the LiveKit Agents SDK (Apache-2.0).
42
-
43
- Temporal knowledge graph inspired by Graphiti (https://github.com/getzep/graphiti)
44
- by Zep AI, licensed under Apache-2.0.
45
-
46
- Browser automation patterns inspired by Skyvern (https://github.com/Skyvern-AI/skyvern)
47
- by Skyvern AI, licensed under AGPL-3.0 (separate service, not bundled).
48
-
49
- For the complete list of third-party dependencies, see THIRD_PARTY_NOTICES.md.
package/README.md CHANGED
@@ -46,7 +46,7 @@
46
46
 
47
47
  ## 🪵 NEW in v6.1 — Mission Chat + Desk view
48
48
 
49
- > Status: `v6.1.0-beta.3` — live on npm at `@latest` and `@beta`. Alpha series concluded with `6.1.0-alpha.57`; the schema promoted to beta once features stabilized.
49
+ > Status: `v6.1.0-beta.7` — live on npm at `@latest` and `@beta`. Alpha series concluded with `6.1.0-alpha.57`; the schema promoted to beta once features stabilized.
50
50
  > Install with `npm i -g titan-agent` (or `npm update -g titan-agent`).
51
51
  > The v6.0 "Presence" feature set below still applies — v6.1.0 layers
52
52
  > a beautiful new surface on top of it.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from "fs";
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, unlinkSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { homedir } from "os";
5
5
  import logger from "../utils/logger.js";
@@ -137,7 +137,7 @@ function __resetLessonsForTests(agentId) {
137
137
  const path = lessonsPath(agentId);
138
138
  if (existsSync(path)) {
139
139
  try {
140
- require("fs").unlinkSync(path);
140
+ unlinkSync(path);
141
141
  } catch {
142
142
  }
143
143
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/agent/agentLessons.ts"],"sourcesContent":["/**\n * TITAN — Per-agent lessons.md (Reflexion-style failure memory).\n *\n * Why this exists\n * ---------------\n * The audit of TITAN's Command Post (2026-05-11) found that retrospectives\n * were computed in-memory per goal but never persisted as a per-agent\n * lookup. That meant the next time the same agent took on similar work,\n * it walked into the same failure modes blind.\n *\n * This module is the persistence layer for the Reflexion pattern as\n * documented in awesome-agent-harness pattern #6:\n *\n * \"When a step fails, a separate critic LLM writes a 1-3 sentence\n * verbal lesson ('I forgot to check the schema before inserting')\n * into lessons.md. Future planner turns read this file first.\"\n *\n * One file per agent at `~/.titan/agents/<agentId>/lessons.md`. Append-only\n * + dedup-on-content. The top-N most recent / highest-relevance entries\n * are injected into that agent's system prompt at the start of every run\n * via {@link renderAgentLessonsForPrompt}.\n *\n * Reference:\n * - Shinn et al. \"Reflexion: Language agents with verbal reinforcement\n * learning\" (NeurIPS 2023) — https://arxiv.org/abs/2303.11366\n * - Picrew/awesome-agent-harness pattern #6\n * - ~/.claude/projects/-Users-michaelelliott/memory/titan-v6-living-canvas.md\n */\nimport { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport logger from '../utils/logger.js';\nimport { recordJournalEvent } from './durableJournal.js';\n\nconst COMPONENT = 'AgentLessons';\n\n/** Lazy path resolution so tests can swap HOME / TITAN_HOME. */\nfunction titanHome(): string {\n const env = process.env.TITAN_HOME;\n if (env) return env.startsWith('~/') ? join(homedir(), env.slice(2)) : env;\n return join(homedir(), '.titan');\n}\nfunction lessonsPath(agentId: string): string {\n return join(titanHome(), 'agents', agentId, 'lessons.md');\n}\n\n/** A single lesson the agent has learned from a past failure or success. */\nexport interface AgentLesson {\n /** Short, verbal — the lesson the agent took away (1–3 sentences). */\n text: string;\n /** \"fail\" lessons come from things that went wrong; \"win\" lessons from\n * things that worked surprisingly well. Both are useful. Default fail. */\n kind?: 'fail' | 'win';\n /** Free-form context tag — what kind of task this was about (e.g.\n * \"merge-conflict\", \"json-extract\", \"vram-acquire\"). Used for fuzzy\n * lookup later. Optional. */\n topic?: string;\n /** Optional sessionId for traceback. */\n sessionId?: string;\n /** Optional goalId — when set, the lesson is also written into the\n * goal's durable journal so a resuming process can see it. */\n goalId?: string;\n}\n\n/** Cap on lesson count per agent. Older entries roll off the head. */\nconst LESSONS_PER_AGENT_CAP = 200;\n/** Cap on lessons rendered into the system prompt at run time. */\nconst PROMPT_RENDER_CAP = 12;\n\n/**\n * Append a lesson to the agent's lessons.md. Idempotent on near-duplicate\n * text (lowercased, whitespace-collapsed match) so the same lesson written\n * twice in close succession doesn't pollute the file.\n */\nexport function recordLesson(agentId: string, lesson: AgentLesson): void {\n if (!agentId || !lesson?.text || !lesson.text.trim()) return;\n try {\n const path = lessonsPath(agentId);\n try { mkdirSync(join(path, '..'), { recursive: true }); } catch { /* exists */ }\n\n const norm = (s: string) => s.toLowerCase().replace(/\\s+/g, ' ').trim();\n const incomingNorm = norm(lesson.text);\n\n // Read existing entries to dedup against recent ones.\n const existingRaw = existsSync(path) ? readFileSync(path, 'utf-8') : '';\n const recentNorms = parseLessonTexts(existingRaw)\n .slice(-20) // only dedup against last 20 — older content fades\n .map(norm);\n if (recentNorms.includes(incomingNorm)) {\n return; // already learned this lesson recently\n }\n\n // Append one entry in the documented format.\n const ts = new Date().toISOString();\n const kind = lesson.kind ?? 'fail';\n const topic = lesson.topic ? ` topic=\"${escapeAttr(lesson.topic)}\"` : '';\n const session = lesson.sessionId ? ` session=\"${escapeAttr(lesson.sessionId)}\"` : '';\n const block = `\\n- [${ts}] **${kind}**${topic}${session} — ${lesson.text.trim()}\\n`;\n\n // Make sure the file has a header on first write.\n if (!existsSync(path)) {\n writeFileSync(path, lessonsHeader(agentId), 'utf-8');\n }\n appendFileSync(path, block, 'utf-8');\n\n // Trim to cap if exceeded.\n trimIfOverCap(path);\n\n logger.info(COMPONENT, `Recorded ${kind}-lesson for ${agentId}${topic} — \"${lesson.text.slice(0, 80)}\"`);\n\n // v6.0 CP upgrade #4 — durable journal: surface lessons in the\n // per-context replay so resuming agents can see what was learned.\n recordJournalEvent(\n { agentId, goalId: lesson.goalId, sessionId: lesson.sessionId },\n 'lesson_recorded',\n { text: lesson.text.trim(), kind, topic: lesson.topic },\n );\n } catch (err) {\n logger.warn(COMPONENT, `Failed to write lesson for ${agentId}: ${(err as Error).message}`);\n }\n}\n\nfunction lessonsHeader(agentId: string): string {\n return [\n `# Lessons — ${agentId}`,\n ``,\n `Append-only verbal-reinforcement journal. Each entry is one lesson the`,\n `agent learned from a past attempt (failure or surprising win). The top`,\n `~${PROMPT_RENDER_CAP} most recent entries are injected into this agent's system`,\n `prompt at the start of every run.`,\n ``,\n `Format: \\`- [iso-ts] **kind** [topic=\"…\"] [session=\"…\"] — verbal lesson\\``,\n ``,\n `---`,\n ``,\n ].join('\\n');\n}\n\nfunction escapeAttr(s: string): string {\n return s.replace(/[\\\\\"\\n\\r]/g, c => c === '\\n' ? ' ' : c === '\\r' ? ' ' : '\\\\' + c).slice(0, 80);\n}\n\n/** Parse the lesson lines from the raw markdown. Returns text content only. */\nfunction parseLessonTexts(raw: string): string[] {\n const lines = raw.split(/\\r?\\n/);\n const out: string[] = [];\n for (const l of lines) {\n // Match the documented format. Capture everything after the trailing \" — \".\n const m = l.match(/^- \\[[^\\]]+\\]\\s+\\*\\*(fail|win)\\*\\*(?:[^—]*)—\\s+(.+)$/);\n if (m && m[2]) out.push(m[2]);\n }\n return out;\n}\n\nfunction trimIfOverCap(path: string): void {\n try {\n const raw = readFileSync(path, 'utf-8');\n const lines = raw.split(/\\r?\\n/);\n // Find the entries (lines starting with \"- [\")\n const entryIdx: number[] = [];\n for (let i = 0; i < lines.length; i++) {\n if (lines[i].startsWith('- [')) entryIdx.push(i);\n }\n if (entryIdx.length <= LESSONS_PER_AGENT_CAP) return;\n // Drop the oldest (LESSONS_PER_AGENT_CAP / 4) when we tip over.\n const dropCount = Math.max(1, Math.floor(LESSONS_PER_AGENT_CAP / 4));\n const cutAt = entryIdx[dropCount] ?? entryIdx[0];\n // Header ends at first \"---\" line.\n const headerEnd = lines.findIndex(l => l.trim() === '---');\n const header = headerEnd >= 0 ? lines.slice(0, headerEnd + 2).join('\\n') : '';\n const kept = lines.slice(cutAt).join('\\n');\n writeFileSync(path, header + '\\n' + kept, 'utf-8');\n logger.info(COMPONENT, `Trimmed ${dropCount} oldest lessons from ${path}`);\n } catch { /* best-effort */ }\n}\n\n/**\n * Render the most relevant lessons for `agentId` as a prompt-ready block.\n * If `topic` is supplied, lessons matching that topic float to the top.\n * Returns empty string when there are no lessons yet (don't add a\n * prompt section with no content).\n */\nexport function renderAgentLessonsForPrompt(agentId: string, topic?: string): string {\n if (!agentId) return '';\n const path = lessonsPath(agentId);\n if (!existsSync(path)) return '';\n try {\n const raw = readFileSync(path, 'utf-8');\n const lines = raw.split(/\\r?\\n/);\n // Parse structured entries: kind + topic + text.\n const entries: Array<{ kind: string; topic: string; text: string; ts: string }> = [];\n for (const l of lines) {\n const m = l.match(/^- \\[([^\\]]+)\\]\\s+\\*\\*(fail|win)\\*\\*(?:\\s+topic=\"([^\"]+)\")?(?:[^—]*)—\\s+(.+)$/);\n if (m) entries.push({ ts: m[1], kind: m[2], topic: m[3] || '', text: m[4] });\n }\n if (entries.length === 0) return '';\n\n // Sort: most recent first, with topic match boost.\n const wantsTopic = (topic || '').toLowerCase();\n const scored = entries.map(e => ({\n e,\n score: (wantsTopic && e.topic.toLowerCase() === wantsTopic ? 1 : 0)\n + (wantsTopic && e.topic.toLowerCase().includes(wantsTopic) ? 0.5 : 0),\n }));\n scored.sort((a, b) => b.score - a.score || b.e.ts.localeCompare(a.e.ts));\n\n const top = scored.slice(0, PROMPT_RENDER_CAP).map(s => s.e);\n if (top.length === 0) return '';\n\n const out: string[] = [`## Lessons you've learned (top ${top.length})`];\n for (const e of top) {\n const tag = e.topic ? ` *(${e.topic})*` : '';\n out.push(`- **${e.kind}**${tag} — ${e.text}`);\n }\n return out.join('\\n');\n } catch {\n return '';\n }\n}\n\n/** Convenience helper — read raw lessons.md content (used by tests + UI). */\nexport function readLessonsMarkdown(agentId: string): string {\n const path = lessonsPath(agentId);\n return existsSync(path) ? readFileSync(path, 'utf-8') : '';\n}\n\n/** Test helper — wipe an agent's lessons file. */\nexport function __resetLessonsForTests(agentId: string): void {\n const path = lessonsPath(agentId);\n if (existsSync(path)) {\n try { require('fs').unlinkSync(path); } catch { /* fine */ }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/agent/agentLessons.ts"],"sourcesContent":["/**\n * TITAN — Per-agent lessons.md (Reflexion-style failure memory).\n *\n * Why this exists\n * ---------------\n * The audit of TITAN's Command Post (2026-05-11) found that retrospectives\n * were computed in-memory per goal but never persisted as a per-agent\n * lookup. That meant the next time the same agent took on similar work,\n * it walked into the same failure modes blind.\n *\n * This module is the persistence layer for the Reflexion pattern as\n * documented in awesome-agent-harness pattern #6:\n *\n * \"When a step fails, a separate critic LLM writes a 1-3 sentence\n * verbal lesson ('I forgot to check the schema before inserting')\n * into lessons.md. Future planner turns read this file first.\"\n *\n * One file per agent at `~/.titan/agents/<agentId>/lessons.md`. Append-only\n * + dedup-on-content. The top-N most recent / highest-relevance entries\n * are injected into that agent's system prompt at the start of every run\n * via {@link renderAgentLessonsForPrompt}.\n *\n * Reference:\n * - Shinn et al. \"Reflexion: Language agents with verbal reinforcement\n * learning\" (NeurIPS 2023) — https://arxiv.org/abs/2303.11366\n * - Picrew/awesome-agent-harness pattern #6\n * - ~/.claude/projects/-Users-michaelelliott/memory/titan-v6-living-canvas.md\n */\nimport { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, unlinkSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport logger from '../utils/logger.js';\nimport { recordJournalEvent } from './durableJournal.js';\n\nconst COMPONENT = 'AgentLessons';\n\n/** Lazy path resolution so tests can swap HOME / TITAN_HOME. */\nfunction titanHome(): string {\n const env = process.env.TITAN_HOME;\n if (env) return env.startsWith('~/') ? join(homedir(), env.slice(2)) : env;\n return join(homedir(), '.titan');\n}\nfunction lessonsPath(agentId: string): string {\n return join(titanHome(), 'agents', agentId, 'lessons.md');\n}\n\n/** A single lesson the agent has learned from a past failure or success. */\nexport interface AgentLesson {\n /** Short, verbal — the lesson the agent took away (1–3 sentences). */\n text: string;\n /** \"fail\" lessons come from things that went wrong; \"win\" lessons from\n * things that worked surprisingly well. Both are useful. Default fail. */\n kind?: 'fail' | 'win';\n /** Free-form context tag — what kind of task this was about (e.g.\n * \"merge-conflict\", \"json-extract\", \"vram-acquire\"). Used for fuzzy\n * lookup later. Optional. */\n topic?: string;\n /** Optional sessionId for traceback. */\n sessionId?: string;\n /** Optional goalId — when set, the lesson is also written into the\n * goal's durable journal so a resuming process can see it. */\n goalId?: string;\n}\n\n/** Cap on lesson count per agent. Older entries roll off the head. */\nconst LESSONS_PER_AGENT_CAP = 200;\n/** Cap on lessons rendered into the system prompt at run time. */\nconst PROMPT_RENDER_CAP = 12;\n\n/**\n * Append a lesson to the agent's lessons.md. Idempotent on near-duplicate\n * text (lowercased, whitespace-collapsed match) so the same lesson written\n * twice in close succession doesn't pollute the file.\n */\nexport function recordLesson(agentId: string, lesson: AgentLesson): void {\n if (!agentId || !lesson?.text || !lesson.text.trim()) return;\n try {\n const path = lessonsPath(agentId);\n try { mkdirSync(join(path, '..'), { recursive: true }); } catch { /* exists */ }\n\n const norm = (s: string) => s.toLowerCase().replace(/\\s+/g, ' ').trim();\n const incomingNorm = norm(lesson.text);\n\n // Read existing entries to dedup against recent ones.\n const existingRaw = existsSync(path) ? readFileSync(path, 'utf-8') : '';\n const recentNorms = parseLessonTexts(existingRaw)\n .slice(-20) // only dedup against last 20 — older content fades\n .map(norm);\n if (recentNorms.includes(incomingNorm)) {\n return; // already learned this lesson recently\n }\n\n // Append one entry in the documented format.\n const ts = new Date().toISOString();\n const kind = lesson.kind ?? 'fail';\n const topic = lesson.topic ? ` topic=\"${escapeAttr(lesson.topic)}\"` : '';\n const session = lesson.sessionId ? ` session=\"${escapeAttr(lesson.sessionId)}\"` : '';\n const block = `\\n- [${ts}] **${kind}**${topic}${session} — ${lesson.text.trim()}\\n`;\n\n // Make sure the file has a header on first write.\n if (!existsSync(path)) {\n writeFileSync(path, lessonsHeader(agentId), 'utf-8');\n }\n appendFileSync(path, block, 'utf-8');\n\n // Trim to cap if exceeded.\n trimIfOverCap(path);\n\n logger.info(COMPONENT, `Recorded ${kind}-lesson for ${agentId}${topic} — \"${lesson.text.slice(0, 80)}\"`);\n\n // v6.0 CP upgrade #4 — durable journal: surface lessons in the\n // per-context replay so resuming agents can see what was learned.\n recordJournalEvent(\n { agentId, goalId: lesson.goalId, sessionId: lesson.sessionId },\n 'lesson_recorded',\n { text: lesson.text.trim(), kind, topic: lesson.topic },\n );\n } catch (err) {\n logger.warn(COMPONENT, `Failed to write lesson for ${agentId}: ${(err as Error).message}`);\n }\n}\n\nfunction lessonsHeader(agentId: string): string {\n return [\n `# Lessons — ${agentId}`,\n ``,\n `Append-only verbal-reinforcement journal. Each entry is one lesson the`,\n `agent learned from a past attempt (failure or surprising win). The top`,\n `~${PROMPT_RENDER_CAP} most recent entries are injected into this agent's system`,\n `prompt at the start of every run.`,\n ``,\n `Format: \\`- [iso-ts] **kind** [topic=\"…\"] [session=\"…\"] — verbal lesson\\``,\n ``,\n `---`,\n ``,\n ].join('\\n');\n}\n\nfunction escapeAttr(s: string): string {\n return s.replace(/[\\\\\"\\n\\r]/g, c => c === '\\n' ? ' ' : c === '\\r' ? ' ' : '\\\\' + c).slice(0, 80);\n}\n\n/** Parse the lesson lines from the raw markdown. Returns text content only. */\nfunction parseLessonTexts(raw: string): string[] {\n const lines = raw.split(/\\r?\\n/);\n const out: string[] = [];\n for (const l of lines) {\n // Match the documented format. Capture everything after the trailing \" — \".\n const m = l.match(/^- \\[[^\\]]+\\]\\s+\\*\\*(fail|win)\\*\\*(?:[^—]*)—\\s+(.+)$/);\n if (m && m[2]) out.push(m[2]);\n }\n return out;\n}\n\nfunction trimIfOverCap(path: string): void {\n try {\n const raw = readFileSync(path, 'utf-8');\n const lines = raw.split(/\\r?\\n/);\n // Find the entries (lines starting with \"- [\")\n const entryIdx: number[] = [];\n for (let i = 0; i < lines.length; i++) {\n if (lines[i].startsWith('- [')) entryIdx.push(i);\n }\n if (entryIdx.length <= LESSONS_PER_AGENT_CAP) return;\n // Drop the oldest (LESSONS_PER_AGENT_CAP / 4) when we tip over.\n const dropCount = Math.max(1, Math.floor(LESSONS_PER_AGENT_CAP / 4));\n const cutAt = entryIdx[dropCount] ?? entryIdx[0];\n // Header ends at first \"---\" line.\n const headerEnd = lines.findIndex(l => l.trim() === '---');\n const header = headerEnd >= 0 ? lines.slice(0, headerEnd + 2).join('\\n') : '';\n const kept = lines.slice(cutAt).join('\\n');\n writeFileSync(path, header + '\\n' + kept, 'utf-8');\n logger.info(COMPONENT, `Trimmed ${dropCount} oldest lessons from ${path}`);\n } catch { /* best-effort */ }\n}\n\n/**\n * Render the most relevant lessons for `agentId` as a prompt-ready block.\n * If `topic` is supplied, lessons matching that topic float to the top.\n * Returns empty string when there are no lessons yet (don't add a\n * prompt section with no content).\n */\nexport function renderAgentLessonsForPrompt(agentId: string, topic?: string): string {\n if (!agentId) return '';\n const path = lessonsPath(agentId);\n if (!existsSync(path)) return '';\n try {\n const raw = readFileSync(path, 'utf-8');\n const lines = raw.split(/\\r?\\n/);\n // Parse structured entries: kind + topic + text.\n const entries: Array<{ kind: string; topic: string; text: string; ts: string }> = [];\n for (const l of lines) {\n const m = l.match(/^- \\[([^\\]]+)\\]\\s+\\*\\*(fail|win)\\*\\*(?:\\s+topic=\"([^\"]+)\")?(?:[^—]*)—\\s+(.+)$/);\n if (m) entries.push({ ts: m[1], kind: m[2], topic: m[3] || '', text: m[4] });\n }\n if (entries.length === 0) return '';\n\n // Sort: most recent first, with topic match boost.\n const wantsTopic = (topic || '').toLowerCase();\n const scored = entries.map(e => ({\n e,\n score: (wantsTopic && e.topic.toLowerCase() === wantsTopic ? 1 : 0)\n + (wantsTopic && e.topic.toLowerCase().includes(wantsTopic) ? 0.5 : 0),\n }));\n scored.sort((a, b) => b.score - a.score || b.e.ts.localeCompare(a.e.ts));\n\n const top = scored.slice(0, PROMPT_RENDER_CAP).map(s => s.e);\n if (top.length === 0) return '';\n\n const out: string[] = [`## Lessons you've learned (top ${top.length})`];\n for (const e of top) {\n const tag = e.topic ? ` *(${e.topic})*` : '';\n out.push(`- **${e.kind}**${tag} — ${e.text}`);\n }\n return out.join('\\n');\n } catch {\n return '';\n }\n}\n\n/** Convenience helper — read raw lessons.md content (used by tests + UI). */\nexport function readLessonsMarkdown(agentId: string): string {\n const path = lessonsPath(agentId);\n return existsSync(path) ? readFileSync(path, 'utf-8') : '';\n}\n\n/** Test helper — wipe an agent's lessons file. */\nexport function __resetLessonsForTests(agentId: string): void {\n const path = lessonsPath(agentId);\n if (existsSync(path)) {\n try { unlinkSync(path); } catch { /* fine */ }\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ import { getToolContract } from "./toolContract.js";
3
+ import { loadConfig } from "../config/config.js";
4
+ import logger from "../utils/logger.js";
5
+ const COMPONENT = "autoMode";
6
+ function getAutoModePolicy() {
7
+ const envPolicy = process.env.TITAN_AUTOMODE_POLICY;
8
+ if (envPolicy === "paranoid" || envPolicy === "permissive" || envPolicy === "yolo" || envPolicy === "standard") {
9
+ return envPolicy;
10
+ }
11
+ try {
12
+ const cfg = loadConfig();
13
+ const sec = cfg.security;
14
+ const am = sec?.autoMode;
15
+ const policy = am?.policy;
16
+ if (policy === "paranoid" || policy === "permissive" || policy === "yolo" || policy === "standard") {
17
+ return policy;
18
+ }
19
+ } catch {
20
+ }
21
+ return "standard";
22
+ }
23
+ function classifyToolCall(toolName) {
24
+ const policy = getAutoModePolicy();
25
+ const contract = getToolContract(toolName);
26
+ const riskLevel = contract?.riskLevel;
27
+ if (policy === "yolo") {
28
+ warnYoloOnce();
29
+ return {
30
+ decision: "auto",
31
+ reason: "yolo policy \u2014 gating disabled",
32
+ policy,
33
+ riskLevel
34
+ };
35
+ }
36
+ if (policy === "paranoid") {
37
+ return {
38
+ decision: "gate",
39
+ reason: "paranoid policy \u2014 every tool call requires approval",
40
+ policy,
41
+ riskLevel
42
+ };
43
+ }
44
+ if (!contract) {
45
+ return {
46
+ decision: "gate",
47
+ reason: `no contract registered for "${toolName}" \u2014 gating by default`,
48
+ policy
49
+ };
50
+ }
51
+ if (riskLevel === "safe") {
52
+ return { decision: "auto", reason: "contract riskLevel=safe", policy, riskLevel };
53
+ }
54
+ if (riskLevel === "moderate") {
55
+ if (policy === "permissive") {
56
+ return { decision: "auto", reason: "permissive policy \u2014 moderate risk auto-runs", policy, riskLevel };
57
+ }
58
+ return { decision: "notify", reason: "contract riskLevel=moderate", policy, riskLevel };
59
+ }
60
+ if (riskLevel === "high") {
61
+ return { decision: "gate", reason: "contract riskLevel=high", policy, riskLevel };
62
+ }
63
+ return {
64
+ decision: "gate",
65
+ reason: `unrecognised riskLevel "${riskLevel}" \u2014 gating defensively`,
66
+ policy,
67
+ riskLevel
68
+ };
69
+ }
70
+ let _yoloWarned = false;
71
+ function warnYoloOnce() {
72
+ if (_yoloWarned) return;
73
+ _yoloWarned = true;
74
+ logger.warn(
75
+ COMPONENT,
76
+ "AUTO_MODE=yolo \u2014 every tool call will run without approval. This mode is ONLY intended for sandboxed eval runs. If you see this in production logs, something is misconfigured."
77
+ );
78
+ }
79
+ function _resetYoloWarning() {
80
+ _yoloWarned = false;
81
+ }
82
+ function shouldGate(result) {
83
+ return result.decision === "gate";
84
+ }
85
+ function formatBreadcrumb(toolName, result) {
86
+ return `automode:${result.decision} tool=${toolName} risk=${result.riskLevel ?? "unknown"} policy=${result.policy} (${result.reason})`;
87
+ }
88
+ export {
89
+ _resetYoloWarning,
90
+ classifyToolCall,
91
+ formatBreadcrumb,
92
+ getAutoModePolicy,
93
+ shouldGate
94
+ };
95
+ //# sourceMappingURL=autoModeClassifier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/agent/autoModeClassifier.ts"],"sourcesContent":["/**\n * TITAN — Auto-Mode Classifier (v6.1.0-beta.16, Phase D.4)\n *\n * Reads the tool contract registry to decide whether an incoming\n * tool call can run autonomously, deserves a passive notification,\n * or requires explicit human approval. Pattern from Anthropic's\n * \"Claude Code auto-mode\" blog (in the Picrew/awesome-agent-harness\n * featured-reading list): classifier-backed approval delegation\n * gives the agent autonomy on safe paths while keeping a kill\n * switch on the risky ones.\n *\n * THREE DECISIONS:\n *\n * - 'auto' — run without prompting. Read-only ops + workspace\n * writes. Most missions run mostly 'auto' calls.\n * - 'notify' — run, but record a passive notification so a\n * watcher (Mission Canvas, log tail) can see it\n * happened. Default for network calls.\n * - 'gate' — require explicit human approval before running.\n * Default for destructive ops (shell, delete, etc.)\n * AND for any tool without a registered contract\n * (unknown → conservative).\n *\n * POLICY OVERRIDES (config.security.autoMode.policy):\n *\n * - 'standard' (default) — riskLevel drives the decision per\n * the table above.\n * - 'paranoid' — every tool call is 'gate'. For\n * demos, regulated environments, or\n * when Tony is debugging a misbehaving\n * agent.\n * - 'permissive' — riskLevel 'moderate' is auto-run\n * (still 'gate' on 'high'). For\n * trusted hot loops where ping-the-user\n * on every web_search is annoying.\n * - 'yolo' — everything auto, including 'high'.\n * ONLY for sandboxed eval runs. Logs\n * a loud warning on init.\n *\n * WHY this module exists:\n *\n * - Phase D.3 added riskLevel + sideEffects metadata to the 9\n * canonical contracts. D.4 makes the metadata actually do\n * something — gating decisions, not just labels.\n *\n * - The existing approval-gate path in toolRunner uses a hardcoded\n * \"is this tool name dangerous?\" list. That's a string-match\n * allowlist that doesn't scale and doesn't account for HOW the\n * tool is being used. The classifier is the structural answer:\n * contracts declare risk; the classifier reads contracts.\n *\n * - Decoupling the policy from the runner means changing the\n * gating semantics (e.g. add a new 'risky-during-business-hours'\n * decision) is a one-file change — never a wholesale runner\n * refactor.\n *\n * TRADE-OFFS:\n *\n * - First cut keys decisions on contract.riskLevel alone. A\n * follow-up could consider call ARGS (e.g. shell with\n * `command: 'ls'` could be 'auto' while shell with\n * `command: 'rm -rf'` is still 'gate'). Skipped for now to\n * keep the surface small + predictable.\n *\n * - Tools without a registered contract default to 'gate'. This\n * is conservative but means the long-tail 239 skills will hit\n * the approval gate on every call until their contracts ship.\n * If that becomes painful, lift the default to 'notify' via\n * the `unknownToolDefault` config field.\n *\n * FOLLOW-UP:\n *\n * - Phase D.5 reads sideEffects==='destructive' to route the\n * call into a worktree-isolated executor.\n * - Args-aware classification (shell `rm -rf` vs shell `ls`).\n * - Per-session policy override so a \"draft a quick email\"\n * mission can opt into 'permissive' while a \"review and merge\n * this PR\" mission stays on 'paranoid'.\n */\n\nimport { getToolContract, type RiskLevel } from './toolContract.js';\nimport { loadConfig } from '../config/config.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'autoMode';\n\n/* ────────────────────────── Types ────────────────────────── */\n\nexport type AutoModeDecision = 'auto' | 'notify' | 'gate';\nexport type AutoModePolicy = 'standard' | 'paranoid' | 'permissive' | 'yolo';\n\nexport interface ClassificationResult {\n /** What the runner should do with the call. */\n decision: AutoModeDecision;\n /** Plain-english reason — logged and surfaced in the trajectory. */\n reason: string;\n /** Policy that produced this decision (drives observability). */\n policy: AutoModePolicy;\n /** Risk level from the contract, if any. Undefined for unknown tools. */\n riskLevel?: RiskLevel;\n}\n\n/* ────────────────────────── Policy loader ────────────────────────── */\n\n/**\n * Resolve the current auto-mode policy. Reads\n * `config.security.autoMode.policy` if present; defaults to 'standard'.\n * Pure synchronous lookup — no IO, safe to call per-tool-call.\n */\nexport function getAutoModePolicy(): AutoModePolicy {\n // beta.21 — env override. Lets the test suite (and any embedder\n // that wants a process-wide policy without touching config files)\n // short-circuit the classifier without mutating loaded config.\n // Production callers should configure via `config.security.autoMode.policy`\n // — this env hook is opt-in and silent when unset.\n const envPolicy = process.env.TITAN_AUTOMODE_POLICY;\n if (envPolicy === 'paranoid' || envPolicy === 'permissive' || envPolicy === 'yolo' || envPolicy === 'standard') {\n return envPolicy;\n }\n try {\n const cfg = loadConfig();\n const sec = (cfg as Record<string, unknown>).security as Record<string, unknown> | undefined;\n const am = sec?.autoMode as Record<string, unknown> | undefined;\n const policy = am?.policy as string | undefined;\n if (policy === 'paranoid' || policy === 'permissive' || policy === 'yolo' || policy === 'standard') {\n return policy;\n }\n } catch { /* config unavailable — fall through */ }\n return 'standard';\n}\n\n/* ────────────────────────── Classifier ────────────────────────── */\n\n/**\n * Decide whether a tool call should run automatically, with a\n * notification, or with explicit approval. The runner uses this to\n * decide whether to short-circuit the existing approval-gate path.\n *\n * Pure: no IO beyond reading the config + contract registry. Cheap\n * enough to call on every tool call without measuring.\n */\nexport function classifyToolCall(toolName: string): ClassificationResult {\n const policy = getAutoModePolicy();\n const contract = getToolContract(toolName);\n const riskLevel = contract?.riskLevel;\n\n // yolo short-circuit: regardless of risk, run. ONLY for sandboxed\n // eval runs — logs a loud warning the first time per process.\n if (policy === 'yolo') {\n warnYoloOnce();\n return {\n decision: 'auto',\n reason: 'yolo policy — gating disabled',\n policy,\n riskLevel,\n };\n }\n\n // paranoid short-circuit: every call gated.\n if (policy === 'paranoid') {\n return {\n decision: 'gate',\n reason: 'paranoid policy — every tool call requires approval',\n policy,\n riskLevel,\n };\n }\n\n // Unknown tool (no registered contract) → conservative 'gate'.\n // The long-tail 239 skills without contracts hit this branch\n // until their contracts ship. Override via config if the prompt\n // pings become annoying.\n if (!contract) {\n return {\n decision: 'gate',\n reason: `no contract registered for \"${toolName}\" — gating by default`,\n policy,\n };\n }\n\n // Standard + permissive paths read riskLevel.\n if (riskLevel === 'safe') {\n return { decision: 'auto', reason: 'contract riskLevel=safe', policy, riskLevel };\n }\n if (riskLevel === 'moderate') {\n if (policy === 'permissive') {\n return { decision: 'auto', reason: 'permissive policy — moderate risk auto-runs', policy, riskLevel };\n }\n return { decision: 'notify', reason: 'contract riskLevel=moderate', policy, riskLevel };\n }\n if (riskLevel === 'high') {\n return { decision: 'gate', reason: 'contract riskLevel=high', policy, riskLevel };\n }\n\n // Defensive fallback for an unknown future riskLevel value.\n return {\n decision: 'gate',\n reason: `unrecognised riskLevel \"${riskLevel}\" — gating defensively`,\n policy,\n riskLevel,\n };\n}\n\n/* ────────────────────────── Helpers ────────────────────────── */\n\nlet _yoloWarned = false;\nfunction warnYoloOnce(): void {\n if (_yoloWarned) return;\n _yoloWarned = true;\n logger.warn(\n COMPONENT,\n 'AUTO_MODE=yolo — every tool call will run without approval. This mode is ONLY intended for sandboxed eval runs. If you see this in production logs, something is misconfigured.',\n );\n}\n\n/** For tests. */\nexport function _resetYoloWarning(): void {\n _yoloWarned = false;\n}\n\n/**\n * Convenience: returns true iff the decision means the call should\n * NOT proceed without explicit human approval. Used by the runner\n * to short-circuit the existing approval-gate path on 'auto' /\n * 'notify' decisions.\n */\nexport function shouldGate(result: ClassificationResult): boolean {\n return result.decision === 'gate';\n}\n\n/**\n * Format the classification as a one-line breadcrumb suitable for\n * the trajectory `note` event. Used by the runner on 'notify'\n * decisions so the trajectory captures what the agent did\n * autonomously vs what required a human prompt.\n */\nexport function formatBreadcrumb(toolName: string, result: ClassificationResult): string {\n return `automode:${result.decision} tool=${toolName} risk=${result.riskLevel ?? 'unknown'} policy=${result.policy} (${result.reason})`;\n}\n"],"mappings":";AAgFA,SAAS,uBAAuC;AAChD,SAAS,kBAAkB;AAC3B,OAAO,YAAY;AAEnB,MAAM,YAAY;AAyBX,SAAS,oBAAoC;AAMhD,QAAM,YAAY,QAAQ,IAAI;AAC9B,MAAI,cAAc,cAAc,cAAc,gBAAgB,cAAc,UAAU,cAAc,YAAY;AAC5G,WAAO;AAAA,EACX;AACA,MAAI;AACA,UAAM,MAAM,WAAW;AACvB,UAAM,MAAO,IAAgC;AAC7C,UAAM,KAAK,KAAK;AAChB,UAAM,SAAS,IAAI;AACnB,QAAI,WAAW,cAAc,WAAW,gBAAgB,WAAW,UAAU,WAAW,YAAY;AAChG,aAAO;AAAA,IACX;AAAA,EACJ,QAAQ;AAAA,EAA0C;AAClD,SAAO;AACX;AAYO,SAAS,iBAAiB,UAAwC;AACrE,QAAM,SAAS,kBAAkB;AACjC,QAAM,WAAW,gBAAgB,QAAQ;AACzC,QAAM,YAAY,UAAU;AAI5B,MAAI,WAAW,QAAQ;AACnB,iBAAa;AACb,WAAO;AAAA,MACH,UAAU;AAAA,MACV,QAAQ;AAAA,MACR;AAAA,MACA;AAAA,IACJ;AAAA,EACJ;AAGA,MAAI,WAAW,YAAY;AACvB,WAAO;AAAA,MACH,UAAU;AAAA,MACV,QAAQ;AAAA,MACR;AAAA,MACA;AAAA,IACJ;AAAA,EACJ;AAMA,MAAI,CAAC,UAAU;AACX,WAAO;AAAA,MACH,UAAU;AAAA,MACV,QAAQ,+BAA+B,QAAQ;AAAA,MAC/C;AAAA,IACJ;AAAA,EACJ;AAGA,MAAI,cAAc,QAAQ;AACtB,WAAO,EAAE,UAAU,QAAQ,QAAQ,2BAA2B,QAAQ,UAAU;AAAA,EACpF;AACA,MAAI,cAAc,YAAY;AAC1B,QAAI,WAAW,cAAc;AACzB,aAAO,EAAE,UAAU,QAAQ,QAAQ,oDAA+C,QAAQ,UAAU;AAAA,IACxG;AACA,WAAO,EAAE,UAAU,UAAU,QAAQ,+BAA+B,QAAQ,UAAU;AAAA,EAC1F;AACA,MAAI,cAAc,QAAQ;AACtB,WAAO,EAAE,UAAU,QAAQ,QAAQ,2BAA2B,QAAQ,UAAU;AAAA,EACpF;AAGA,SAAO;AAAA,IACH,UAAU;AAAA,IACV,QAAQ,2BAA2B,SAAS;AAAA,IAC5C;AAAA,IACA;AAAA,EACJ;AACJ;AAIA,IAAI,cAAc;AAClB,SAAS,eAAqB;AAC1B,MAAI,YAAa;AACjB,gBAAc;AACd,SAAO;AAAA,IACH;AAAA,IACA;AAAA,EACJ;AACJ;AAGO,SAAS,oBAA0B;AACtC,gBAAc;AAClB;AAQO,SAAS,WAAW,QAAuC;AAC9D,SAAO,OAAO,aAAa;AAC/B;AAQO,SAAS,iBAAiB,UAAkB,QAAsC;AACrF,SAAO,YAAY,OAAO,QAAQ,SAAS,QAAQ,SAAS,OAAO,aAAa,SAAS,WAAW,OAAO,MAAM,KAAK,OAAO,MAAM;AACvI;","names":[]}
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, readFileSync, appendFileSync, mkdirSync } from "fs";
2
+ import { existsSync, readFileSync, appendFileSync, mkdirSync, unlinkSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { homedir } from "os";
5
5
  import logger from "../utils/logger.js";
@@ -125,7 +125,7 @@ function __resetJournalForTests(scope, id) {
125
125
  const path = shardPath(scope, id);
126
126
  if (existsSync(path)) {
127
127
  try {
128
- require("fs").unlinkSync(path);
128
+ unlinkSync(path);
129
129
  } catch {
130
130
  }
131
131
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/agent/durableJournal.ts"],"sourcesContent":["/**\n * TITAN — Durable per-context event journal (Temporal-style replay).\n *\n * Why this exists\n * ---------------\n * Per awesome-agent-harness pattern #4 + Anthropic's \"Effective harnesses\n * for long-running agents\":\n *\n * \"Build systems that can resume from where the agent was when the errors\n * occurred… deterministic safeguards like retry logic and regular\n * checkpoints.\"\n *\n * TITAN's pre-existing activity-log.jsonl is GLOBAL — every event from\n * every agent in one stream. Useful for the activity feed, useless for\n * crash-replay because you can't isolate \"what was THIS agent doing on\n * THIS goal.\" This module adds per-context shards:\n *\n * `~/.titan/journals/goals/<goalId>.jsonl` — per-goal event log\n * `~/.titan/journals/agents/<agentId>.jsonl` — per-agent event log\n * `~/.titan/journals/sessions/<sessionId>.jsonl` — per-session event log\n *\n * Each line is one `{at, kind, payload}` event. Append-only — the file\n * is the source of truth, the in-memory state is derived. A fresh\n * process can call `replayGoal(goalId)` and reconstruct what the\n * previous agent was doing without losing work.\n *\n * Critically, this is NOT a replacement for the activity feed — that\n * stays as the UI-facing global stream. The journals are the AGENT's\n * own memory of its work.\n *\n * Reference:\n * - Picrew/awesome-agent-harness pattern #4 (Durable event journal)\n * - Anthropic \"Effective harnesses for long-running agents\"\n * - Temporal Continue-As-New pattern\n * - HumanLayer 12-factor agents §8 (Own Your Control Flow) + §12 (Stateless Reducer)\n */\nimport { existsSync, readFileSync, appendFileSync, mkdirSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'DurableJournal';\n\nfunction titanHome(): string {\n const env = process.env.TITAN_HOME;\n if (env) return env.startsWith('~/') ? join(homedir(), env.slice(2)) : env;\n return join(homedir(), '.titan');\n}\nfunction shardDir(scope: 'goals' | 'agents' | 'sessions'): string {\n return join(titanHome(), 'journals', scope);\n}\nfunction shardPath(scope: 'goals' | 'agents' | 'sessions', id: string): string {\n return join(shardDir(scope), `${sanitizeId(id)}.jsonl`);\n}\nfunction sanitizeId(id: string): string {\n // Belt + braces — never let an id traverse the filesystem.\n // 1. Replace any char outside the safe alphabet with `_`.\n // 2. Collapse any run of two or more dots (traversal segments) into `_`.\n const cleaned = id.replace(/[^a-zA-Z0-9_.-]/g, '_').replace(/\\.{2,}/g, '_');\n return cleaned.slice(0, 120) || '_';\n}\n\n/** The vocabulary of journal events. Add new kinds here as the agent\n * loop grows. Keep them short, verb-past-tense, and grep-friendly. */\nexport type JournalEventKind =\n | 'task_started' // agent picked up a task\n | 'task_completed' // agent finished cleanly\n | 'task_failed' // agent reported failure\n | 'task_abandoned' // lease expired / different agent adopted\n | 'tool_called' // agent called a tool\n | 'tool_result' // tool returned (success or error)\n | 'subagent_spawned' // agent invoked a sub-agent\n | 'subagent_returned' // sub-agent reported back\n | 'lesson_recorded' // a Reflexion lesson was logged\n | 'budget_warning' // policy crossed warning threshold\n | 'budget_breached' // policy crossed limit\n | 'checkpoint_saved' // round-state snapshot persisted\n | 'plan_step_updated' // goal plan.md step changed status\n | 'user_input' // user sent a chat message\n | 'agent_response'; // agent emitted final response text\n\n/** Single journal line — minimal, JSON-serialisable. */\nexport interface JournalEvent<P = Record<string, unknown>> {\n at: string; // ISO-8601 timestamp\n kind: JournalEventKind;\n payload: P;\n}\n\n/** Scope a single append can target. */\nexport interface JournalScope {\n goalId?: string;\n agentId?: string;\n sessionId?: string;\n}\n\n/**\n * Append one event to every scope it applies to. Safe to call with all\n * three scopes set — the event is recorded in each shard. Idempotent on\n * write failures (best-effort, never throws). The single write per shard\n * is O(1) — one open / append / close, no read-modify-write cycle.\n */\nexport function recordJournalEvent<P extends Record<string, unknown>>(\n scope: JournalScope,\n kind: JournalEventKind,\n payload: P = {} as P,\n): void {\n if (!scope.goalId && !scope.agentId && !scope.sessionId) return;\n const evt: JournalEvent<P> = {\n at: new Date().toISOString(),\n kind,\n payload,\n };\n const line = JSON.stringify(evt) + '\\n';\n\n const writeShard = (path: string) => {\n try {\n mkdirSync(join(path, '..'), { recursive: true });\n appendFileSync(path, line, 'utf-8');\n } catch (err) {\n // Never let a journal write break the agent loop.\n logger.warn(COMPONENT, `journal append failed: ${(err as Error).message}`);\n }\n };\n\n if (scope.goalId) writeShard(shardPath('goals', scope.goalId));\n if (scope.agentId) writeShard(shardPath('agents', scope.agentId));\n if (scope.sessionId) writeShard(shardPath('sessions', scope.sessionId));\n}\n\n/** Read all events for a scope, in order. Returns [] when no journal exists. */\nexport function readJournal(scope: 'goals' | 'agents' | 'sessions', id: string): JournalEvent[] {\n const path = shardPath(scope, id);\n if (!existsSync(path)) return [];\n try {\n const raw = readFileSync(path, 'utf-8');\n const lines = raw.split('\\n').filter(l => l.trim().length > 0);\n const events: JournalEvent[] = [];\n for (const l of lines) {\n try { events.push(JSON.parse(l) as JournalEvent); }\n catch { /* skip malformed line — never abort the whole replay */ }\n }\n return events;\n } catch (err) {\n logger.warn(COMPONENT, `journal read failed for ${scope}/${id}: ${(err as Error).message}`);\n return [];\n }\n}\n\n// ─── Replay primitives ─────────────────────────────────────────────\n\n/**\n * The state we derive from replaying a goal's journal. Intentionally\n * minimal — just the fields a resuming process needs to decide what to\n * do next. The agent runtime can stuff more derived fields into this\n * shape later; this is the floor.\n */\nexport interface GoalReplayState {\n goalId: string;\n /** True once a task_completed event has been seen for this goal. */\n completed: boolean;\n /** True if task_failed appears with no later task_started. */\n failed: boolean;\n /** Most-recent agent that picked up the goal. */\n currentAgentId: string | null;\n /** Tool calls observed (count). Used as a coarse progress signal. */\n toolCallCount: number;\n /** Sub-agents spawned (count). */\n subAgentCount: number;\n /** Most-recent plan-step status updates, keyed by stepId. */\n stepStatus: Record<string, string>;\n /** Lessons recorded during this goal (text only — full lesson lives\n * in agentLessons.ts). Lets a resuming agent see what it learned. */\n lessonsRecorded: string[];\n /** Last event timestamp — useful for staleness detection. */\n lastEventAt: string | null;\n /** Total event count (for telemetry). */\n eventCount: number;\n}\n\n/** Pure reduction — apply one event to a state snapshot. Exported so the\n * wakeup reducer (upgrade #5) can use the same fold. */\nexport function applyEventToGoalState(state: GoalReplayState, evt: JournalEvent): GoalReplayState {\n const next: GoalReplayState = {\n ...state,\n stepStatus: { ...state.stepStatus },\n lessonsRecorded: [...state.lessonsRecorded],\n lastEventAt: evt.at,\n eventCount: state.eventCount + 1,\n };\n\n switch (evt.kind) {\n case 'task_started': {\n const aid = (evt.payload as { agentId?: string }).agentId;\n if (typeof aid === 'string' && aid) next.currentAgentId = aid;\n next.failed = false;\n break;\n }\n case 'task_completed':\n next.completed = true;\n next.failed = false;\n break;\n case 'task_failed':\n next.failed = true;\n break;\n case 'task_abandoned':\n next.currentAgentId = null;\n break;\n case 'tool_called':\n next.toolCallCount = state.toolCallCount + 1;\n break;\n case 'subagent_spawned':\n next.subAgentCount = state.subAgentCount + 1;\n break;\n case 'lesson_recorded': {\n const text = (evt.payload as { text?: string }).text;\n if (typeof text === 'string' && text) next.lessonsRecorded.push(text);\n break;\n }\n case 'plan_step_updated': {\n const sid = (evt.payload as { stepId?: string }).stepId;\n const st = (evt.payload as { status?: string }).status;\n if (typeof sid === 'string' && typeof st === 'string') next.stepStatus[sid] = st;\n break;\n }\n default:\n // tool_result, checkpoint_saved, budget_*, user_input, agent_response —\n // currently no state derivation; just tick eventCount.\n break;\n }\n return next;\n}\n\n/**\n * Replay a goal's journal into a derived state snapshot. O(n) in events.\n * A fresh process calls this on resume to know what's been done already.\n */\nexport function replayGoal(goalId: string): GoalReplayState {\n const events = readJournal('goals', goalId);\n const seed: GoalReplayState = {\n goalId,\n completed: false,\n failed: false,\n currentAgentId: null,\n toolCallCount: 0,\n subAgentCount: 0,\n stepStatus: {},\n lessonsRecorded: [],\n lastEventAt: null,\n eventCount: 0,\n };\n return events.reduce(applyEventToGoalState, seed);\n}\n\n/** Test helper — wipe a scope's journal. */\nexport function __resetJournalForTests(scope: 'goals' | 'agents' | 'sessions', id: string): void {\n const path = shardPath(scope, id);\n if (existsSync(path)) {\n try { require('fs').unlinkSync(path); } catch { /* fine */ }\n }\n}\n"],"mappings":";AAoCA,SAAS,YAAY,cAAc,gBAAgB,iBAAiB;AACpE,SAAS,YAAY;AACrB,SAAS,eAAe;AACxB,OAAO,YAAY;AAEnB,MAAM,YAAY;AAElB,SAAS,YAAoB;AACzB,QAAM,MAAM,QAAQ,IAAI;AACxB,MAAI,IAAK,QAAO,IAAI,WAAW,IAAI,IAAI,KAAK,QAAQ,GAAG,IAAI,MAAM,CAAC,CAAC,IAAI;AACvE,SAAO,KAAK,QAAQ,GAAG,QAAQ;AACnC;AACA,SAAS,SAAS,OAAgD;AAC9D,SAAO,KAAK,UAAU,GAAG,YAAY,KAAK;AAC9C;AACA,SAAS,UAAU,OAAwC,IAAoB;AAC3E,SAAO,KAAK,SAAS,KAAK,GAAG,GAAG,WAAW,EAAE,CAAC,QAAQ;AAC1D;AACA,SAAS,WAAW,IAAoB;AAIpC,QAAM,UAAU,GAAG,QAAQ,oBAAoB,GAAG,EAAE,QAAQ,WAAW,GAAG;AAC1E,SAAO,QAAQ,MAAM,GAAG,GAAG,KAAK;AACpC;AAyCO,SAAS,mBACZ,OACA,MACA,UAAa,CAAC,GACV;AACJ,MAAI,CAAC,MAAM,UAAU,CAAC,MAAM,WAAW,CAAC,MAAM,UAAW;AACzD,QAAM,MAAuB;AAAA,IACzB,KAAI,oBAAI,KAAK,GAAE,YAAY;AAAA,IAC3B;AAAA,IACA;AAAA,EACJ;AACA,QAAM,OAAO,KAAK,UAAU,GAAG,IAAI;AAEnC,QAAM,aAAa,CAAC,SAAiB;AACjC,QAAI;AACA,gBAAU,KAAK,MAAM,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAC/C,qBAAe,MAAM,MAAM,OAAO;AAAA,IACtC,SAAS,KAAK;AAEV,aAAO,KAAK,WAAW,0BAA2B,IAAc,OAAO,EAAE;AAAA,IAC7E;AAAA,EACJ;AAEA,MAAI,MAAM,OAAW,YAAW,UAAU,SAAS,MAAM,MAAM,CAAC;AAChE,MAAI,MAAM,QAAW,YAAW,UAAU,UAAU,MAAM,OAAO,CAAC;AAClE,MAAI,MAAM,UAAW,YAAW,UAAU,YAAY,MAAM,SAAS,CAAC;AAC1E;AAGO,SAAS,YAAY,OAAwC,IAA4B;AAC5F,QAAM,OAAO,UAAU,OAAO,EAAE;AAChC,MAAI,CAAC,WAAW,IAAI,EAAG,QAAO,CAAC;AAC/B,MAAI;AACA,UAAM,MAAM,aAAa,MAAM,OAAO;AACtC,UAAM,QAAQ,IAAI,MAAM,IAAI,EAAE,OAAO,OAAK,EAAE,KAAK,EAAE,SAAS,CAAC;AAC7D,UAAM,SAAyB,CAAC;AAChC,eAAW,KAAK,OAAO;AACnB,UAAI;AAAE,eAAO,KAAK,KAAK,MAAM,CAAC,CAAiB;AAAA,MAAG,QAC5C;AAAA,MAA2D;AAAA,IACrE;AACA,WAAO;AAAA,EACX,SAAS,KAAK;AACV,WAAO,KAAK,WAAW,2BAA2B,KAAK,IAAI,EAAE,KAAM,IAAc,OAAO,EAAE;AAC1F,WAAO,CAAC;AAAA,EACZ;AACJ;AAmCO,SAAS,sBAAsB,OAAwB,KAAoC;AAC9F,QAAM,OAAwB;AAAA,IAC1B,GAAG;AAAA,IACH,YAAY,EAAE,GAAG,MAAM,WAAW;AAAA,IAClC,iBAAiB,CAAC,GAAG,MAAM,eAAe;AAAA,IAC1C,aAAa,IAAI;AAAA,IACjB,YAAY,MAAM,aAAa;AAAA,EACnC;AAEA,UAAQ,IAAI,MAAM;AAAA,IACd,KAAK,gBAAgB;AACjB,YAAM,MAAO,IAAI,QAAiC;AAClD,UAAI,OAAO,QAAQ,YAAY,IAAK,MAAK,iBAAiB;AAC1D,WAAK,SAAS;AACd;AAAA,IACJ;AAAA,IACA,KAAK;AACD,WAAK,YAAY;AACjB,WAAK,SAAS;AACd;AAAA,IACJ,KAAK;AACD,WAAK,SAAS;AACd;AAAA,IACJ,KAAK;AACD,WAAK,iBAAiB;AACtB;AAAA,IACJ,KAAK;AACD,WAAK,gBAAgB,MAAM,gBAAgB;AAC3C;AAAA,IACJ,KAAK;AACD,WAAK,gBAAgB,MAAM,gBAAgB;AAC3C;AAAA,IACJ,KAAK,mBAAmB;AACpB,YAAM,OAAQ,IAAI,QAA8B;AAChD,UAAI,OAAO,SAAS,YAAY,KAAM,MAAK,gBAAgB,KAAK,IAAI;AACpE;AAAA,IACJ;AAAA,IACA,KAAK,qBAAqB;AACtB,YAAM,MAAO,IAAI,QAAgC;AACjD,YAAM,KAAM,IAAI,QAAgC;AAChD,UAAI,OAAO,QAAQ,YAAY,OAAO,OAAO,SAAU,MAAK,WAAW,GAAG,IAAI;AAC9E;AAAA,IACJ;AAAA,IACA;AAGI;AAAA,EACR;AACA,SAAO;AACX;AAMO,SAAS,WAAW,QAAiC;AACxD,QAAM,SAAS,YAAY,SAAS,MAAM;AAC1C,QAAM,OAAwB;AAAA,IAC1B;AAAA,IACA,WAAW;AAAA,IACX,QAAQ;AAAA,IACR,gBAAgB;AAAA,IAChB,eAAe;AAAA,IACf,eAAe;AAAA,IACf,YAAY,CAAC;AAAA,IACb,iBAAiB,CAAC;AAAA,IAClB,aAAa;AAAA,IACb,YAAY;AAAA,EAChB;AACA,SAAO,OAAO,OAAO,uBAAuB,IAAI;AACpD;AAGO,SAAS,uBAAuB,OAAwC,IAAkB;AAC7F,QAAM,OAAO,UAAU,OAAO,EAAE;AAChC,MAAI,WAAW,IAAI,GAAG;AAClB,QAAI;AAAE,cAAQ,IAAI,EAAE,WAAW,IAAI;AAAA,IAAG,QAAQ;AAAA,IAAa;AAAA,EAC/D;AACJ;","names":[]}
1
+ {"version":3,"sources":["../../src/agent/durableJournal.ts"],"sourcesContent":["/**\n * TITAN — Durable per-context event journal (Temporal-style replay).\n *\n * Why this exists\n * ---------------\n * Per awesome-agent-harness pattern #4 + Anthropic's \"Effective harnesses\n * for long-running agents\":\n *\n * \"Build systems that can resume from where the agent was when the errors\n * occurred… deterministic safeguards like retry logic and regular\n * checkpoints.\"\n *\n * TITAN's pre-existing activity-log.jsonl is GLOBAL — every event from\n * every agent in one stream. Useful for the activity feed, useless for\n * crash-replay because you can't isolate \"what was THIS agent doing on\n * THIS goal.\" This module adds per-context shards:\n *\n * `~/.titan/journals/goals/<goalId>.jsonl` — per-goal event log\n * `~/.titan/journals/agents/<agentId>.jsonl` — per-agent event log\n * `~/.titan/journals/sessions/<sessionId>.jsonl` — per-session event log\n *\n * Each line is one `{at, kind, payload}` event. Append-only — the file\n * is the source of truth, the in-memory state is derived. A fresh\n * process can call `replayGoal(goalId)` and reconstruct what the\n * previous agent was doing without losing work.\n *\n * Critically, this is NOT a replacement for the activity feed — that\n * stays as the UI-facing global stream. The journals are the AGENT's\n * own memory of its work.\n *\n * Reference:\n * - Picrew/awesome-agent-harness pattern #4 (Durable event journal)\n * - Anthropic \"Effective harnesses for long-running agents\"\n * - Temporal Continue-As-New pattern\n * - HumanLayer 12-factor agents §8 (Own Your Control Flow) + §12 (Stateless Reducer)\n */\nimport { existsSync, readFileSync, appendFileSync, mkdirSync, unlinkSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'DurableJournal';\n\nfunction titanHome(): string {\n const env = process.env.TITAN_HOME;\n if (env) return env.startsWith('~/') ? join(homedir(), env.slice(2)) : env;\n return join(homedir(), '.titan');\n}\nfunction shardDir(scope: 'goals' | 'agents' | 'sessions'): string {\n return join(titanHome(), 'journals', scope);\n}\nfunction shardPath(scope: 'goals' | 'agents' | 'sessions', id: string): string {\n return join(shardDir(scope), `${sanitizeId(id)}.jsonl`);\n}\nfunction sanitizeId(id: string): string {\n // Belt + braces — never let an id traverse the filesystem.\n // 1. Replace any char outside the safe alphabet with `_`.\n // 2. Collapse any run of two or more dots (traversal segments) into `_`.\n const cleaned = id.replace(/[^a-zA-Z0-9_.-]/g, '_').replace(/\\.{2,}/g, '_');\n return cleaned.slice(0, 120) || '_';\n}\n\n/** The vocabulary of journal events. Add new kinds here as the agent\n * loop grows. Keep them short, verb-past-tense, and grep-friendly. */\nexport type JournalEventKind =\n | 'task_started' // agent picked up a task\n | 'task_completed' // agent finished cleanly\n | 'task_failed' // agent reported failure\n | 'task_abandoned' // lease expired / different agent adopted\n | 'tool_called' // agent called a tool\n | 'tool_result' // tool returned (success or error)\n | 'subagent_spawned' // agent invoked a sub-agent\n | 'subagent_returned' // sub-agent reported back\n | 'lesson_recorded' // a Reflexion lesson was logged\n | 'budget_warning' // policy crossed warning threshold\n | 'budget_breached' // policy crossed limit\n | 'checkpoint_saved' // round-state snapshot persisted\n | 'plan_step_updated' // goal plan.md step changed status\n | 'user_input' // user sent a chat message\n | 'agent_response'; // agent emitted final response text\n\n/** Single journal line — minimal, JSON-serialisable. */\nexport interface JournalEvent<P = Record<string, unknown>> {\n at: string; // ISO-8601 timestamp\n kind: JournalEventKind;\n payload: P;\n}\n\n/** Scope a single append can target. */\nexport interface JournalScope {\n goalId?: string;\n agentId?: string;\n sessionId?: string;\n}\n\n/**\n * Append one event to every scope it applies to. Safe to call with all\n * three scopes set — the event is recorded in each shard. Idempotent on\n * write failures (best-effort, never throws). The single write per shard\n * is O(1) — one open / append / close, no read-modify-write cycle.\n */\nexport function recordJournalEvent<P extends Record<string, unknown>>(\n scope: JournalScope,\n kind: JournalEventKind,\n payload: P = {} as P,\n): void {\n if (!scope.goalId && !scope.agentId && !scope.sessionId) return;\n const evt: JournalEvent<P> = {\n at: new Date().toISOString(),\n kind,\n payload,\n };\n const line = JSON.stringify(evt) + '\\n';\n\n const writeShard = (path: string) => {\n try {\n mkdirSync(join(path, '..'), { recursive: true });\n appendFileSync(path, line, 'utf-8');\n } catch (err) {\n // Never let a journal write break the agent loop.\n logger.warn(COMPONENT, `journal append failed: ${(err as Error).message}`);\n }\n };\n\n if (scope.goalId) writeShard(shardPath('goals', scope.goalId));\n if (scope.agentId) writeShard(shardPath('agents', scope.agentId));\n if (scope.sessionId) writeShard(shardPath('sessions', scope.sessionId));\n}\n\n/** Read all events for a scope, in order. Returns [] when no journal exists. */\nexport function readJournal(scope: 'goals' | 'agents' | 'sessions', id: string): JournalEvent[] {\n const path = shardPath(scope, id);\n if (!existsSync(path)) return [];\n try {\n const raw = readFileSync(path, 'utf-8');\n const lines = raw.split('\\n').filter(l => l.trim().length > 0);\n const events: JournalEvent[] = [];\n for (const l of lines) {\n try { events.push(JSON.parse(l) as JournalEvent); }\n catch { /* skip malformed line — never abort the whole replay */ }\n }\n return events;\n } catch (err) {\n logger.warn(COMPONENT, `journal read failed for ${scope}/${id}: ${(err as Error).message}`);\n return [];\n }\n}\n\n// ─── Replay primitives ─────────────────────────────────────────────\n\n/**\n * The state we derive from replaying a goal's journal. Intentionally\n * minimal — just the fields a resuming process needs to decide what to\n * do next. The agent runtime can stuff more derived fields into this\n * shape later; this is the floor.\n */\nexport interface GoalReplayState {\n goalId: string;\n /** True once a task_completed event has been seen for this goal. */\n completed: boolean;\n /** True if task_failed appears with no later task_started. */\n failed: boolean;\n /** Most-recent agent that picked up the goal. */\n currentAgentId: string | null;\n /** Tool calls observed (count). Used as a coarse progress signal. */\n toolCallCount: number;\n /** Sub-agents spawned (count). */\n subAgentCount: number;\n /** Most-recent plan-step status updates, keyed by stepId. */\n stepStatus: Record<string, string>;\n /** Lessons recorded during this goal (text only — full lesson lives\n * in agentLessons.ts). Lets a resuming agent see what it learned. */\n lessonsRecorded: string[];\n /** Last event timestamp — useful for staleness detection. */\n lastEventAt: string | null;\n /** Total event count (for telemetry). */\n eventCount: number;\n}\n\n/** Pure reduction — apply one event to a state snapshot. Exported so the\n * wakeup reducer (upgrade #5) can use the same fold. */\nexport function applyEventToGoalState(state: GoalReplayState, evt: JournalEvent): GoalReplayState {\n const next: GoalReplayState = {\n ...state,\n stepStatus: { ...state.stepStatus },\n lessonsRecorded: [...state.lessonsRecorded],\n lastEventAt: evt.at,\n eventCount: state.eventCount + 1,\n };\n\n switch (evt.kind) {\n case 'task_started': {\n const aid = (evt.payload as { agentId?: string }).agentId;\n if (typeof aid === 'string' && aid) next.currentAgentId = aid;\n next.failed = false;\n break;\n }\n case 'task_completed':\n next.completed = true;\n next.failed = false;\n break;\n case 'task_failed':\n next.failed = true;\n break;\n case 'task_abandoned':\n next.currentAgentId = null;\n break;\n case 'tool_called':\n next.toolCallCount = state.toolCallCount + 1;\n break;\n case 'subagent_spawned':\n next.subAgentCount = state.subAgentCount + 1;\n break;\n case 'lesson_recorded': {\n const text = (evt.payload as { text?: string }).text;\n if (typeof text === 'string' && text) next.lessonsRecorded.push(text);\n break;\n }\n case 'plan_step_updated': {\n const sid = (evt.payload as { stepId?: string }).stepId;\n const st = (evt.payload as { status?: string }).status;\n if (typeof sid === 'string' && typeof st === 'string') next.stepStatus[sid] = st;\n break;\n }\n default:\n // tool_result, checkpoint_saved, budget_*, user_input, agent_response —\n // currently no state derivation; just tick eventCount.\n break;\n }\n return next;\n}\n\n/**\n * Replay a goal's journal into a derived state snapshot. O(n) in events.\n * A fresh process calls this on resume to know what's been done already.\n */\nexport function replayGoal(goalId: string): GoalReplayState {\n const events = readJournal('goals', goalId);\n const seed: GoalReplayState = {\n goalId,\n completed: false,\n failed: false,\n currentAgentId: null,\n toolCallCount: 0,\n subAgentCount: 0,\n stepStatus: {},\n lessonsRecorded: [],\n lastEventAt: null,\n eventCount: 0,\n };\n return events.reduce(applyEventToGoalState, seed);\n}\n\n/** Test helper — wipe a scope's journal. */\nexport function __resetJournalForTests(scope: 'goals' | 'agents' | 'sessions', id: string): void {\n const path = shardPath(scope, id);\n if (existsSync(path)) {\n try { unlinkSync(path); } catch { /* fine */ }\n }\n}\n"],"mappings":";AAoCA,SAAS,YAAY,cAAc,gBAAgB,WAAW,kBAAkB;AAChF,SAAS,YAAY;AACrB,SAAS,eAAe;AACxB,OAAO,YAAY;AAEnB,MAAM,YAAY;AAElB,SAAS,YAAoB;AACzB,QAAM,MAAM,QAAQ,IAAI;AACxB,MAAI,IAAK,QAAO,IAAI,WAAW,IAAI,IAAI,KAAK,QAAQ,GAAG,IAAI,MAAM,CAAC,CAAC,IAAI;AACvE,SAAO,KAAK,QAAQ,GAAG,QAAQ;AACnC;AACA,SAAS,SAAS,OAAgD;AAC9D,SAAO,KAAK,UAAU,GAAG,YAAY,KAAK;AAC9C;AACA,SAAS,UAAU,OAAwC,IAAoB;AAC3E,SAAO,KAAK,SAAS,KAAK,GAAG,GAAG,WAAW,EAAE,CAAC,QAAQ;AAC1D;AACA,SAAS,WAAW,IAAoB;AAIpC,QAAM,UAAU,GAAG,QAAQ,oBAAoB,GAAG,EAAE,QAAQ,WAAW,GAAG;AAC1E,SAAO,QAAQ,MAAM,GAAG,GAAG,KAAK;AACpC;AAyCO,SAAS,mBACZ,OACA,MACA,UAAa,CAAC,GACV;AACJ,MAAI,CAAC,MAAM,UAAU,CAAC,MAAM,WAAW,CAAC,MAAM,UAAW;AACzD,QAAM,MAAuB;AAAA,IACzB,KAAI,oBAAI,KAAK,GAAE,YAAY;AAAA,IAC3B;AAAA,IACA;AAAA,EACJ;AACA,QAAM,OAAO,KAAK,UAAU,GAAG,IAAI;AAEnC,QAAM,aAAa,CAAC,SAAiB;AACjC,QAAI;AACA,gBAAU,KAAK,MAAM,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAC/C,qBAAe,MAAM,MAAM,OAAO;AAAA,IACtC,SAAS,KAAK;AAEV,aAAO,KAAK,WAAW,0BAA2B,IAAc,OAAO,EAAE;AAAA,IAC7E;AAAA,EACJ;AAEA,MAAI,MAAM,OAAW,YAAW,UAAU,SAAS,MAAM,MAAM,CAAC;AAChE,MAAI,MAAM,QAAW,YAAW,UAAU,UAAU,MAAM,OAAO,CAAC;AAClE,MAAI,MAAM,UAAW,YAAW,UAAU,YAAY,MAAM,SAAS,CAAC;AAC1E;AAGO,SAAS,YAAY,OAAwC,IAA4B;AAC5F,QAAM,OAAO,UAAU,OAAO,EAAE;AAChC,MAAI,CAAC,WAAW,IAAI,EAAG,QAAO,CAAC;AAC/B,MAAI;AACA,UAAM,MAAM,aAAa,MAAM,OAAO;AACtC,UAAM,QAAQ,IAAI,MAAM,IAAI,EAAE,OAAO,OAAK,EAAE,KAAK,EAAE,SAAS,CAAC;AAC7D,UAAM,SAAyB,CAAC;AAChC,eAAW,KAAK,OAAO;AACnB,UAAI;AAAE,eAAO,KAAK,KAAK,MAAM,CAAC,CAAiB;AAAA,MAAG,QAC5C;AAAA,MAA2D;AAAA,IACrE;AACA,WAAO;AAAA,EACX,SAAS,KAAK;AACV,WAAO,KAAK,WAAW,2BAA2B,KAAK,IAAI,EAAE,KAAM,IAAc,OAAO,EAAE;AAC1F,WAAO,CAAC;AAAA,EACZ;AACJ;AAmCO,SAAS,sBAAsB,OAAwB,KAAoC;AAC9F,QAAM,OAAwB;AAAA,IAC1B,GAAG;AAAA,IACH,YAAY,EAAE,GAAG,MAAM,WAAW;AAAA,IAClC,iBAAiB,CAAC,GAAG,MAAM,eAAe;AAAA,IAC1C,aAAa,IAAI;AAAA,IACjB,YAAY,MAAM,aAAa;AAAA,EACnC;AAEA,UAAQ,IAAI,MAAM;AAAA,IACd,KAAK,gBAAgB;AACjB,YAAM,MAAO,IAAI,QAAiC;AAClD,UAAI,OAAO,QAAQ,YAAY,IAAK,MAAK,iBAAiB;AAC1D,WAAK,SAAS;AACd;AAAA,IACJ;AAAA,IACA,KAAK;AACD,WAAK,YAAY;AACjB,WAAK,SAAS;AACd;AAAA,IACJ,KAAK;AACD,WAAK,SAAS;AACd;AAAA,IACJ,KAAK;AACD,WAAK,iBAAiB;AACtB;AAAA,IACJ,KAAK;AACD,WAAK,gBAAgB,MAAM,gBAAgB;AAC3C;AAAA,IACJ,KAAK;AACD,WAAK,gBAAgB,MAAM,gBAAgB;AAC3C;AAAA,IACJ,KAAK,mBAAmB;AACpB,YAAM,OAAQ,IAAI,QAA8B;AAChD,UAAI,OAAO,SAAS,YAAY,KAAM,MAAK,gBAAgB,KAAK,IAAI;AACpE;AAAA,IACJ;AAAA,IACA,KAAK,qBAAqB;AACtB,YAAM,MAAO,IAAI,QAAgC;AACjD,YAAM,KAAM,IAAI,QAAgC;AAChD,UAAI,OAAO,QAAQ,YAAY,OAAO,OAAO,SAAU,MAAK,WAAW,GAAG,IAAI;AAC9E;AAAA,IACJ;AAAA,IACA;AAGI;AAAA,EACR;AACA,SAAO;AACX;AAMO,SAAS,WAAW,QAAiC;AACxD,QAAM,SAAS,YAAY,SAAS,MAAM;AAC1C,QAAM,OAAwB;AAAA,IAC1B;AAAA,IACA,WAAW;AAAA,IACX,QAAQ;AAAA,IACR,gBAAgB;AAAA,IAChB,eAAe;AAAA,IACf,eAAe;AAAA,IACf,YAAY,CAAC;AAAA,IACb,iBAAiB,CAAC;AAAA,IAClB,aAAa;AAAA,IACb,YAAY;AAAA,EAChB;AACA,SAAO,OAAO,OAAO,uBAAuB,IAAI;AACpD;AAGO,SAAS,uBAAuB,OAAwC,IAAkB;AAC7F,QAAM,OAAO,UAAU,OAAO,EAAE;AAChC,MAAI,WAAW,IAAI,GAAG;AAClB,QAAI;AAAE,iBAAW,IAAI;AAAA,IAAG,QAAQ;AAAA,IAAa;AAAA,EACjD;AACJ;","names":[]}
@@ -10,6 +10,7 @@ import { DEFAULT_BUDGET_CAPS, checkBudget, suggestDegradation, recordSpend } fro
10
10
  import { structuredSpawn } from "./structuredSpawn.js";
11
11
  import { verifyByKind } from "./verifier.js";
12
12
  import { isGenericQuestion, forceCommitDirective } from "./questionQuality.js";
13
+ import { verificationEventsSince } from "./verification.js";
13
14
  import { onGoalCompleted, onGoalFailed, onGoalBlocked } from "./somaFeedback.js";
14
15
  import { emitAgentEvent } from "./agentEvents.js";
15
16
  const COMPONENT = "GoalDriver";
@@ -366,11 +367,48 @@ ${baseTask}` : baseTask;
366
367
  ...result.artifacts.map((a) => a.ref)
367
368
  ])];
368
369
  if (result.status === "done") {
370
+ subState.consecutiveNeedsInfoCount = 0;
371
+ try {
372
+ const spawnStartMs = subState.pendingSpawn?.attemptedAt ? new Date(subState.pendingSpawn.attemptedAt).getTime() : Date.now() - durationMs;
373
+ const events = verificationEventsSince(spawnStartMs);
374
+ const failed = events.filter((e) => !e.result.passed);
375
+ const passed = events.filter((e) => e.result.passed);
376
+ if (failed.length > 0 && passed.length === 0) {
377
+ subState.consecutiveAssertionFailures = (subState.consecutiveAssertionFailures ?? 0) + 1;
378
+ const count = subState.consecutiveAssertionFailures;
379
+ appendHistory(
380
+ state,
381
+ "observing",
382
+ `Spawn produced ${failed.length} verification failure(s) (counter=${count}): ${failed[0].result.reason.slice(0, 100)}`
383
+ );
384
+ if (count >= 3) {
385
+ try {
386
+ const { failSubtask } = await import("./goals.js");
387
+ failSubtask(
388
+ goal.id,
389
+ next.id,
390
+ `Subtask failed verification ${count}\xD7 in a row. Last failure: ${failed[0].result.reason.slice(0, 200)}. The specialist keeps producing artifacts that don't survive readback (byte-count drift, magic-header mismatch, partial write). Failing to break the loop.`
391
+ );
392
+ } catch {
393
+ }
394
+ subState.lastError = `Failed: ${count} consecutive verification failures`;
395
+ subState.pendingSpawn = void 0;
396
+ state.currentSubtaskId = void 0;
397
+ state.phase = "delegating";
398
+ appendHistory(state, "delegating", `Verification stuck-loop fail on ${next.id} (${count}\xD7 failures)`);
399
+ return;
400
+ }
401
+ } else if (passed.length > 0) {
402
+ subState.consecutiveAssertionFailures = 0;
403
+ }
404
+ } catch {
405
+ }
369
406
  state.phase = "verifying";
370
407
  appendHistory(state, "verifying", `Spawn returned done with ${result.artifacts.length} artifact(s), confidence ${result.confidence.toFixed(2)}`);
371
408
  subState.lastSpawnResult = result;
372
409
  } else if (result.status === "failed") {
373
410
  subState.lastError = result.reasoning || "failed";
411
+ subState.consecutiveNeedsInfoCount = 0;
374
412
  state.phase = "iterating";
375
413
  appendHistory(state, "iterating", `Spawn returned failed: ${subState.lastError.slice(0, 120)}`);
376
414
  } else if (result.status === "needs_info" || result.status === "blocked") {
@@ -379,6 +417,35 @@ ${baseTask}` : baseTask;
379
417
  const attemptCount = subState.attempts;
380
418
  const lastErr = subState.lastError;
381
419
  const rawQuestion = result.questions[0] ?? "";
420
+ subState.consecutiveNeedsInfoCount = (subState.consecutiveNeedsInfoCount || 0) + 1;
421
+ const trimmedQuestion = rawQuestion.trim();
422
+ const needsInfoCount = subState.consecutiveNeedsInfoCount;
423
+ if (trimmedQuestion.length === 0 && attemptCount >= 2) {
424
+ try {
425
+ const { failSubtask } = await import("./goals.js");
426
+ failSubtask(goal.id, next.id, `Specialist ${specialistName} returned needs_info with an empty question on attempt ${attemptCount}. No actionable signal \u2014 failing subtask to break the loop.`);
427
+ } catch {
428
+ }
429
+ subState.lastError = `Failed: empty needs_info on attempt ${attemptCount} (specialist ${specialistName})`;
430
+ state.currentSubtaskId = void 0;
431
+ state.phase = "delegating";
432
+ appendHistory(state, "delegating", `Empty needs_info fail on ${next.id} (attempt ${attemptCount})`);
433
+ subState.pendingSpawn = void 0;
434
+ return;
435
+ }
436
+ if (needsInfoCount >= 3) {
437
+ try {
438
+ const { failSubtask } = await import("./goals.js");
439
+ failSubtask(goal.id, next.id, `Specialist ${specialistName} returned needs_info ${needsInfoCount}\xD7 in a row on this subtask. Forcing failure to break the loop. Last question: "${trimmedQuestion.slice(0, 160)}"`);
440
+ } catch {
441
+ }
442
+ subState.lastError = `Failed: ${needsInfoCount} consecutive needs_info returns`;
443
+ state.currentSubtaskId = void 0;
444
+ state.phase = "delegating";
445
+ appendHistory(state, "delegating", `Consecutive needs_info fail on ${next.id} (${needsInfoCount}\xD7)`);
446
+ subState.pendingSpawn = void 0;
447
+ return;
448
+ }
382
449
  if (subState.commitOverride) {
383
450
  subState.commitOverride = false;
384
451
  try {