titan-agent 6.5.2 → 7.0.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.
- package/README.md +180 -53
- package/assets/agent-skills/NOTICE-ECC.md +39 -0
- package/assets/agent-skills/agent-introspection-debugging/SKILL.md +153 -0
- package/assets/agent-skills/continuous-agent-loop/SKILL.md +45 -0
- package/assets/agent-skills/continuous-learning/SKILL.md +131 -0
- package/assets/agent-skills/continuous-learning/config.json +18 -0
- package/assets/agent-skills/continuous-learning/evaluate-session.sh +69 -0
- package/assets/agent-skills/production-audit/SKILL.md +206 -0
- package/assets/agent-skills/recursive-decision-ledger/SKILL.md +79 -0
- package/assets/agent-skills/scientific-thinking-literature-review/SKILL.md +192 -0
- package/assets/agent-skills/scientific-thinking-scholar-evaluation/SKILL.md +160 -0
- package/assets/agent-skills/verification-loop/SKILL.md +126 -0
- package/dist/agent/agent.js +67 -26
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/agentLoop.js +6 -2
- package/dist/agent/agentLoop.js.map +1 -1
- package/dist/agent/autoModeClassifier.js +26 -2
- package/dist/agent/autoModeClassifier.js.map +1 -1
- package/dist/agent/goalDriver.js +2 -0
- package/dist/agent/goalDriver.js.map +1 -1
- package/dist/agent/goalSpec.js +229 -0
- package/dist/agent/goalSpec.js.map +1 -0
- package/dist/agent/heartbeat.js +130 -0
- package/dist/agent/heartbeat.js.map +1 -0
- package/dist/agent/missionDecompose.js +38 -0
- package/dist/agent/missionDecompose.js.map +1 -0
- package/dist/agent/muscleMemory.js +291 -0
- package/dist/agent/muscleMemory.js.map +1 -0
- package/dist/agent/observationWriter.js +56 -0
- package/dist/agent/observationWriter.js.map +1 -0
- package/dist/agent/promptSectionCaps.js +6 -0
- package/dist/agent/promptSectionCaps.js.map +1 -1
- package/dist/agent/reminders.js +101 -0
- package/dist/agent/reminders.js.map +1 -0
- package/dist/agent/systemWidgets.js +41 -0
- package/dist/agent/systemWidgets.js.map +1 -0
- package/dist/agent/toolRunner.js +24 -0
- package/dist/agent/toolRunner.js.map +1 -1
- package/dist/agent/toolSearch.js +3 -0
- package/dist/agent/toolSearch.js.map +1 -1
- package/dist/channels/messenger.js +39 -0
- package/dist/channels/messenger.js.map +1 -1
- package/dist/config/config.js +7 -0
- package/dist/config/config.js.map +1 -1
- package/dist/config/schema.js +18 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/eval/harness.js +13 -3
- package/dist/eval/harness.js.map +1 -1
- package/dist/gateway/onboarding.js +91 -0
- package/dist/gateway/onboarding.js.map +1 -0
- package/dist/gateway/routes/skills.js +2 -1
- package/dist/gateway/routes/skills.js.map +1 -1
- package/dist/gateway/routes/studioRouter.js +88 -0
- package/dist/gateway/routes/studioRouter.js.map +1 -0
- package/dist/gateway/routes/watchRouter.js +4 -4
- package/dist/gateway/routes/watchRouter.js.map +1 -1
- package/dist/gateway/server.js +251 -43
- package/dist/gateway/server.js.map +1 -1
- package/dist/memory/taxonomy.js +117 -0
- package/dist/memory/taxonomy.js.map +1 -0
- package/dist/providers/modelCapabilities.js +13 -0
- package/dist/providers/modelCapabilities.js.map +1 -1
- package/dist/providers/ollama.js +8 -2
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/openai.js +1 -1
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openai_compat.js +38 -2
- package/dist/providers/openai_compat.js.map +1 -1
- package/dist/providers/router.js +14 -0
- package/dist/providers/router.js.map +1 -1
- package/dist/recipes/runner.js +8 -1
- package/dist/recipes/runner.js.map +1 -1
- package/dist/recipes/store.js +2 -1
- package/dist/recipes/store.js.map +1 -1
- package/dist/skills/builtin/codebase_explore.js +214 -0
- package/dist/skills/builtin/codebase_explore.js.map +1 -0
- package/dist/skills/builtin/config_audit.js +160 -0
- package/dist/skills/builtin/config_audit.js.map +1 -0
- package/dist/skills/builtin/delegate_agent.js +107 -0
- package/dist/skills/builtin/delegate_agent.js.map +1 -0
- package/dist/skills/builtin/memory_map.js +38 -0
- package/dist/skills/builtin/memory_map.js.map +1 -0
- package/dist/skills/builtin/reminders.js +67 -0
- package/dist/skills/builtin/reminders.js.map +1 -0
- package/dist/skills/builtin/spec.js +139 -0
- package/dist/skills/builtin/spec.js.map +1 -0
- package/dist/skills/builtin/widget_gallery.js +12 -5
- package/dist/skills/builtin/widget_gallery.js.map +1 -1
- package/dist/skills/frontmatterLoader.js +7 -1
- package/dist/skills/frontmatterLoader.js.map +1 -1
- package/dist/skills/registry.js +12 -0
- package/dist/skills/registry.js.map +1 -1
- package/dist/substrate/traceBus.js.map +1 -1
- package/dist/telemetry/traceStore.js +127 -0
- package/dist/telemetry/traceStore.js.map +1 -0
- package/dist/utils/constants.js +1 -1
- package/dist/utils/constants.js.map +1 -1
- package/dist/utils/contrast.js +69 -0
- package/dist/utils/contrast.js.map +1 -0
- package/dist/watch/sessionContext.js +14 -0
- package/dist/watch/sessionContext.js.map +1 -0
- package/dist/watch/sessionTrace.js +51 -0
- package/dist/watch/sessionTrace.js.map +1 -0
- package/dist/watch/studioBuffer.js +53 -0
- package/dist/watch/studioBuffer.js.map +1 -0
- package/package.json +4 -3
- package/ui/dist/assets/AboutYouMemory-CZKEOd_O.js +11 -0
- package/ui/dist/assets/AuditPanel-Bu71N5Er.js +1 -0
- package/ui/dist/assets/{AutonomyPanel-CmaXJV8d.js → AutonomyPanel-DB-39dX4.js} +2 -2
- package/ui/dist/assets/{AutopilotPanel-CbohPtXW.js → AutopilotPanel-Cj8oKxWv.js} +1 -1
- package/ui/dist/assets/{AutoresearchPanel-DCwiQ2v2.js → AutoresearchPanel-3s8pjm6Z.js} +2 -2
- package/ui/dist/assets/BackupPanel-EUQJMJE7.js +1 -0
- package/ui/dist/assets/{BrowserPanel-D6UaSxap.js → BrowserPanel-Beo4F5Ar.js} +2 -2
- package/ui/dist/assets/Button-MdXmb4_n.js +1 -0
- package/ui/dist/assets/CPActivity-B1Se9VVc.js +1 -0
- package/ui/dist/assets/CPAgentDetail-BYji6tT8.js +1 -0
- package/ui/dist/assets/CPAgents-BSUKucTe.js +1 -0
- package/ui/dist/assets/CPApprovals-B4v0em6e.js +1 -0
- package/ui/dist/assets/CPCosts-9ikh61Kv.js +1 -0
- package/ui/dist/assets/CPDashboard-sOok2T5v.js +1 -0
- package/ui/dist/assets/{CPFiles-BO73EZwJ.js → CPFiles-DB2nY5JE.js} +1 -1
- package/ui/dist/assets/CPGoals-8Z1yOY8e.js +1 -0
- package/ui/dist/assets/CPInbox-Cq6-UY8F.js +11 -0
- package/ui/dist/assets/CPIssueDetail-NAGfmJg8.js +1 -0
- package/ui/dist/assets/CPIssues-BzX6Rckx.js +1 -0
- package/ui/dist/assets/CPLayout-BcRM6QSg.js +2 -0
- package/ui/dist/assets/CPOrg-B7DQ4UQP.js +1 -0
- package/ui/dist/assets/CPRuns-D-cpPw_M.js +1 -0
- package/ui/dist/assets/{CPSocial-Qw5rKy_4.js → CPSocial-DshtmcK0.js} +3 -3
- package/ui/dist/assets/CPVoice-DfXRnQVU.js +1 -0
- package/ui/dist/assets/{ChannelsPanel-DS21E-Iz.js → ChannelsPanel-BnaTOeyf.js} +1 -1
- package/ui/dist/assets/CheckpointsPanel-C62naE4U.js +1 -0
- package/ui/dist/assets/CommandPostHub-B_L1CAss.js +24 -0
- package/ui/dist/assets/ConfirmDialog-VErHWAbo.js +1 -0
- package/ui/dist/assets/CronPanel-BwMrrZ5d.js +1 -0
- package/ui/dist/assets/{DataTable-PnhPeDBG.js → DataTable-BCqSUuN7.js} +1 -1
- package/ui/dist/assets/{DreamPanel-DIrtlk2K.js → DreamPanel-BNAcCLBB.js} +2 -2
- package/ui/dist/assets/EmptyState-D9kQf1PX.js +1 -0
- package/ui/dist/assets/{EvalHarnessPanel-uYb_ZEQG.js → EvalHarnessPanel-BaMLdBNm.js} +2 -2
- package/ui/dist/assets/EvalPanel-CW-SSjtg.js +1 -0
- package/ui/dist/assets/{FileViewer-CVO3TRs3.js → FileViewer-BL00nsd7.js} +1 -1
- package/ui/dist/assets/FilesPanel-CeC6YUSd.js +16 -0
- package/ui/dist/assets/FleetPanel-EyWF7l3W.js +1 -0
- package/ui/dist/assets/{HomelabPanel-BZvN4LPM.js → HomelabPanel-BRfKRYe7.js} +2 -2
- package/ui/dist/assets/InfraView-Jmqhg_Yc.js +2 -0
- package/ui/dist/assets/{InlineEditableField-mBzpbcLB.js → InlineEditableField-DgMrLDj1.js} +1 -1
- package/ui/dist/assets/Input-CBTwa_Iw.js +1 -0
- package/ui/dist/assets/IntegrationsPanel-DQL-7Ec6.js +1 -0
- package/ui/dist/assets/IntelligenceView-DxD78qfP.js +2 -0
- package/ui/dist/assets/LearningPanel-C_JzSbJU.js +1 -0
- package/ui/dist/assets/LiveStudio-Db5yeaWo.js +7 -0
- package/ui/dist/assets/{LogsPanel-DbP3Mwnr.js → LogsPanel-CpinKwaZ.js} +1 -1
- package/ui/dist/assets/McpPanel-D-MqN_f-.js +1 -0
- package/ui/dist/assets/MemoryGraphPanel-BnD5jRqf.js +1 -0
- package/ui/dist/assets/MemoryTaxonomyPanel-BBI202dd.js +6 -0
- package/ui/dist/assets/MemoryWikiPanel-Dn3XZHL0.js +6 -0
- package/ui/dist/assets/{MeshPanel-Bf-4brXL.js → MeshPanel-C-mRJpr0.js} +1 -1
- package/ui/dist/assets/{MissionCanvas-CJyhnauG.js → MissionCanvas-CbZW83Ch.js} +1 -1
- package/ui/dist/assets/{MissionChat-C2Nto3zM.js → MissionChat-Bs75jPAD.js} +1 -1
- package/ui/dist/assets/{MissionLibrary-RXyBHsLL.js → MissionLibrary-BxQ6SxPe.js} +1 -1
- package/ui/dist/assets/MissionStart-CBuBPasl.js +1 -0
- package/ui/dist/assets/Modal-ybPGvlQs.js +9 -0
- package/ui/dist/assets/{NvidiaPanel-4DlcQlyl.js → NvidiaPanel-DdG13OX4.js} +1 -1
- package/ui/dist/assets/OrganismPanel-xZWUJKyr.js +1 -0
- package/ui/dist/assets/OverviewPanel-B45ZYhJV.js +1 -0
- package/ui/dist/assets/{PageHeader-bnc85wQN.js → PageHeader-DdrbS1aq.js} +1 -1
- package/ui/dist/assets/PersonaProfilesPanel-CKER-EU5.js +8 -0
- package/ui/dist/assets/{PersonasPanel-dR8VppcD.js → PersonasPanel-CMMQ3nh3.js} +1 -1
- package/ui/dist/assets/{PhoneDeskPanel-CdVOY7H9.js → PhoneDeskPanel-Bw_Y-cVE.js} +2 -2
- package/ui/dist/assets/RecipesPanel-DzMcxpJJ.js +1 -0
- package/ui/dist/assets/SecurityPanel-BCq3Fzbr.js +1 -0
- package/ui/dist/assets/SelfImprovePanel-YE3hT8jK.js +1 -0
- package/ui/dist/assets/SelfProposalsPanel-DXuqBM6p.js +1 -0
- package/ui/dist/assets/SessionsPanel-DORz5goR.js +1 -0
- package/ui/dist/assets/SessionsTab-qYHDOuC8.js +1 -0
- package/ui/dist/assets/SettingsPanel-BbwPu1i7.js +1 -0
- package/ui/dist/assets/SettingsView-zgqkDDBJ.js +2 -0
- package/ui/dist/assets/{SkeletonLoader-Cax7YTqW.js → SkeletonLoader-r6OGXaU9.js} +1 -1
- package/ui/dist/assets/SkillsPanel-BjEOBMRo.js +11 -0
- package/ui/dist/assets/{SomaView-eG3wI4S1.js → SomaView-DLzApZVW.js} +2 -2
- package/ui/dist/assets/{StatCard-Djr487DV.js → StatCard-DLq8aBps.js} +1 -1
- package/ui/dist/assets/{StatusBadge-JpCoA33_.js → StatusBadge-BqlvIbdj.js} +1 -1
- package/ui/dist/assets/{Tabs-DJHu-roP.js → Tabs-DdUThlp1.js} +1 -1
- package/ui/dist/assets/TeamsPanel-uIIKoU7I.js +1 -0
- package/ui/dist/assets/{TelemetryPanel-D4PXsd-X.js → TelemetryPanel-dVr3yQQr.js} +1 -1
- package/ui/dist/assets/TitanCanvas-xHAJqwG2.js +945 -0
- package/ui/dist/assets/ToolsView-CG1mZdiy.js +2 -0
- package/ui/dist/assets/{Tooltip-B1LhTXJ8.js → Tooltip-C2tXJcrN.js} +1 -1
- package/ui/dist/assets/TraceViewer-BIMD_Osr.js +1 -0
- package/ui/dist/assets/TracesPanel-nMUiyMj5.js +1 -0
- package/ui/dist/assets/TrainingPanel-CT5R4HWm.js +1 -0
- package/ui/dist/assets/{VoiceOverlay-B_ss7Zg_.js → VoiceOverlay-Br4QnCtw.js} +3 -3
- package/ui/dist/assets/VramPanel-CdnJYytg.js +1 -0
- package/ui/dist/assets/WatchView-TNU6Ns1Q.js +13 -0
- package/ui/dist/assets/WorkTab-wTlX_UKP.js +1 -0
- package/ui/dist/assets/WorkflowBuilderPanel-CDpVt5ux.js +6 -0
- package/ui/dist/assets/WorkflowsPanel-zsFqsLdn.js +11 -0
- package/ui/dist/assets/{archive-gZFnGd5A.js → archive-Bh689DNg.js} +1 -1
- package/ui/dist/assets/{arrow-left-qA0Qer2h.js → arrow-left-WkVYLm80.js} +1 -1
- package/ui/dist/assets/{book-open-DIxdPqmD.js → book-open-ohEXgUsS.js} +1 -1
- package/ui/dist/assets/{bot-CvIF1uHi.js → bot-DvEcyQcD.js} +1 -1
- package/ui/dist/assets/{chart-column-BQiM-Td4.js → chart-column-w3goFd5f.js} +1 -1
- package/ui/dist/assets/{check-DgyDEFtq.js → check-CuRYh4Zh.js} +1 -1
- package/ui/dist/assets/{chevron-right-CQ1NTOtd.js → chevron-right-DbLSHmff.js} +1 -1
- package/ui/dist/assets/{circle-check-big-CAFcaPPH.js → circle-check-big-Bkba568b.js} +1 -1
- package/ui/dist/assets/{circle-stop-C-8xGv0n.js → circle-stop-Bx2ohEg8.js} +1 -1
- package/ui/dist/assets/{clipboard-list-9DdqBj1F.js → clipboard-list-Dj9tORnu.js} +1 -1
- package/ui/dist/assets/{clock-CF9BCIpL.js → clock-JyQAYy6X.js} +1 -1
- package/ui/dist/assets/{dollar-sign-LDRZBae6.js → dollar-sign-XxwPAA2a.js} +1 -1
- package/ui/dist/assets/{download-BBNXvBO0.js → download-D1izVEOL.js} +1 -1
- package/ui/dist/assets/{external-link-D_ji4Bec.js → external-link-DZ_rZsIt.js} +1 -1
- package/ui/dist/assets/{file-text-DrdTDEYp.js → file-text-Cfawu8Bs.js} +1 -1
- package/ui/dist/assets/{flask-conical-BGn5PgFX.js → flask-conical-Ql8QxhgY.js} +1 -1
- package/ui/dist/assets/{folder-BeZZUGnK.js → folder-DAdIw-Pb.js} +1 -1
- package/ui/dist/assets/{funnel-CAJgGO5M.js → funnel-FycfZcXo.js} +1 -1
- package/ui/dist/assets/{git-branch-BcXabeOs.js → git-branch-C2qrhGGu.js} +1 -1
- package/ui/dist/assets/index-Cml2x9pA.js +507 -0
- package/ui/dist/assets/index-yWybhcfI.css +1 -0
- package/ui/dist/assets/{layers-DxYDmPA_.js → layers-yzjtGlHB.js} +1 -1
- package/ui/dist/assets/{legacy-dHWI7k8P.js → legacy-D21WSMF1.js} +1 -1
- package/ui/dist/assets/{lightbulb-B0Du4SM4.js → lightbulb-BZJNK69S.js} +1 -1
- package/ui/dist/assets/{link-CUpaumqr.js → link-CnFkBkaY.js} +1 -1
- package/ui/dist/assets/{list-todo-CDDhmPQB.js → list-todo-BlgfQHup.js} +1 -1
- package/ui/dist/assets/{message-square-D5VWlv-Q.js → message-square-CRsG9u6_.js} +1 -1
- package/ui/dist/assets/{mic-Ddcc6Ann.js → mic-B32RHc7Y.js} +1 -1
- package/ui/dist/assets/{missions-2zox16Tc.js → missions-1iw8Z2_0.js} +1 -1
- package/ui/dist/assets/{network-DaJ7ugwX.js → network-C9RXQGrV.js} +1 -1
- package/ui/dist/assets/{pause-BukaK6rs.js → pause-BT1Bc2q3.js} +1 -1
- package/ui/dist/assets/{pencil-D_YwQBeg.js → pencil-BK5IU6o0.js} +1 -1
- package/ui/dist/assets/{play-BjdwtYMk.js → play-Cl1nC_gc.js} +1 -1
- package/ui/dist/assets/{plug-yISDdeW2.js → plug-DvcpOalj.js} +1 -1
- package/ui/dist/assets/{plus-BSjTdepW.js → plus-Dpo1qfrl.js} +1 -1
- package/ui/dist/assets/{radio-B7kX_yWE.js → radio-C5c9IyqX.js} +1 -1
- package/ui/dist/assets/{refresh-cw-CwDpzXUn.js → refresh-cw-C-iZkzuS.js} +1 -1
- package/ui/dist/assets/{rotate-ccw-7SQDTI0_.js → rotate-ccw-qzrB3SNw.js} +1 -1
- package/ui/dist/assets/{save-CjTPAQM0.js → save-DwR9wQVu.js} +1 -1
- package/ui/dist/assets/{search-BP7VlNxX.js → search-D10l0m3-.js} +1 -1
- package/ui/dist/assets/{send-C2XDuRPf.js → send-DtXGsaFW.js} +1 -1
- package/ui/dist/assets/{server-BE8W-AT4.js → server-BzisF3LL.js} +1 -1
- package/ui/dist/assets/settings-Dbwd1_-c.js +6 -0
- package/ui/dist/assets/{shield-check-s7la2J2K.js → shield-check-CXlDJyAW.js} +1 -1
- package/ui/dist/assets/square-BBC-JKzY.js +11 -0
- package/ui/dist/assets/{terminal-B8IK4WsO.js → terminal-DaBIlWEM.js} +1 -1
- package/ui/dist/assets/{test-tube-Xfn9DJeg.js → test-tube-CDATRD5t.js} +1 -1
- package/ui/dist/assets/{toggle-right-CHpzOIlj.js → toggle-right-DTmrC5RD.js} +1 -1
- package/ui/dist/assets/{trash-2-B1C85ePK.js → trash-2-CTeyjxD_.js} +1 -1
- package/ui/dist/assets/{trending-up-DDGHQZY1.js → trending-up-CXPRjRV4.js} +1 -1
- package/ui/dist/assets/{trophy-D0LhJSPA.js → trophy-DbVdXRtU.js} +1 -1
- package/ui/dist/assets/{volume-2-DMaE91b5.js → volume-2-DYDBmm3M.js} +1 -1
- package/ui/dist/assets/{wifi-off-DR9h8HGA.js → wifi-off-BBuIGbiP.js} +1 -1
- package/ui/dist/assets/zoom-out-D47_pRAO.js +16 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/sw.js +1 -1
- package/ui/dist/assets/AuditPanel-BaftX5MP.js +0 -1
- package/ui/dist/assets/BackupPanel-DAy9G7Wt.js +0 -1
- package/ui/dist/assets/CPActivity-BYDsUoMn.js +0 -1
- package/ui/dist/assets/CPAgentDetail-Bb9-GAJY.js +0 -1
- package/ui/dist/assets/CPAgents-BTZuGnki.js +0 -1
- package/ui/dist/assets/CPApprovals-BME2ARkO.js +0 -1
- package/ui/dist/assets/CPCosts-mOXwWq0i.js +0 -1
- package/ui/dist/assets/CPDashboard-CTlKGXXQ.js +0 -1
- package/ui/dist/assets/CPGoals-BNWhNP14.js +0 -1
- package/ui/dist/assets/CPInbox-ybe4r8LW.js +0 -16
- package/ui/dist/assets/CPIssueDetail-DaMMGbjw.js +0 -1
- package/ui/dist/assets/CPIssues-BgpQOISw.js +0 -1
- package/ui/dist/assets/CPLayout-bgErauTx.js +0 -2
- package/ui/dist/assets/CPOrg-C8l5SIWJ.js +0 -8
- package/ui/dist/assets/CPRuns-DB8S7loF.js +0 -1
- package/ui/dist/assets/CPVoice-DfwHPewA.js +0 -1
- package/ui/dist/assets/CheckpointsPanel-9-l_8GdG.js +0 -1
- package/ui/dist/assets/CommandPostHub-z55Yrhj1.js +0 -24
- package/ui/dist/assets/CronPanel-kEZK3gpx.js +0 -1
- package/ui/dist/assets/EmptyState-CfvU7SdB.js +0 -1
- package/ui/dist/assets/EvalPanel-CDG-njyj.js +0 -1
- package/ui/dist/assets/FilesPanel-CaLPog2e.js +0 -16
- package/ui/dist/assets/FleetPanel-BtlaV6pH.js +0 -1
- package/ui/dist/assets/InfraView-C0zEuFrR.js +0 -2
- package/ui/dist/assets/Input-CbrMbLgn.js +0 -1
- package/ui/dist/assets/IntegrationsPanel-B4TyR6jC.js +0 -1
- package/ui/dist/assets/IntelligenceView-DZ4HxYZ3.js +0 -2
- package/ui/dist/assets/LearningPanel-B4RIm_iQ.js +0 -1
- package/ui/dist/assets/McpPanel-BmNUFozc.js +0 -1
- package/ui/dist/assets/MemoryGraphPanel-JPurHV0L.js +0 -16
- package/ui/dist/assets/MemoryWikiPanel-DdvCAZHa.js +0 -6
- package/ui/dist/assets/MissionStart-wUkcEP0T.js +0 -1
- package/ui/dist/assets/Modal-BmS7C6FN.js +0 -1
- package/ui/dist/assets/OrganismPanel-C2LYDPrn.js +0 -1
- package/ui/dist/assets/OverviewPanel-DK8uuq4D.js +0 -1
- package/ui/dist/assets/PersonaProfilesPanel-DGtokXUu.js +0 -8
- package/ui/dist/assets/RecipesPanel-D3Zs2wxr.js +0 -1
- package/ui/dist/assets/SecurityPanel-BMERf796.js +0 -1
- package/ui/dist/assets/SelfImprovePanel-BxqWHXCU.js +0 -1
- package/ui/dist/assets/SelfProposalsPanel-CuC94vdx.js +0 -2
- package/ui/dist/assets/SessionsPanel-CQ8qvzp7.js +0 -1
- package/ui/dist/assets/SessionsTab-B3eHV3jf.js +0 -1
- package/ui/dist/assets/SettingsPanel-Cumls7N2.js +0 -1
- package/ui/dist/assets/SettingsView-BtA67pol.js +0 -2
- package/ui/dist/assets/SkillsPanel-CA415CT8.js +0 -11
- package/ui/dist/assets/TeamsPanel-C_Vu6vnY.js +0 -1
- package/ui/dist/assets/TitanCanvas-TuYZSZO7.js +0 -1117
- package/ui/dist/assets/ToolsView-BvTN6vFn.js +0 -2
- package/ui/dist/assets/TraceViewer-Bh8Jo3Dr.js +0 -1
- package/ui/dist/assets/TrainingPanel-Dmbd1rJ-.js +0 -1
- package/ui/dist/assets/VramPanel-CPICbZ2s.js +0 -1
- package/ui/dist/assets/WatchView-BCgLVfiV.js +0 -13
- package/ui/dist/assets/WorkTab-iptofXO9.js +0 -1
- package/ui/dist/assets/WorkflowsPanel-3RYkFHyf.js +0 -11
- package/ui/dist/assets/chevron-up-UyfdjRp6.js +0 -6
- package/ui/dist/assets/index-CUgepw5Q.js +0 -337
- package/ui/dist/assets/index-Dm6zcSTS.css +0 -1
- package/ui/dist/assets/proxy-CWET5FFX.js +0 -9
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { randomBytes } from "crypto";
|
|
5
|
+
import logger from "../utils/logger.js";
|
|
6
|
+
import { TITAN_HOME } from "../utils/constants.js";
|
|
7
|
+
import { getRecentTrajectories, getSequenceSignature } from "./trajectoryLogger.js";
|
|
8
|
+
import { registerWatcher } from "./daemon.js";
|
|
9
|
+
import { speakNote } from "./heartbeat.js";
|
|
10
|
+
import { saveRecipe, getRecipe } from "../recipes/store.js";
|
|
11
|
+
import { loadConfig } from "../config/config.js";
|
|
12
|
+
const COMPONENT = "MuscleMemory";
|
|
13
|
+
const STORE_PATH = join(TITAN_HOME, "muscle-memory.json");
|
|
14
|
+
const SCAN_INTERVAL_MS = 60 * 60 * 1e3;
|
|
15
|
+
const DRAFT_COOLDOWN_MS = 20 * 60 * 60 * 1e3;
|
|
16
|
+
const MAX_PENDING = 3;
|
|
17
|
+
const MIN_SEQUENCE_LEN = 2;
|
|
18
|
+
const EXAM_UNSAFE = /(^|_)(shell|exec|execute|run|spawn|kill|delete|remove|destroy|wipe|purge|clear|reset|send|post|reply|forward|publish|deploy|apply|restore|rollback|write|save|create|update|set|schedule|control|click|type|press|submit|upload|install|uninstall|pay|payment|purchase|buy|sell|transfer|trade|tweet|email|sms|call|notify|approve|merge|push|commit)(_|$)/i;
|
|
19
|
+
function isExamSafe(toolSequence) {
|
|
20
|
+
return toolSequence.every((t) => !EXAM_UNSAFE.test(t));
|
|
21
|
+
}
|
|
22
|
+
function readStore() {
|
|
23
|
+
try {
|
|
24
|
+
if (existsSync(STORE_PATH)) return JSON.parse(readFileSync(STORE_PATH, "utf-8"));
|
|
25
|
+
} catch (e) {
|
|
26
|
+
logger.warn(COMPONENT, `Store unreadable, starting fresh: ${e.message}`);
|
|
27
|
+
}
|
|
28
|
+
return { skills: [] };
|
|
29
|
+
}
|
|
30
|
+
function writeStore(store) {
|
|
31
|
+
mkdirSync(TITAN_HOME, { recursive: true });
|
|
32
|
+
const cutoff = Date.now() - 60 * 24 * 60 * 60 * 1e3;
|
|
33
|
+
store.skills = store.skills.filter((s) => s.status === "adopted" || s.status === "proposed" || new Date(s.createdAt).getTime() > cutoff);
|
|
34
|
+
writeFileSync(STORE_PATH, JSON.stringify(store, null, 2));
|
|
35
|
+
}
|
|
36
|
+
function listSkills() {
|
|
37
|
+
return readStore().skills.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
38
|
+
}
|
|
39
|
+
function medianOf(nums) {
|
|
40
|
+
if (nums.length === 0) return 0;
|
|
41
|
+
const s = [...nums].sort((a, b) => a - b);
|
|
42
|
+
const mid = Math.floor(s.length / 2);
|
|
43
|
+
return s.length % 2 ? s[mid] : Math.round((s[mid - 1] + s[mid]) / 2);
|
|
44
|
+
}
|
|
45
|
+
function mineCandidates(trajectories, knownSignatures, minOccurrences) {
|
|
46
|
+
const groups = /* @__PURE__ */ new Map();
|
|
47
|
+
for (const t of trajectories) {
|
|
48
|
+
if (!t.success || !Array.isArray(t.toolSequence) || t.toolSequence.length < MIN_SEQUENCE_LEN) continue;
|
|
49
|
+
if (!isExamSafe(t.toolSequence)) continue;
|
|
50
|
+
const signature = `${t.taskType}::${getSequenceSignature(t.toolSequence)}`;
|
|
51
|
+
if (knownSignatures.has(signature)) continue;
|
|
52
|
+
const g = groups.get(signature) || { taskType: t.taskType, signature, toolSequence: t.toolSequence, occurrences: [] };
|
|
53
|
+
g.occurrences.push(t);
|
|
54
|
+
groups.set(signature, g);
|
|
55
|
+
}
|
|
56
|
+
return [...groups.values()].filter((g) => g.occurrences.length >= minOccurrences).sort((a, b) => b.occurrences.length - a.occurrences.length);
|
|
57
|
+
}
|
|
58
|
+
function draftFallback(candidate) {
|
|
59
|
+
const newest = candidate.occurrences[0];
|
|
60
|
+
const slug = candidate.taskType.replace(/[^a-z0-9]+/gi, "-").toLowerCase().slice(0, 24) || "task";
|
|
61
|
+
return {
|
|
62
|
+
name: `Learned: ${candidate.taskType}`,
|
|
63
|
+
description: `Repeats your proven ${candidate.taskType} workflow (${candidate.toolSequence.join(" \u2192 ")}).`,
|
|
64
|
+
slashCommand: slug,
|
|
65
|
+
stepPrompt: `Repeat this proven workflow. Use the tools ${candidate.toolSequence.join(", ")} in that order, adapted to: {{request}}. A previous successful example of this task was: "${newest.task.slice(0, 200)}"`,
|
|
66
|
+
parameters: { request: { description: "What to apply the workflow to this time", required: false, default: "same as last time" } }
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async function draftSkill(candidate) {
|
|
70
|
+
try {
|
|
71
|
+
const { chat } = await import("../providers/router.js");
|
|
72
|
+
const examples = candidate.occurrences.slice(0, 3).map((t, i) => `${i + 1}. "${t.task.slice(0, 180)}" \u2192 ${t.toolSequence.join(" \u2192 ")}`).join("\n");
|
|
73
|
+
const argsSample = candidate.occurrences[0].toolDetails.map((d) => `${d.name}(${JSON.stringify(d.args).slice(0, 120)})`).join("; ");
|
|
74
|
+
const response = await chat({
|
|
75
|
+
messages: [{
|
|
76
|
+
role: "user",
|
|
77
|
+
content: `These ${candidate.occurrences.length} user requests were each solved successfully with the SAME tool workflow:
|
|
78
|
+
${examples}
|
|
79
|
+
|
|
80
|
+
Sample tool arguments: ${argsSample}
|
|
81
|
+
|
|
82
|
+
Write a reusable skill definition as STRICT JSON (no markdown fence):
|
|
83
|
+
{"name": "short friendly name", "description": "one sentence: what it does and when to use it", "slashCommand": "/kebab-case-verb", "stepPrompt": "an instruction to an AI agent that reproduces this workflow, using {{param}} placeholders for the parts that varied between the examples", "parameters": {"param": {"description": "...", "required": false, "default": "..."}}}
|
|
84
|
+
|
|
85
|
+
Rules: 0-2 parameters maximum; the stepPrompt MUST name the tools in order (${candidate.toolSequence.join(", ")}); keep everything concise.`
|
|
86
|
+
}]
|
|
87
|
+
});
|
|
88
|
+
const raw = (response.content || "").trim().replace(/^```(json)?/i, "").replace(/```$/, "").trim();
|
|
89
|
+
const parsed = JSON.parse(raw);
|
|
90
|
+
if (!parsed.name || !parsed.stepPrompt || !parsed.slashCommand) throw new Error("missing fields");
|
|
91
|
+
parsed.slashCommand = parsed.slashCommand.replace(/^\/+/, "").replace(/[^a-z0-9-]+/gi, "-").toLowerCase();
|
|
92
|
+
parsed.name = parsed.name.slice(0, 60);
|
|
93
|
+
parsed.description = (parsed.description || "").slice(0, 160);
|
|
94
|
+
parsed.parameters = parsed.parameters || {};
|
|
95
|
+
return parsed;
|
|
96
|
+
} catch (e) {
|
|
97
|
+
logger.warn(COMPONENT, `LLM draft failed (${e.message}) \u2014 using deterministic fallback`);
|
|
98
|
+
return draftFallback(candidate);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function examineDraft(candidate, draft) {
|
|
102
|
+
try {
|
|
103
|
+
const { runEval } = await import("../eval/harness.js");
|
|
104
|
+
const { processMessage } = await import("./agent.js");
|
|
105
|
+
const newest = candidate.occurrences[0];
|
|
106
|
+
const interpolated = draft.stepPrompt.replace(/\{\{(\w+)\}\}/g, (_, key) => draft.parameters[key]?.default || newest.task.slice(0, 200));
|
|
107
|
+
const result = await runEval(
|
|
108
|
+
{
|
|
109
|
+
name: `muscle-exam:${candidate.signature.slice(0, 40)}`,
|
|
110
|
+
input: interpolated,
|
|
111
|
+
expectedToolSequence: candidate.toolSequence,
|
|
112
|
+
timeoutMs: 18e4
|
|
113
|
+
},
|
|
114
|
+
async (input) => {
|
|
115
|
+
const r = await processMessage(input, "eval", `eval-muscle-${randomBytes(4).toString("hex")}`, {
|
|
116
|
+
allowedTools: [...new Set(candidate.toolSequence)]
|
|
117
|
+
});
|
|
118
|
+
return { content: r.content, toolsUsed: r.orderedToolSequence ?? r.toolsUsed };
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
return {
|
|
122
|
+
passed: result.passed,
|
|
123
|
+
detail: result.passed ? `Replay exam passed: reproduced ${candidate.toolSequence.join(" \u2192 ")}` : `Replay exam failed: ${result.errors.join("; ").slice(0, 300)}`
|
|
124
|
+
};
|
|
125
|
+
} catch (e) {
|
|
126
|
+
return { passed: false, detail: `Exam errored: ${e.message}` };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function muscleConfig() {
|
|
130
|
+
try {
|
|
131
|
+
const cfg = loadConfig();
|
|
132
|
+
return {
|
|
133
|
+
enabled: cfg.muscleMemory?.enabled ?? true,
|
|
134
|
+
minOccurrences: cfg.muscleMemory?.minOccurrences ?? 3,
|
|
135
|
+
verifyBeforePropose: cfg.muscleMemory?.verifyBeforePropose ?? true
|
|
136
|
+
};
|
|
137
|
+
} catch {
|
|
138
|
+
return { enabled: true, minOccurrences: 3, verifyBeforePropose: true };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function runMuscleScan(force = false) {
|
|
142
|
+
const cfg = muscleConfig();
|
|
143
|
+
if (!cfg.enabled) return { action: "disabled" };
|
|
144
|
+
const store = readStore();
|
|
145
|
+
const pending = store.skills.filter((s) => s.status === "proposed");
|
|
146
|
+
if (pending.length >= MAX_PENDING) return { action: "pending-cap" };
|
|
147
|
+
if (!force && store.lastDraftAt && Date.now() - new Date(store.lastDraftAt).getTime() < DRAFT_COOLDOWN_MS) {
|
|
148
|
+
return { action: "cooldown" };
|
|
149
|
+
}
|
|
150
|
+
const known = new Set(store.skills.map((s) => s.signature));
|
|
151
|
+
const trajectories = getRecentTrajectories(200, { success: true });
|
|
152
|
+
const candidates = mineCandidates(trajectories, known, cfg.minOccurrences);
|
|
153
|
+
if (candidates.length === 0) return { action: "nothing-to-learn" };
|
|
154
|
+
const candidate = candidates[0];
|
|
155
|
+
logger.info(COMPONENT, `Learning candidate: ${candidate.taskType} \xD7 ${candidate.occurrences.length} (${candidate.toolSequence.join(" \u2192 ")})`);
|
|
156
|
+
const draft = await draftSkill(candidate);
|
|
157
|
+
const exam = cfg.verifyBeforePropose ? await examineDraft(candidate, draft) : { passed: true, detail: "Exam skipped by config (verifyBeforePropose=false)" };
|
|
158
|
+
const skill = {
|
|
159
|
+
id: `mm-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`,
|
|
160
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
161
|
+
status: exam.passed ? "proposed" : "failed-exam",
|
|
162
|
+
name: draft.name,
|
|
163
|
+
description: draft.description,
|
|
164
|
+
slashCommand: draft.slashCommand,
|
|
165
|
+
stepPrompt: draft.stepPrompt,
|
|
166
|
+
parameters: draft.parameters,
|
|
167
|
+
taskType: candidate.taskType,
|
|
168
|
+
toolSequence: candidate.toolSequence,
|
|
169
|
+
signature: candidate.signature,
|
|
170
|
+
evidence: candidate.occurrences.slice(0, 5).map((t) => ({
|
|
171
|
+
trajectoryId: t.id,
|
|
172
|
+
task: t.task.slice(0, 160),
|
|
173
|
+
at: t.timestamp,
|
|
174
|
+
durationMs: t.durationMs,
|
|
175
|
+
rounds: t.rounds
|
|
176
|
+
})),
|
|
177
|
+
exam: { ...exam, examinedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
178
|
+
baselineDurationMs: medianOf(candidate.occurrences.map((t) => t.durationMs)),
|
|
179
|
+
baselineRounds: medianOf(candidate.occurrences.map((t) => t.rounds))
|
|
180
|
+
};
|
|
181
|
+
const fresh = readStore();
|
|
182
|
+
if (fresh.skills.some((x) => x.signature === skill.signature)) return { action: "duplicate-race" };
|
|
183
|
+
fresh.skills.push(skill);
|
|
184
|
+
fresh.lastDraftAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
185
|
+
writeStore(fresh);
|
|
186
|
+
if (skill.status === "proposed") {
|
|
187
|
+
speakNote(`\u{1F4AA} Check Muscle Memory: I taught myself "${skill.name}" from ${skill.evidence.length} of your tasks \u2014 replay exam passed.`);
|
|
188
|
+
logger.info(COMPONENT, `Proposed learned skill "${skill.name}" (${skill.slashCommand}) \u2014 exam PASSED`);
|
|
189
|
+
return { action: "proposed", skill };
|
|
190
|
+
}
|
|
191
|
+
logger.info(COMPONENT, `Draft "${skill.name}" failed its replay exam \u2014 filed, not proposed. ${exam.detail}`);
|
|
192
|
+
return { action: "failed-exam", skill };
|
|
193
|
+
}
|
|
194
|
+
function adoptSkill(id) {
|
|
195
|
+
const store = readStore();
|
|
196
|
+
const skill = store.skills.find((s) => s.id === id);
|
|
197
|
+
if (!skill) return { error: `No learned skill ${id}` };
|
|
198
|
+
if (skill.status !== "proposed") return { error: `Skill ${id} is ${skill.status}, not proposed` };
|
|
199
|
+
skill.slashCommand = skill.slashCommand.replace(/^\/+/, "");
|
|
200
|
+
if (getRecipe(skill.slashCommand)) skill.slashCommand = `${skill.slashCommand}-learned`;
|
|
201
|
+
const recipe = {
|
|
202
|
+
id: skill.id,
|
|
203
|
+
name: skill.name,
|
|
204
|
+
description: `${skill.description} (learned automatically from your usage; replay-verified)`,
|
|
205
|
+
slashCommand: skill.slashCommand,
|
|
206
|
+
parameters: skill.parameters,
|
|
207
|
+
steps: [{ prompt: skill.stepPrompt }],
|
|
208
|
+
author: "muscle-memory",
|
|
209
|
+
tags: ["learned"],
|
|
210
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
211
|
+
};
|
|
212
|
+
saveRecipe(recipe);
|
|
213
|
+
skill.status = "adopted";
|
|
214
|
+
skill.adoptedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
215
|
+
skill.runs = [];
|
|
216
|
+
writeStore(store);
|
|
217
|
+
speakNote(`\u{1F389} "${skill.name}" adopted \u2014 try /${skill.slashCommand} in any channel.`);
|
|
218
|
+
logger.info(COMPONENT, `Adopted "${skill.name}" as recipe ${skill.id} (${skill.slashCommand})`);
|
|
219
|
+
return skill;
|
|
220
|
+
}
|
|
221
|
+
function dismissSkill(id, reason) {
|
|
222
|
+
const store = readStore();
|
|
223
|
+
const skill = store.skills.find((s) => s.id === id);
|
|
224
|
+
if (!skill || skill.status !== "proposed") return false;
|
|
225
|
+
skill.status = "dismissed";
|
|
226
|
+
skill.resolvedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
227
|
+
writeStore(store);
|
|
228
|
+
logger.info(COMPONENT, `Dismissed "${skill.name}"${reason ? ` (${reason})` : ""} \u2014 signature remembered, won't re-propose`);
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
function recordAdoptedRun(recipeId, durationMs, totalTokens) {
|
|
232
|
+
try {
|
|
233
|
+
const store = readStore();
|
|
234
|
+
const skill = store.skills.find((s) => s.id === recipeId && s.status === "adopted");
|
|
235
|
+
if (!skill) return;
|
|
236
|
+
skill.runs = skill.runs || [];
|
|
237
|
+
skill.runs.push({ at: (/* @__PURE__ */ new Date()).toISOString(), durationMs, totalTokens });
|
|
238
|
+
if (skill.runs.length > 200) skill.runs = skill.runs.slice(-200);
|
|
239
|
+
writeStore(store);
|
|
240
|
+
} catch {
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function muscleStats() {
|
|
244
|
+
const skills = readStore().skills;
|
|
245
|
+
const adopted = skills.filter((s) => s.status === "adopted");
|
|
246
|
+
const totalRuns = adopted.reduce((n, s) => n + (s.runs?.length || 0), 0);
|
|
247
|
+
const speedups = [];
|
|
248
|
+
for (const s of adopted) {
|
|
249
|
+
if (!s.baselineDurationMs || !s.runs?.length) continue;
|
|
250
|
+
speedups.push(s.baselineDurationMs - medianOf(s.runs.map((r) => r.durationMs)));
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
learned: skills.filter((s) => s.status !== "failed-exam").length,
|
|
254
|
+
adopted: adopted.length,
|
|
255
|
+
totalRuns,
|
|
256
|
+
medianSpeedupMs: medianOf(speedups)
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function registerMuscleWatcher() {
|
|
260
|
+
const scan = async () => {
|
|
261
|
+
try {
|
|
262
|
+
await runMuscleScan();
|
|
263
|
+
} catch (e) {
|
|
264
|
+
logger.warn(COMPONENT, `Scan failed: ${e.message}`);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
registerWatcher("muscle-memory", scan, SCAN_INTERVAL_MS);
|
|
268
|
+
try {
|
|
269
|
+
const cfg = loadConfig();
|
|
270
|
+
if (cfg.daemon?.enabled !== true) {
|
|
271
|
+
setInterval(scan, SCAN_INTERVAL_MS).unref();
|
|
272
|
+
logger.info(COMPONENT, "Daemon disabled \u2014 Muscle Memory running on its own hourly timer");
|
|
273
|
+
}
|
|
274
|
+
} catch {
|
|
275
|
+
}
|
|
276
|
+
logger.info(COMPONENT, "Muscle Memory watcher registered (hourly scan, replay-verified proposals)");
|
|
277
|
+
}
|
|
278
|
+
export {
|
|
279
|
+
adoptSkill,
|
|
280
|
+
dismissSkill,
|
|
281
|
+
draftFallback,
|
|
282
|
+
isExamSafe,
|
|
283
|
+
listSkills,
|
|
284
|
+
medianOf,
|
|
285
|
+
mineCandidates,
|
|
286
|
+
muscleStats,
|
|
287
|
+
recordAdoptedRun,
|
|
288
|
+
registerMuscleWatcher,
|
|
289
|
+
runMuscleScan
|
|
290
|
+
};
|
|
291
|
+
//# sourceMappingURL=muscleMemory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/agent/muscleMemory.ts"],"sourcesContent":["/**\n * Muscle Memory — trustworthy automatic self-improvement (v7.0).\n *\n * The #1 gap reviewers cite across every agent framework (Jun-Jul 2026):\n * agents don't turn repeated workflows into skills by themselves, and when\n * they try (Hermes), users can't trust the self-evaluation (\"it always\n * thinks it did a good job\"). Muscle Memory closes both halves:\n *\n * MINE repeated successful tool-sequences from real task trajectories\n * DRAFT a parameterized recipe (+ slash command) with the LLM\n * EXAM replay the draft against the real historical request through the\n * deterministic eval harness — the skill must reproduce the same\n * tool path BEFORE a human ever sees it (no self-grading)\n * PROPOSE surface it: mascot announces, widget card shows evidence + exam\n * ADOPT one click → saved recipe + instant /slash-command everywhere\n *\n * Every proposal carries its evidence (which runs taught it) and its exam\n * result. Dismissals are remembered. Nothing auto-adopts.\n */\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { join } from 'path';\nimport { randomBytes } from 'crypto';\nimport logger from '../utils/logger.js';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport { getRecentTrajectories, getSequenceSignature, type TaskTrajectory } from './trajectoryLogger.js';\nimport { registerWatcher } from './daemon.js';\nimport { speakNote } from './heartbeat.js';\nimport { saveRecipe, getRecipe } from '../recipes/store.js';\nimport type { Recipe } from '../recipes/types.js';\nimport { loadConfig } from '../config/config.js';\n\nconst COMPONENT = 'MuscleMemory';\nconst STORE_PATH = join(TITAN_HOME, 'muscle-memory.json');\nconst SCAN_INTERVAL_MS = 60 * 60 * 1000; // hourly watcher\nconst DRAFT_COOLDOWN_MS = 20 * 60 * 60 * 1000; // at most one new draft per 20h\nconst MAX_PENDING = 3;\nconst MIN_SEQUENCE_LEN = 2;\n/** Sequences containing these are never mined: the replay exam runs tools for\n * REAL, and a learned workflow must never re-send, re-deploy, or re-delete\n * anything as a side effect of TITAN examining itself. */\n// Segment-based: TITAN tool names put the verb anywhere (email_send, x_post,\n// kb_delete, fb_delete_post) — match verb tokens between underscores, not a\n// name prefix. Over-blocking is fine; under-blocking is a real re-sent email.\nconst EXAM_UNSAFE = /(^|_)(shell|exec|execute|run|spawn|kill|delete|remove|destroy|wipe|purge|clear|reset|send|post|reply|forward|publish|deploy|apply|restore|rollback|write|save|create|update|set|schedule|control|click|type|press|submit|upload|install|uninstall|pay|payment|purchase|buy|sell|transfer|trade|tweet|email|sms|call|notify|approve|merge|push|commit)(_|$)/i;\nexport function isExamSafe(toolSequence: string[]): boolean {\n return toolSequence.every(t => !EXAM_UNSAFE.test(t));\n}\n\nexport interface LearnedSkill {\n id: string;\n createdAt: string;\n status: 'proposed' | 'adopted' | 'dismissed' | 'failed-exam';\n /** Human-facing */\n name: string;\n description: string;\n slashCommand: string;\n stepPrompt: string;\n parameters: Record<string, { description: string; required: boolean; default?: string }>;\n /** Provenance */\n taskType: string;\n toolSequence: string[];\n signature: string;\n evidence: Array<{ trajectoryId: string; task: string; at: string; durationMs: number; rounds: number }>;\n /** Replay exam */\n exam: { passed: boolean; detail: string; examinedAt: string } | null;\n /** Post-adoption ledger */\n adoptedAt?: string;\n runs?: Array<{ at: string; durationMs: number; totalTokens: number }>;\n baselineDurationMs?: number;\n baselineRounds?: number;\n resolvedAt?: string;\n}\n\ninterface MuscleStore { skills: LearnedSkill[]; lastDraftAt?: string }\n\nfunction readStore(): MuscleStore {\n try {\n if (existsSync(STORE_PATH)) return JSON.parse(readFileSync(STORE_PATH, 'utf-8')) as MuscleStore;\n } catch (e) {\n logger.warn(COMPONENT, `Store unreadable, starting fresh: ${(e as Error).message}`);\n }\n return { skills: [] };\n}\n\nfunction writeStore(store: MuscleStore): void {\n mkdirSync(TITAN_HOME, { recursive: true });\n // Bound the file: drop dismissed/failed older than 60 days\n const cutoff = Date.now() - 60 * 24 * 60 * 60 * 1000;\n store.skills = store.skills.filter(s =>\n s.status === 'adopted' || s.status === 'proposed' || new Date(s.createdAt).getTime() > cutoff);\n writeFileSync(STORE_PATH, JSON.stringify(store, null, 2));\n}\n\nexport function listSkills(): LearnedSkill[] {\n return readStore().skills.sort((a, b) => b.createdAt.localeCompare(a.createdAt));\n}\n\nexport function medianOf(nums: number[]): number {\n if (nums.length === 0) return 0;\n const s = [...nums].sort((a, b) => a - b);\n const mid = Math.floor(s.length / 2);\n return s.length % 2 ? s[mid] : Math.round((s[mid - 1] + s[mid]) / 2);\n}\n\nexport interface MinedCandidate {\n taskType: string;\n signature: string;\n toolSequence: string[];\n occurrences: TaskTrajectory[];\n}\n\n/**\n * Find repeated successful workflows worth learning. Pure — unit-testable.\n * Groups successful trajectories by (taskType, exact ordered tool sequence);\n * a group qualifies with >= minOccurrences and a sequence of >= 2 tools that\n * isn't already known (proposed/adopted/dismissed/failed).\n */\nexport function mineCandidates(\n trajectories: TaskTrajectory[],\n knownSignatures: Set<string>,\n minOccurrences: number,\n): MinedCandidate[] {\n const groups = new Map<string, MinedCandidate>();\n for (const t of trajectories) {\n if (!t.success || !Array.isArray(t.toolSequence) || t.toolSequence.length < MIN_SEQUENCE_LEN) continue;\n if (!isExamSafe(t.toolSequence)) continue;\n const signature = `${t.taskType}::${getSequenceSignature(t.toolSequence)}`;\n if (knownSignatures.has(signature)) continue;\n const g = groups.get(signature) || { taskType: t.taskType, signature, toolSequence: t.toolSequence, occurrences: [] };\n g.occurrences.push(t);\n groups.set(signature, g);\n }\n return [...groups.values()]\n .filter(g => g.occurrences.length >= minOccurrences)\n .sort((a, b) => b.occurrences.length - a.occurrences.length);\n}\n\ninterface DraftResult {\n name: string;\n description: string;\n slashCommand: string;\n stepPrompt: string;\n parameters: Record<string, { description: string; required: boolean; default?: string }>;\n}\n\n/** Deterministic fallback when the LLM draft fails. Exported for tests. */\nexport function draftFallback(candidate: MinedCandidate): DraftResult {\n const newest = candidate.occurrences[0];\n const slug = candidate.taskType.replace(/[^a-z0-9]+/gi, '-').toLowerCase().slice(0, 24) || 'task';\n return {\n name: `Learned: ${candidate.taskType}`,\n description: `Repeats your proven ${candidate.taskType} workflow (${candidate.toolSequence.join(' → ')}).`,\n slashCommand: slug,\n stepPrompt: `Repeat this proven workflow. Use the tools ${candidate.toolSequence.join(', ')} in that order, adapted to: {{request}}. A previous successful example of this task was: \"${newest.task.slice(0, 200)}\"`,\n parameters: { request: { description: 'What to apply the workflow to this time', required: false, default: 'same as last time' } },\n };\n}\n\n/** LLM-draft a reusable recipe from the mined evidence; falls back deterministically. */\nasync function draftSkill(candidate: MinedCandidate): Promise<DraftResult> {\n try {\n const { chat } = await import('../providers/router.js');\n const examples = candidate.occurrences.slice(0, 3).map((t, i) =>\n `${i + 1}. \"${t.task.slice(0, 180)}\" → ${t.toolSequence.join(' → ')}`).join('\\n');\n const argsSample = candidate.occurrences[0].toolDetails\n .map(d => `${d.name}(${JSON.stringify(d.args).slice(0, 120)})`).join('; ');\n const response = await chat({\n messages: [{\n role: 'user',\n content: `These ${candidate.occurrences.length} user requests were each solved successfully with the SAME tool workflow:\\n${examples}\\n\\nSample tool arguments: ${argsSample}\\n\\nWrite a reusable skill definition as STRICT JSON (no markdown fence):\\n{\"name\": \"short friendly name\", \"description\": \"one sentence: what it does and when to use it\", \"slashCommand\": \"/kebab-case-verb\", \"stepPrompt\": \"an instruction to an AI agent that reproduces this workflow, using {{param}} placeholders for the parts that varied between the examples\", \"parameters\": {\"param\": {\"description\": \"...\", \"required\": false, \"default\": \"...\"}}}\\n\\nRules: 0-2 parameters maximum; the stepPrompt MUST name the tools in order (${candidate.toolSequence.join(', ')}); keep everything concise.`,\n }],\n });\n const raw = (response.content || '').trim().replace(/^```(json)?/i, '').replace(/```$/, '').trim();\n const parsed = JSON.parse(raw) as DraftResult;\n if (!parsed.name || !parsed.stepPrompt || !parsed.slashCommand) throw new Error('missing fields');\n parsed.slashCommand = parsed.slashCommand.replace(/^\\/+/, '').replace(/[^a-z0-9-]+/gi, '-').toLowerCase();\n parsed.name = parsed.name.slice(0, 60);\n parsed.description = (parsed.description || '').slice(0, 160);\n parsed.parameters = parsed.parameters || {};\n return parsed;\n } catch (e) {\n logger.warn(COMPONENT, `LLM draft failed (${(e as Error).message}) — using deterministic fallback`);\n return draftFallback(candidate);\n }\n}\n\n/**\n * The replay exam — the trust layer. The drafted skill's prompt is run\n * through the REAL agent (isolated eval user) against the most recent\n * historical request, and must reproduce the mined tool sequence\n * (ordered-subsequence check via the deterministic eval harness).\n */\nasync function examineDraft(candidate: MinedCandidate, draft: DraftResult): Promise<{ passed: boolean; detail: string }> {\n try {\n const { runEval } = await import('../eval/harness.js');\n const { processMessage } = await import('./agent.js');\n const newest = candidate.occurrences[0];\n const interpolated = draft.stepPrompt.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key: string) =>\n draft.parameters[key]?.default || newest.task.slice(0, 200));\n const result = await runEval(\n {\n name: `muscle-exam:${candidate.signature.slice(0, 40)}`,\n input: interpolated,\n expectedToolSequence: candidate.toolSequence,\n timeoutMs: 180_000,\n },\n async (input: string) => {\n // Sandbox: the exam binds EXACTLY the mined tools — it cannot\n // execute anything the learned workflow doesn't contain.\n const r = await processMessage(input, 'eval', `eval-muscle-${randomBytes(4).toString('hex')}`, {\n allowedTools: [...new Set(candidate.toolSequence)],\n });\n // Use the repeats-preserving order (toolsUsed is deduplicated,\n // which would fail sequences like search → browse → search).\n return { content: r.content, toolsUsed: r.orderedToolSequence ?? r.toolsUsed };\n },\n );\n return {\n passed: result.passed,\n detail: result.passed\n ? `Replay exam passed: reproduced ${candidate.toolSequence.join(' → ')}`\n : `Replay exam failed: ${result.errors.join('; ').slice(0, 300)}`,\n };\n } catch (e) {\n return { passed: false, detail: `Exam errored: ${(e as Error).message}` };\n }\n}\n\nexport interface MuscleConfig { enabled: boolean; minOccurrences: number; verifyBeforePropose: boolean }\n\nfunction muscleConfig(): MuscleConfig {\n try {\n const cfg = loadConfig() as { muscleMemory?: Partial<MuscleConfig> };\n return {\n enabled: cfg.muscleMemory?.enabled ?? true,\n minOccurrences: cfg.muscleMemory?.minOccurrences ?? 3,\n verifyBeforePropose: cfg.muscleMemory?.verifyBeforePropose ?? true,\n };\n } catch {\n return { enabled: true, minOccurrences: 3, verifyBeforePropose: true };\n }\n}\n\n/**\n * One scan: mine → draft → exam → propose. Returns what happened (for the\n * run-now endpoint / demos). Guards: cooldown, pending cap.\n */\nexport async function runMuscleScan(force = false): Promise<{ action: string; skill?: LearnedSkill }> {\n const cfg = muscleConfig();\n if (!cfg.enabled) return { action: 'disabled' };\n\n const store = readStore();\n const pending = store.skills.filter(s => s.status === 'proposed');\n if (pending.length >= MAX_PENDING) return { action: 'pending-cap' };\n if (!force && store.lastDraftAt && Date.now() - new Date(store.lastDraftAt).getTime() < DRAFT_COOLDOWN_MS) {\n return { action: 'cooldown' };\n }\n\n const known = new Set(store.skills.map(s => s.signature));\n const trajectories = getRecentTrajectories(200, { success: true });\n const candidates = mineCandidates(trajectories, known, cfg.minOccurrences);\n if (candidates.length === 0) return { action: 'nothing-to-learn' };\n\n const candidate = candidates[0];\n logger.info(COMPONENT, `Learning candidate: ${candidate.taskType} × ${candidate.occurrences.length} (${candidate.toolSequence.join(' → ')})`);\n\n const draft = await draftSkill(candidate);\n const exam = cfg.verifyBeforePropose\n ? await examineDraft(candidate, draft)\n : { passed: true, detail: 'Exam skipped by config (verifyBeforePropose=false)' };\n\n const skill: LearnedSkill = {\n id: `mm-${Date.now().toString(36)}-${randomBytes(3).toString('hex')}`,\n createdAt: new Date().toISOString(),\n status: exam.passed ? 'proposed' : 'failed-exam',\n name: draft.name,\n description: draft.description,\n slashCommand: draft.slashCommand,\n stepPrompt: draft.stepPrompt,\n parameters: draft.parameters,\n taskType: candidate.taskType,\n toolSequence: candidate.toolSequence,\n signature: candidate.signature,\n evidence: candidate.occurrences.slice(0, 5).map(t => ({\n trajectoryId: t.id, task: t.task.slice(0, 160), at: t.timestamp, durationMs: t.durationMs, rounds: t.rounds,\n })),\n exam: { ...exam, examinedAt: new Date().toISOString() },\n baselineDurationMs: medianOf(candidate.occurrences.map(t => t.durationMs)),\n baselineRounds: medianOf(candidate.occurrences.map(t => t.rounds)),\n };\n // Re-read fresh: the draft+exam awaits can take minutes, during which the\n // user may adopt/dismiss other skills — never clobber those writes.\n const fresh = readStore();\n if (fresh.skills.some(x => x.signature === skill.signature)) return { action: 'duplicate-race' };\n fresh.skills.push(skill);\n fresh.lastDraftAt = new Date().toISOString();\n writeStore(fresh);\n\n if (skill.status === 'proposed') {\n speakNote(`💪 Check Muscle Memory: I taught myself \"${skill.name}\" from ${skill.evidence.length} of your tasks — replay exam passed.`);\n logger.info(COMPONENT, `Proposed learned skill \"${skill.name}\" (${skill.slashCommand}) — exam PASSED`);\n return { action: 'proposed', skill };\n }\n logger.info(COMPONENT, `Draft \"${skill.name}\" failed its replay exam — filed, not proposed. ${exam.detail}`);\n return { action: 'failed-exam', skill };\n}\n\n/** Adopt: persist as a real recipe → instant slash command. */\nexport function adoptSkill(id: string): LearnedSkill | { error: string } {\n const store = readStore();\n const skill = store.skills.find(s => s.id === id);\n if (!skill) return { error: `No learned skill ${id}` };\n if (skill.status !== 'proposed') return { error: `Skill ${id} is ${skill.status}, not proposed` };\n skill.slashCommand = skill.slashCommand.replace(/^\\/+/, '');\n if (getRecipe(skill.slashCommand)) skill.slashCommand = `${skill.slashCommand}-learned`;\n\n const recipe: Recipe = {\n id: skill.id,\n name: skill.name,\n description: `${skill.description} (learned automatically from your usage; replay-verified)`,\n slashCommand: skill.slashCommand,\n parameters: skill.parameters,\n steps: [{ prompt: skill.stepPrompt }],\n author: 'muscle-memory',\n tags: ['learned'],\n createdAt: new Date().toISOString(),\n };\n saveRecipe(recipe);\n skill.status = 'adopted';\n skill.adoptedAt = new Date().toISOString();\n skill.runs = [];\n writeStore(store);\n speakNote(`🎉 \"${skill.name}\" adopted — try /${skill.slashCommand} in any channel.`);\n logger.info(COMPONENT, `Adopted \"${skill.name}\" as recipe ${skill.id} (${skill.slashCommand})`);\n return skill;\n}\n\nexport function dismissSkill(id: string, reason?: string): boolean {\n const store = readStore();\n const skill = store.skills.find(s => s.id === id);\n if (!skill || skill.status !== 'proposed') return false;\n skill.status = 'dismissed';\n skill.resolvedAt = new Date().toISOString();\n writeStore(store);\n logger.info(COMPONENT, `Dismissed \"${skill.name}\"${reason ? ` (${reason})` : ''} — signature remembered, won't re-propose`);\n return true;\n}\n\n/** Called from the slash-command run path to feed the savings ledger. */\nexport function recordAdoptedRun(recipeId: string, durationMs: number, totalTokens: number): void {\n try {\n const store = readStore();\n const skill = store.skills.find(s => s.id === recipeId && s.status === 'adopted');\n if (!skill) return;\n skill.runs = skill.runs || [];\n skill.runs.push({ at: new Date().toISOString(), durationMs, totalTokens });\n if (skill.runs.length > 200) skill.runs = skill.runs.slice(-200);\n writeStore(store);\n } catch { /* ledger is best-effort */ }\n}\n\n/** Aggregate stats for the dashboard widget. */\nexport function muscleStats(): { learned: number; adopted: number; totalRuns: number; medianSpeedupMs: number } {\n const skills = readStore().skills;\n const adopted = skills.filter(s => s.status === 'adopted');\n const totalRuns = adopted.reduce((n, s) => n + (s.runs?.length || 0), 0);\n const speedups: number[] = [];\n for (const s of adopted) {\n if (!s.baselineDurationMs || !s.runs?.length) continue;\n speedups.push(s.baselineDurationMs - medianOf(s.runs.map(r => r.durationMs)));\n }\n return {\n learned: skills.filter(s => s.status !== 'failed-exam').length,\n adopted: adopted.length,\n totalRuns,\n medianSpeedupMs: medianOf(speedups),\n };\n}\n\nexport function registerMuscleWatcher(): void {\n const scan = async () => {\n try {\n await runMuscleScan();\n } catch (e) {\n logger.warn(COMPONENT, `Scan failed: ${(e as Error).message}`);\n }\n };\n registerWatcher('muscle-memory', scan, SCAN_INTERVAL_MS);\n // The daemon is off by default — Muscle Memory has its own enabled flag,\n // so fall back to a self-owned timer when the daemon never starts.\n try {\n const cfg = loadConfig() as { daemon?: { enabled?: boolean } };\n if (cfg.daemon?.enabled !== true) {\n setInterval(scan, SCAN_INTERVAL_MS).unref();\n logger.info(COMPONENT, 'Daemon disabled — Muscle Memory running on its own hourly timer');\n }\n } catch { /* daemon config unreadable — keep the registerWatcher path */ }\n logger.info(COMPONENT, 'Muscle Memory watcher registered (hourly scan, replay-verified proposals)');\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import logger from "../utils/logger.js";
|
|
3
|
+
const COMPONENT = "ObservationWriter";
|
|
4
|
+
const THROTTLE_MS = Number(process.env.TITAN_OBSERVE_MS) || 10 * 60 * 1e3;
|
|
5
|
+
const MIN_MESSAGE_CHARS = 30;
|
|
6
|
+
const SKIP_CHANNELS = /^(eval|deliberation|autopilot|initiative|agent-|company-|swarm|subagent|plan-|sandbox|widget-assist|mission-decomposer|heartbeat)/;
|
|
7
|
+
let lastRunAt = 0;
|
|
8
|
+
function resetObservationThrottle() {
|
|
9
|
+
lastRunAt = 0;
|
|
10
|
+
}
|
|
11
|
+
function shouldObserve(channel, userMessage, now = Date.now()) {
|
|
12
|
+
if (SKIP_CHANNELS.test(channel)) return false;
|
|
13
|
+
if ((userMessage || "").trim().length < MIN_MESSAGE_CHARS) return false;
|
|
14
|
+
if (now - lastRunAt < THROTTLE_MS) return false;
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
function maybeObserve(channel, userMessage, reply) {
|
|
18
|
+
if (!shouldObserve(channel, userMessage)) return;
|
|
19
|
+
lastRunAt = Date.now();
|
|
20
|
+
void (async () => {
|
|
21
|
+
try {
|
|
22
|
+
const { spawnSubAgent } = await import("./subAgent.js");
|
|
23
|
+
const result = await spawnSubAgent({
|
|
24
|
+
name: "observe-user",
|
|
25
|
+
task: [
|
|
26
|
+
'You quietly maintain a profile of the human you work with. From this exchange, extract AT MOST ONE durable observation about the PERSON \u2014 a stable preference, interest, constraint, or fact about them (e.g. "Prefers replies short and direct", "Produces music and DJs", "Works on a homelab with a 5090").',
|
|
27
|
+
"",
|
|
28
|
+
`THEY SAID: ${userMessage.slice(0, 800)}`,
|
|
29
|
+
`YOU REPLIED (for context only): ${reply.slice(0, 400)}`,
|
|
30
|
+
"",
|
|
31
|
+
"RULES:",
|
|
32
|
+
"- It must be durable \u2014 true next month, not about this one task. Task details, one-off requests, or anything you are unsure of: reply exactly NONE.",
|
|
33
|
+
"- Never store sensitive categories (health, politics, religion, finances).",
|
|
34
|
+
"- Reply with ONLY the observation sentence (max 120 chars), or NONE. No quotes, no markdown, no explanation."
|
|
35
|
+
].join("\n"),
|
|
36
|
+
tier: "fast",
|
|
37
|
+
maxRounds: 1
|
|
38
|
+
});
|
|
39
|
+
const text = (result.content || "").trim();
|
|
40
|
+
if (!text || /^NONE\b/i.test(text) || text.length > 160) return;
|
|
41
|
+
const { appendObservation, readSomaProfile } = await import("../storage/somaProfile.js");
|
|
42
|
+
const existing = readSomaProfile("default-user").learnedAboutUser;
|
|
43
|
+
const norm = (s) => s.toLowerCase().replace(/[^a-z0-9 ]/g, "").slice(0, 60);
|
|
44
|
+
if (existing.some((o) => norm(o) === norm(text))) return;
|
|
45
|
+
appendObservation("default-user", text);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
logger.debug(COMPONENT, `Observation pass skipped: ${err.message}`);
|
|
48
|
+
}
|
|
49
|
+
})();
|
|
50
|
+
}
|
|
51
|
+
export {
|
|
52
|
+
maybeObserve,
|
|
53
|
+
resetObservationThrottle,
|
|
54
|
+
shouldObserve
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=observationWriter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/agent/observationWriter.ts"],"sourcesContent":["/**\n * Observation Writer — proactive memory (v7.0 life loop, Hermes move).\n *\n * After a real user conversation turn, TITAN occasionally asks itself one\n * cheap question: \"did I just learn something durable about this person?\"\n * If yes, the observation lands in the Soma profile — the same store the\n * system prompt reads (renderSomaProfileForPrompt) and the \"What I know\n * about you\" page shows. This closes the loop the Soma audit flagged:\n * appendObservation existed with ZERO callers, so the agent never learned.\n *\n * Deliberately frugal + safe:\n * - throttled to one extraction per THROTTLE_MS (default 10 min)\n * - only real user channels (never eval/deliberation/internal)\n * - only substantive messages\n * - fire-and-forget — never delays or fails the user's reply\n * - strict prompt: durable facts/preferences about the PERSON, else NONE\n */\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'ObservationWriter';\nconst THROTTLE_MS = Number(process.env.TITAN_OBSERVE_MS) || 10 * 60 * 1000;\nconst MIN_MESSAGE_CHARS = 30;\n\n/** Channels that are never a real human talking to their agent. */\nconst SKIP_CHANNELS = /^(eval|deliberation|autopilot|initiative|agent-|company-|swarm|subagent|plan-|sandbox|widget-assist|mission-decomposer|heartbeat)/;\n\nlet lastRunAt = 0;\n\n/** Test hook. */\nexport function resetObservationThrottle(): void { lastRunAt = 0; }\n\n/** Pure gate — should this turn even be considered? Exported for tests. */\nexport function shouldObserve(channel: string, userMessage: string, now: number = Date.now()): boolean {\n if (SKIP_CHANNELS.test(channel)) return false;\n if ((userMessage || '').trim().length < MIN_MESSAGE_CHARS) return false;\n if (now - lastRunAt < THROTTLE_MS) return false;\n return true;\n}\n\n/**\n * Fire-and-forget: maybe extract ONE durable observation about the user from\n * this exchange and append it to the Soma profile ('default-user' — the same\n * id the prompt-injection reads; thread real ids when multi-user lands).\n */\nexport function maybeObserve(channel: string, userMessage: string, reply: string): void {\n if (!shouldObserve(channel, userMessage)) return;\n lastRunAt = Date.now();\n\n void (async () => {\n try {\n const { spawnSubAgent } = await import('./subAgent.js');\n const result = await spawnSubAgent({\n name: 'observe-user',\n task: [\n 'You quietly maintain a profile of the human you work with. From this exchange, extract AT MOST ONE durable observation about the PERSON — a stable preference, interest, constraint, or fact about them (e.g. \"Prefers replies short and direct\", \"Produces music and DJs\", \"Works on a homelab with a 5090\").',\n '',\n `THEY SAID: ${userMessage.slice(0, 800)}`,\n `YOU REPLIED (for context only): ${reply.slice(0, 400)}`,\n '',\n 'RULES:',\n '- It must be durable — true next month, not about this one task. Task details, one-off requests, or anything you are unsure of: reply exactly NONE.',\n '- Never store sensitive categories (health, politics, religion, finances).',\n '- Reply with ONLY the observation sentence (max 120 chars), or NONE. No quotes, no markdown, no explanation.',\n ].join('\\n'),\n tier: 'fast',\n maxRounds: 1,\n });\n const text = (result.content || '').trim();\n if (!text || /^NONE\\b/i.test(text) || text.length > 160) return;\n const { appendObservation, readSomaProfile } = await import('../storage/somaProfile.js');\n // Cheap dedupe — skip if we already know something nearly identical.\n const existing = readSomaProfile('default-user').learnedAboutUser;\n const norm = (s: string) => s.toLowerCase().replace(/[^a-z0-9 ]/g, '').slice(0, 60);\n if (existing.some(o => norm(o) === norm(text))) return;\n appendObservation('default-user', text);\n } catch (err) {\n logger.debug(COMPONENT, `Observation pass skipped: ${(err as Error).message}`);\n }\n })();\n}\n"],"mappings":";AAiBA,OAAO,YAAY;AAEnB,MAAM,YAAY;AAClB,MAAM,cAAc,OAAO,QAAQ,IAAI,gBAAgB,KAAK,KAAK,KAAK;AACtE,MAAM,oBAAoB;AAG1B,MAAM,gBAAgB;AAEtB,IAAI,YAAY;AAGT,SAAS,2BAAiC;AAAE,cAAY;AAAG;AAG3D,SAAS,cAAc,SAAiB,aAAqB,MAAc,KAAK,IAAI,GAAY;AACnG,MAAI,cAAc,KAAK,OAAO,EAAG,QAAO;AACxC,OAAK,eAAe,IAAI,KAAK,EAAE,SAAS,kBAAmB,QAAO;AAClE,MAAI,MAAM,YAAY,YAAa,QAAO;AAC1C,SAAO;AACX;AAOO,SAAS,aAAa,SAAiB,aAAqB,OAAqB;AACpF,MAAI,CAAC,cAAc,SAAS,WAAW,EAAG;AAC1C,cAAY,KAAK,IAAI;AAErB,QAAM,YAAY;AACd,QAAI;AACA,YAAM,EAAE,cAAc,IAAI,MAAM,OAAO,eAAe;AACtD,YAAM,SAAS,MAAM,cAAc;AAAA,QAC/B,MAAM;AAAA,QACN,MAAM;AAAA,UACF;AAAA,UACA;AAAA,UACA,cAAc,YAAY,MAAM,GAAG,GAAG,CAAC;AAAA,UACvC,mCAAmC,MAAM,MAAM,GAAG,GAAG,CAAC;AAAA,UACtD;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACJ,EAAE,KAAK,IAAI;AAAA,QACX,MAAM;AAAA,QACN,WAAW;AAAA,MACf,CAAC;AACD,YAAM,QAAQ,OAAO,WAAW,IAAI,KAAK;AACzC,UAAI,CAAC,QAAQ,WAAW,KAAK,IAAI,KAAK,KAAK,SAAS,IAAK;AACzD,YAAM,EAAE,mBAAmB,gBAAgB,IAAI,MAAM,OAAO,2BAA2B;AAEvF,YAAM,WAAW,gBAAgB,cAAc,EAAE;AACjD,YAAM,OAAO,CAAC,MAAc,EAAE,YAAY,EAAE,QAAQ,eAAe,EAAE,EAAE,MAAM,GAAG,EAAE;AAClF,UAAI,SAAS,KAAK,OAAK,KAAK,CAAC,MAAM,KAAK,IAAI,CAAC,EAAG;AAChD,wBAAkB,gBAAgB,IAAI;AAAA,IAC1C,SAAS,KAAK;AACV,aAAO,MAAM,WAAW,6BAA8B,IAAc,OAAO,EAAE;AAAA,IACjF;AAAA,EACJ,GAAG;AACP;","names":[]}
|
|
@@ -47,6 +47,12 @@ const SECTION_CAP_DEFAULTS = Object.freeze({
|
|
|
47
47
|
// 4 KB matches the renderer's PROMPT_RENDER_CAP — scratchpad gets
|
|
48
48
|
// trimmed first if the steps list alone is big.
|
|
49
49
|
goalPlan: 4096,
|
|
50
|
+
// v7.0 — Living goal SPEC (spec-driven / context engineering). The
|
|
51
|
+
// definition-of-done (problem + requirements + checkable acceptance
|
|
52
|
+
// criteria) for the goal this session is tied to. Lands just before
|
|
53
|
+
// goalPlan so the agent sees "are we done?" before "what's next?".
|
|
54
|
+
// 4 KB matches the renderer's PROMPT_RENDER_CAP.
|
|
55
|
+
goalSpec: 4096,
|
|
50
56
|
// v6.0.5 — User-authored / agent-authored SKILL.md library. The
|
|
51
57
|
// renderer caps itself at SKILL_PROMPT_CAP (6 KB) and skips body
|
|
52
58
|
// injection past that, so this cap mostly matters as a defence-in-
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/agent/promptSectionCaps.ts"],"sourcesContent":["/**\n * TITAN — System-prompt section caps.\n *\n * The Persona injection cap (`src/personas/manager.ts`) already proves that\n * section-aware truncation works without breaking semantics — TITAN_PERSONA_CAP\n * has been live since v5.0. This module extends the same idea to every dynamic\n * section in `buildSystemPrompt`, so when memory/hindsight/wisdom/skill hints\n * bloat over time the prompt doesn't silently grow to 30 KB.\n *\n * Each cap is overridable via env (TITAN_CAP_<SECTION>) for power users; the\n * defaults are conservative and grounded in the audit in docs/HARNESS-PATTERNS.md.\n *\n * Reference:\n * - Anthropic — \"Effective context engineering for AI agents\" (context-as-bottleneck)\n * - 12 Factor Agents §3 \"Own your context window\"\n * - awesome-agent-harness top-10 pattern #3 \"Context Compaction & Working-State Management\"\n * - Hermes Agent's per-block size budget (procedural-memory layer)\n *\n * Caps are applied at *injection time* — the source data (memory store,\n * hindsight, wisdom) is never truncated. Only what reaches the LLM is bounded.\n */\n\n/** Default cap (bytes) per section. 0 = no cap. */\nexport const SECTION_CAP_DEFAULTS: Readonly<Record<string, number>> = Object.freeze({\n // Identity-y things — should be small by construction; cap defensively.\n identityBlocks: 2048,\n dateTimeBlock: 512,\n\n // Continuous-learning hints — the historical 5-in-1 grab-bag that\n // tends to grow because each sub-hint accumulates over a session.\n learningBlock: 3072,\n\n // Frustration / teaching / custom — short by construction.\n frustrationBlock: 512,\n teachingBlock: 1024,\n customBlock: 4096,\n\n // Workspace context — combines TITAN.md / AGENTS.md / SOUL.md / persona /\n // TOOLS.md. Persona already has its own cap; this one bounds the union.\n // Big users put project rules here, so cap is generous.\n workspaceContext: 8192,\n\n // Memory-related — these grow as the user accumulates knowledge.\n memoryContext: 2048,\n personalContext: 1024,\n graphSection: 4096,\n memoryToolsBlock: 768,\n\n // Self-awareness + optimized-prompt blocks — should stay small.\n selfAwareness: 2048,\n optimizedBlock: 1024,\n\n // Prompt-includes — user-controlled; reasonable upper bound.\n systemIncludes: 4096,\n transientIncludes: 2048,\n\n // v6.0 step 5 — Active Space intent. Lands at the recency position\n // (U-shaped attention) so the model uses it. Generous cap so users\n // can write a few paragraphs of Space-specific instructions.\n activeSpace: 4096,\n\n // v6.0 step 14 — Per-user Soma profile (long-term baseline + recent\n // observations). Grows slowly over weeks of use. 4 KB lets the model\n // see ~50 short observations.\n somaProfile: 4096,\n\n // v6.0 \"Living Canvas\" voice — TITAN's character block. Lands FIRST in\n // the dynamic sections so the LLM primes on it. Generous cap so the\n // posture rules + drive-interpretation rules both fit.\n livingCanvasVoice: 4096,\n\n // v6.0 Command Post upgrade #1 — Per-agent lessons (Reflexion).\n // Top-N lessons the agent has learned. Cap at 3 KB — twelve concise\n // entries fit comfortably; older lessons roll off via the lessons.md\n // file-level cap (200) so this prompt section never exceeds the cap.\n agentLessons: 3072,\n\n // v6.0 Command Post upgrade #2 — Living goal plan (Manus recitation).\n // The current plan.md rendered for the goal this session is tied to.\n // 4 KB matches the renderer's PROMPT_RENDER_CAP — scratchpad gets\n // trimmed first if the steps list alone is big.\n goalPlan: 4096,\n\n // v6.0.5 — User-authored / agent-authored SKILL.md library. The\n // renderer caps itself at SKILL_PROMPT_CAP (6 KB) and skips body\n // injection past that, so this cap mostly matters as a defence-in-\n // depth fallback if the catalog of lazy skills grows huge.\n userSkills: 8192,\n});\n\n/** Resolve a section's cap from env override or default. Returns 0 if disabled. */\nexport function getSectionCap(name: string): number {\n const envKey = 'TITAN_CAP_' + name.toUpperCase();\n const raw = process.env[envKey];\n if (raw !== undefined) {\n const n = parseInt(raw, 10);\n if (Number.isFinite(n) && n >= 0) return n;\n }\n return SECTION_CAP_DEFAULTS[name] ?? 0;\n}\n\n/**\n * Truncate a section to the cap, preferring a markdown header in the final\n * quarter for a clean cut (same heuristic as persona truncation). Appends a\n * truncated-marker so the model can SEE that content was clipped — important\n * to prevent \"I didn't see Y in your instructions\" confusion.\n *\n * Returns the input unchanged when:\n * - cap is 0 (disabled)\n * - content is already within the cap\n * - content is empty / whitespace\n */\nexport function capSection(name: string, content: string): string {\n if (!content || !content.trim()) return content;\n const cap = getSectionCap(name);\n if (cap === 0 || content.length <= cap) return content;\n\n const slice = content.slice(0, cap);\n const lookback = Math.floor(cap * 0.75);\n const tail = slice.slice(lookback);\n const headerMatch = tail.match(/\\n(#{1,6} [^\\n]+)\\n[\\s\\S]*$/);\n let cut: string;\n if (headerMatch && headerMatch.index !== undefined) {\n cut = slice.slice(0, lookback + headerMatch.index);\n } else {\n const lastBreak = slice.lastIndexOf('\\n\\n');\n cut = lastBreak > cap * 0.5 ? slice.slice(0, lastBreak) : slice;\n }\n return cut.trimEnd() + `\\n\\n[${name} truncated at ${cap} bytes — full ${content.length} bytes still in source]`;\n}\n\n/**\n * Apply caps to a list of named sections. Returns the same shape, with each\n * over-budget section trimmed via {@link capSection}. Pure function — does\n * not log or mutate input.\n */\nexport function applyCaps<T extends { name: string; content: string }>(sections: T[]): T[] {\n return sections.map((s) => ({ ...s, content: capSection(s.name, s.content) }));\n}\n\n/**\n * Compute the total size of a section list. Useful for the prompt-size\n * regression test in `tests/unit/system-prompt-size.test.ts`.\n */\nexport function totalSize(sections: Array<{ content: string }>): number {\n return sections.reduce((n, s) => n + (s.content?.length || 0), 0);\n}\n"],"mappings":";AAuBO,MAAM,uBAAyD,OAAO,OAAO;AAAA;AAAA,EAEhF,gBAAmB;AAAA,EACnB,eAAmB;AAAA;AAAA;AAAA,EAInB,eAAmB;AAAA;AAAA,EAGnB,kBAAmB;AAAA,EACnB,eAAmB;AAAA,EACnB,aAAmB;AAAA;AAAA;AAAA;AAAA,EAKnB,kBAAmB;AAAA;AAAA,EAGnB,eAAmB;AAAA,EACnB,iBAAmB;AAAA,EACnB,cAAmB;AAAA,EACnB,kBAAmB;AAAA;AAAA,EAGnB,eAAmB;AAAA,EACnB,gBAAmB;AAAA;AAAA,EAGnB,gBAAmB;AAAA,EACnB,mBAAmB;AAAA;AAAA;AAAA;AAAA,EAKnB,aAAmB;AAAA;AAAA;AAAA;AAAA,EAKnB,aAAmB;AAAA;AAAA;AAAA;AAAA,EAKnB,mBAAmB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMnB,cAAiB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMjB,UAAiB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMjB,YAAiB;AACrB,CAAC;AAGM,SAAS,cAAc,MAAsB;AAChD,QAAM,SAAS,eAAe,KAAK,YAAY;AAC/C,QAAM,MAAM,QAAQ,IAAI,MAAM;AAC9B,MAAI,QAAQ,QAAW;AACnB,UAAM,IAAI,SAAS,KAAK,EAAE;AAC1B,QAAI,OAAO,SAAS,CAAC,KAAK,KAAK,EAAG,QAAO;AAAA,EAC7C;AACA,SAAO,qBAAqB,IAAI,KAAK;AACzC;AAaO,SAAS,WAAW,MAAc,SAAyB;AAC9D,MAAI,CAAC,WAAW,CAAC,QAAQ,KAAK,EAAG,QAAO;AACxC,QAAM,MAAM,cAAc,IAAI;AAC9B,MAAI,QAAQ,KAAK,QAAQ,UAAU,IAAK,QAAO;AAE/C,QAAM,QAAQ,QAAQ,MAAM,GAAG,GAAG;AAClC,QAAM,WAAW,KAAK,MAAM,MAAM,IAAI;AACtC,QAAM,OAAO,MAAM,MAAM,QAAQ;AACjC,QAAM,cAAc,KAAK,MAAM,6BAA6B;AAC5D,MAAI;AACJ,MAAI,eAAe,YAAY,UAAU,QAAW;AAChD,UAAM,MAAM,MAAM,GAAG,WAAW,YAAY,KAAK;AAAA,EACrD,OAAO;AACH,UAAM,YAAY,MAAM,YAAY,MAAM;AAC1C,UAAM,YAAY,MAAM,MAAM,MAAM,MAAM,GAAG,SAAS,IAAI;AAAA,EAC9D;AACA,SAAO,IAAI,QAAQ,IAAI;AAAA;AAAA,GAAQ,IAAI,iBAAiB,GAAG,sBAAiB,QAAQ,MAAM;AAC1F;AAOO,SAAS,UAAuD,UAAoB;AACvF,SAAO,SAAS,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,SAAS,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE;AACjF;AAMO,SAAS,UAAU,UAA8C;AACpE,SAAO,SAAS,OAAO,CAAC,GAAG,MAAM,KAAK,EAAE,SAAS,UAAU,IAAI,CAAC;AACpE;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/agent/promptSectionCaps.ts"],"sourcesContent":["/**\n * TITAN — System-prompt section caps.\n *\n * The Persona injection cap (`src/personas/manager.ts`) already proves that\n * section-aware truncation works without breaking semantics — TITAN_PERSONA_CAP\n * has been live since v5.0. This module extends the same idea to every dynamic\n * section in `buildSystemPrompt`, so when memory/hindsight/wisdom/skill hints\n * bloat over time the prompt doesn't silently grow to 30 KB.\n *\n * Each cap is overridable via env (TITAN_CAP_<SECTION>) for power users; the\n * defaults are conservative and grounded in the audit in docs/HARNESS-PATTERNS.md.\n *\n * Reference:\n * - Anthropic — \"Effective context engineering for AI agents\" (context-as-bottleneck)\n * - 12 Factor Agents §3 \"Own your context window\"\n * - awesome-agent-harness top-10 pattern #3 \"Context Compaction & Working-State Management\"\n * - Hermes Agent's per-block size budget (procedural-memory layer)\n *\n * Caps are applied at *injection time* — the source data (memory store,\n * hindsight, wisdom) is never truncated. Only what reaches the LLM is bounded.\n */\n\n/** Default cap (bytes) per section. 0 = no cap. */\nexport const SECTION_CAP_DEFAULTS: Readonly<Record<string, number>> = Object.freeze({\n // Identity-y things — should be small by construction; cap defensively.\n identityBlocks: 2048,\n dateTimeBlock: 512,\n\n // Continuous-learning hints — the historical 5-in-1 grab-bag that\n // tends to grow because each sub-hint accumulates over a session.\n learningBlock: 3072,\n\n // Frustration / teaching / custom — short by construction.\n frustrationBlock: 512,\n teachingBlock: 1024,\n customBlock: 4096,\n\n // Workspace context — combines TITAN.md / AGENTS.md / SOUL.md / persona /\n // TOOLS.md. Persona already has its own cap; this one bounds the union.\n // Big users put project rules here, so cap is generous.\n workspaceContext: 8192,\n\n // Memory-related — these grow as the user accumulates knowledge.\n memoryContext: 2048,\n personalContext: 1024,\n graphSection: 4096,\n memoryToolsBlock: 768,\n\n // Self-awareness + optimized-prompt blocks — should stay small.\n selfAwareness: 2048,\n optimizedBlock: 1024,\n\n // Prompt-includes — user-controlled; reasonable upper bound.\n systemIncludes: 4096,\n transientIncludes: 2048,\n\n // v6.0 step 5 — Active Space intent. Lands at the recency position\n // (U-shaped attention) so the model uses it. Generous cap so users\n // can write a few paragraphs of Space-specific instructions.\n activeSpace: 4096,\n\n // v6.0 step 14 — Per-user Soma profile (long-term baseline + recent\n // observations). Grows slowly over weeks of use. 4 KB lets the model\n // see ~50 short observations.\n somaProfile: 4096,\n\n // v6.0 \"Living Canvas\" voice — TITAN's character block. Lands FIRST in\n // the dynamic sections so the LLM primes on it. Generous cap so the\n // posture rules + drive-interpretation rules both fit.\n livingCanvasVoice: 4096,\n\n // v6.0 Command Post upgrade #1 — Per-agent lessons (Reflexion).\n // Top-N lessons the agent has learned. Cap at 3 KB — twelve concise\n // entries fit comfortably; older lessons roll off via the lessons.md\n // file-level cap (200) so this prompt section never exceeds the cap.\n agentLessons: 3072,\n\n // v6.0 Command Post upgrade #2 — Living goal plan (Manus recitation).\n // The current plan.md rendered for the goal this session is tied to.\n // 4 KB matches the renderer's PROMPT_RENDER_CAP — scratchpad gets\n // trimmed first if the steps list alone is big.\n goalPlan: 4096,\n\n // v7.0 — Living goal SPEC (spec-driven / context engineering). The\n // definition-of-done (problem + requirements + checkable acceptance\n // criteria) for the goal this session is tied to. Lands just before\n // goalPlan so the agent sees \"are we done?\" before \"what's next?\".\n // 4 KB matches the renderer's PROMPT_RENDER_CAP.\n goalSpec: 4096,\n\n // v6.0.5 — User-authored / agent-authored SKILL.md library. The\n // renderer caps itself at SKILL_PROMPT_CAP (6 KB) and skips body\n // injection past that, so this cap mostly matters as a defence-in-\n // depth fallback if the catalog of lazy skills grows huge.\n userSkills: 8192,\n});\n\n/** Resolve a section's cap from env override or default. Returns 0 if disabled. */\nexport function getSectionCap(name: string): number {\n const envKey = 'TITAN_CAP_' + name.toUpperCase();\n const raw = process.env[envKey];\n if (raw !== undefined) {\n const n = parseInt(raw, 10);\n if (Number.isFinite(n) && n >= 0) return n;\n }\n return SECTION_CAP_DEFAULTS[name] ?? 0;\n}\n\n/**\n * Truncate a section to the cap, preferring a markdown header in the final\n * quarter for a clean cut (same heuristic as persona truncation). Appends a\n * truncated-marker so the model can SEE that content was clipped — important\n * to prevent \"I didn't see Y in your instructions\" confusion.\n *\n * Returns the input unchanged when:\n * - cap is 0 (disabled)\n * - content is already within the cap\n * - content is empty / whitespace\n */\nexport function capSection(name: string, content: string): string {\n if (!content || !content.trim()) return content;\n const cap = getSectionCap(name);\n if (cap === 0 || content.length <= cap) return content;\n\n const slice = content.slice(0, cap);\n const lookback = Math.floor(cap * 0.75);\n const tail = slice.slice(lookback);\n const headerMatch = tail.match(/\\n(#{1,6} [^\\n]+)\\n[\\s\\S]*$/);\n let cut: string;\n if (headerMatch && headerMatch.index !== undefined) {\n cut = slice.slice(0, lookback + headerMatch.index);\n } else {\n const lastBreak = slice.lastIndexOf('\\n\\n');\n cut = lastBreak > cap * 0.5 ? slice.slice(0, lastBreak) : slice;\n }\n return cut.trimEnd() + `\\n\\n[${name} truncated at ${cap} bytes — full ${content.length} bytes still in source]`;\n}\n\n/**\n * Apply caps to a list of named sections. Returns the same shape, with each\n * over-budget section trimmed via {@link capSection}. Pure function — does\n * not log or mutate input.\n */\nexport function applyCaps<T extends { name: string; content: string }>(sections: T[]): T[] {\n return sections.map((s) => ({ ...s, content: capSection(s.name, s.content) }));\n}\n\n/**\n * Compute the total size of a section list. Useful for the prompt-size\n * regression test in `tests/unit/system-prompt-size.test.ts`.\n */\nexport function totalSize(sections: Array<{ content: string }>): number {\n return sections.reduce((n, s) => n + (s.content?.length || 0), 0);\n}\n"],"mappings":";AAuBO,MAAM,uBAAyD,OAAO,OAAO;AAAA;AAAA,EAEhF,gBAAmB;AAAA,EACnB,eAAmB;AAAA;AAAA;AAAA,EAInB,eAAmB;AAAA;AAAA,EAGnB,kBAAmB;AAAA,EACnB,eAAmB;AAAA,EACnB,aAAmB;AAAA;AAAA;AAAA;AAAA,EAKnB,kBAAmB;AAAA;AAAA,EAGnB,eAAmB;AAAA,EACnB,iBAAmB;AAAA,EACnB,cAAmB;AAAA,EACnB,kBAAmB;AAAA;AAAA,EAGnB,eAAmB;AAAA,EACnB,gBAAmB;AAAA;AAAA,EAGnB,gBAAmB;AAAA,EACnB,mBAAmB;AAAA;AAAA;AAAA;AAAA,EAKnB,aAAmB;AAAA;AAAA;AAAA;AAAA,EAKnB,aAAmB;AAAA;AAAA;AAAA;AAAA,EAKnB,mBAAmB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMnB,cAAiB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMjB,UAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOjB,UAAiB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMjB,YAAiB;AACrB,CAAC;AAGM,SAAS,cAAc,MAAsB;AAChD,QAAM,SAAS,eAAe,KAAK,YAAY;AAC/C,QAAM,MAAM,QAAQ,IAAI,MAAM;AAC9B,MAAI,QAAQ,QAAW;AACnB,UAAM,IAAI,SAAS,KAAK,EAAE;AAC1B,QAAI,OAAO,SAAS,CAAC,KAAK,KAAK,EAAG,QAAO;AAAA,EAC7C;AACA,SAAO,qBAAqB,IAAI,KAAK;AACzC;AAaO,SAAS,WAAW,MAAc,SAAyB;AAC9D,MAAI,CAAC,WAAW,CAAC,QAAQ,KAAK,EAAG,QAAO;AACxC,QAAM,MAAM,cAAc,IAAI;AAC9B,MAAI,QAAQ,KAAK,QAAQ,UAAU,IAAK,QAAO;AAE/C,QAAM,QAAQ,QAAQ,MAAM,GAAG,GAAG;AAClC,QAAM,WAAW,KAAK,MAAM,MAAM,IAAI;AACtC,QAAM,OAAO,MAAM,MAAM,QAAQ;AACjC,QAAM,cAAc,KAAK,MAAM,6BAA6B;AAC5D,MAAI;AACJ,MAAI,eAAe,YAAY,UAAU,QAAW;AAChD,UAAM,MAAM,MAAM,GAAG,WAAW,YAAY,KAAK;AAAA,EACrD,OAAO;AACH,UAAM,YAAY,MAAM,YAAY,MAAM;AAC1C,UAAM,YAAY,MAAM,MAAM,MAAM,MAAM,GAAG,SAAS,IAAI;AAAA,EAC9D;AACA,SAAO,IAAI,QAAQ,IAAI;AAAA;AAAA,GAAQ,IAAI,iBAAiB,GAAG,sBAAiB,QAAQ,MAAM;AAC1F;AAOO,SAAS,UAAuD,UAAoB;AACvF,SAAO,SAAS,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,SAAS,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE;AACjF;AAMO,SAAS,UAAU,UAA8C;AACpE,SAAO,SAAS,OAAO,CAAC,GAAG,MAAM,KAAK,EAAE,SAAS,UAAU,IAAI,CAAC;AACpE;","names":[]}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { TITAN_HOME } from "../utils/constants.js";
|
|
5
|
+
import logger from "../utils/logger.js";
|
|
6
|
+
const COMPONENT = "Reminders";
|
|
7
|
+
const FILE = () => join(TITAN_HOME, "reminders.json");
|
|
8
|
+
const CHECK_INTERVAL_MS = 6e4;
|
|
9
|
+
function readAll() {
|
|
10
|
+
try {
|
|
11
|
+
if (!existsSync(FILE())) return [];
|
|
12
|
+
const raw = JSON.parse(readFileSync(FILE(), "utf-8"));
|
|
13
|
+
return Array.isArray(raw) ? raw : [];
|
|
14
|
+
} catch {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function writeAll(reminders) {
|
|
19
|
+
try {
|
|
20
|
+
mkdirSync(dirname(FILE()), { recursive: true });
|
|
21
|
+
const cutoff = Date.now() - 30 * 864e5;
|
|
22
|
+
const kept = reminders.filter((r) => !r.firedAt || new Date(r.firedAt).getTime() > cutoff);
|
|
23
|
+
writeFileSync(FILE(), JSON.stringify(kept, null, 2));
|
|
24
|
+
} catch (err) {
|
|
25
|
+
logger.warn(COMPONENT, `Could not persist reminders: ${err.message}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function addReminder(message, dueAtIso) {
|
|
29
|
+
const due = new Date(dueAtIso);
|
|
30
|
+
if (isNaN(due.getTime())) return { error: `Not a valid date/time: "${dueAtIso}". Use ISO 8601, e.g. 2026-06-12T17:00:00-07:00.` };
|
|
31
|
+
if (due.getTime() < Date.now() - 6e4) return { error: `That time is in the past (${due.toLocaleString()}).` };
|
|
32
|
+
const trimmed = message.trim();
|
|
33
|
+
if (!trimmed) return { error: "Reminder message is empty." };
|
|
34
|
+
const reminder = {
|
|
35
|
+
id: `rem-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
|
|
36
|
+
message: trimmed.slice(0, 300),
|
|
37
|
+
dueAt: due.toISOString(),
|
|
38
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
39
|
+
};
|
|
40
|
+
const all = readAll();
|
|
41
|
+
all.push(reminder);
|
|
42
|
+
writeAll(all);
|
|
43
|
+
logger.info(COMPONENT, `Reminder set: "${reminder.message}" at ${reminder.dueAt}`);
|
|
44
|
+
return reminder;
|
|
45
|
+
}
|
|
46
|
+
function listReminders(includeFired = false) {
|
|
47
|
+
const all = readAll();
|
|
48
|
+
return (includeFired ? all : all.filter((r) => !r.firedAt)).sort((a, b) => a.dueAt.localeCompare(b.dueAt));
|
|
49
|
+
}
|
|
50
|
+
function cancelReminder(id) {
|
|
51
|
+
const all = readAll();
|
|
52
|
+
const next = all.filter((r) => r.id !== id && !(id === "all" && !r.firedAt));
|
|
53
|
+
if (next.length === all.length) return false;
|
|
54
|
+
writeAll(next);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
function dueReminders(all, now) {
|
|
58
|
+
return all.filter((r) => !r.firedAt && new Date(r.dueAt).getTime() <= now);
|
|
59
|
+
}
|
|
60
|
+
async function checkReminders(now = Date.now()) {
|
|
61
|
+
const all = readAll();
|
|
62
|
+
const due = dueReminders(all, now);
|
|
63
|
+
if (due.length === 0) return 0;
|
|
64
|
+
const { speakNote } = await import("./heartbeat.js");
|
|
65
|
+
for (const r of due) {
|
|
66
|
+
speakNote(`\u23F0 Reminder: ${r.message}`);
|
|
67
|
+
r.firedAt = new Date(now).toISOString();
|
|
68
|
+
logger.info(COMPONENT, `Fired reminder ${r.id}: ${r.message}`);
|
|
69
|
+
}
|
|
70
|
+
writeAll(all);
|
|
71
|
+
return due.length;
|
|
72
|
+
}
|
|
73
|
+
function registerReminderWatcher() {
|
|
74
|
+
import("./daemon.js").then(({ registerWatcher }) => {
|
|
75
|
+
registerWatcher("reminders", async () => {
|
|
76
|
+
await checkReminders();
|
|
77
|
+
}, CHECK_INTERVAL_MS);
|
|
78
|
+
logger.info(COMPONENT, "Reminder watcher registered (every 60s)");
|
|
79
|
+
}).catch((err) => logger.warn(COMPONENT, `Register failed: ${err.message}`));
|
|
80
|
+
import("../config/config.js").then(({ loadConfig }) => {
|
|
81
|
+
const cfg = loadConfig();
|
|
82
|
+
if (cfg.daemon?.enabled !== true) {
|
|
83
|
+
setInterval(() => {
|
|
84
|
+
checkReminders().catch(() => {
|
|
85
|
+
});
|
|
86
|
+
}, CHECK_INTERVAL_MS).unref();
|
|
87
|
+
logger.info(COMPONENT, "Daemon disabled \u2014 reminders running on their own 60s timer");
|
|
88
|
+
}
|
|
89
|
+
}).catch(() => {
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
export {
|
|
93
|
+
CHECK_INTERVAL_MS,
|
|
94
|
+
addReminder,
|
|
95
|
+
cancelReminder,
|
|
96
|
+
checkReminders,
|
|
97
|
+
dueReminders,
|
|
98
|
+
listReminders,
|
|
99
|
+
registerReminderWatcher
|
|
100
|
+
};
|
|
101
|
+
//# sourceMappingURL=reminders.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/agent/reminders.ts"],"sourcesContent":["/**\n * Reminders — \"remind me Friday at 5pm\" that actually fires (v7.0).\n *\n * User-testing exposed the gap: models CLAIMED to set reminders because the\n * natural tool didn't exist (cron schedules shell commands, not \"tell ME a\n * thing at a time\"). This is that tool's engine: a tiny persistent store +\n * a 60s daemon watcher. Due reminders fire through the heartbeat note\n * channel, so the mascot speaks them — and they show anywhere heartbeat\n * notes show. No new UI, no LLM calls, deterministic.\n */\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { join, dirname } from 'path';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Reminders';\nconst FILE = () => join(TITAN_HOME, 'reminders.json');\nexport const CHECK_INTERVAL_MS = 60_000;\n\nexport interface Reminder {\n id: string;\n message: string;\n /** ISO 8601 — when to fire. */\n dueAt: string;\n createdAt: string;\n firedAt?: string;\n}\n\nfunction readAll(): Reminder[] {\n try {\n if (!existsSync(FILE())) return [];\n const raw = JSON.parse(readFileSync(FILE(), 'utf-8'));\n return Array.isArray(raw) ? raw as Reminder[] : [];\n } catch {\n return [];\n }\n}\n\nfunction writeAll(reminders: Reminder[]): void {\n try {\n mkdirSync(dirname(FILE()), { recursive: true });\n // Keep the file bounded: drop fired reminders older than 30 days.\n const cutoff = Date.now() - 30 * 86_400_000;\n const kept = reminders.filter(r => !r.firedAt || new Date(r.firedAt).getTime() > cutoff);\n writeFileSync(FILE(), JSON.stringify(kept, null, 2));\n } catch (err) {\n logger.warn(COMPONENT, `Could not persist reminders: ${(err as Error).message}`);\n }\n}\n\nexport function addReminder(message: string, dueAtIso: string): Reminder | { error: string } {\n const due = new Date(dueAtIso);\n if (isNaN(due.getTime())) return { error: `Not a valid date/time: \"${dueAtIso}\". Use ISO 8601, e.g. 2026-06-12T17:00:00-07:00.` };\n if (due.getTime() < Date.now() - 60_000) return { error: `That time is in the past (${due.toLocaleString()}).` };\n const trimmed = message.trim();\n if (!trimmed) return { error: 'Reminder message is empty.' };\n const reminder: Reminder = {\n id: `rem-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,\n message: trimmed.slice(0, 300),\n dueAt: due.toISOString(),\n createdAt: new Date().toISOString(),\n };\n const all = readAll();\n all.push(reminder);\n writeAll(all);\n logger.info(COMPONENT, `Reminder set: \"${reminder.message}\" at ${reminder.dueAt}`);\n return reminder;\n}\n\nexport function listReminders(includeFired = false): Reminder[] {\n const all = readAll();\n return (includeFired ? all : all.filter(r => !r.firedAt))\n .sort((a, b) => a.dueAt.localeCompare(b.dueAt));\n}\n\nexport function cancelReminder(id: string): boolean {\n const all = readAll();\n const next = all.filter(r => r.id !== id && !(id === 'all' && !r.firedAt));\n if (next.length === all.length) return false;\n writeAll(next);\n return true;\n}\n\n/** Pure: which pending reminders are due at `now`? Exported for tests. */\nexport function dueReminders(all: Reminder[], now: number): Reminder[] {\n return all.filter(r => !r.firedAt && new Date(r.dueAt).getTime() <= now);\n}\n\n/** One watcher pass: fire anything due via the heartbeat/mascot channel. */\nexport async function checkReminders(now: number = Date.now()): Promise<number> {\n const all = readAll();\n const due = dueReminders(all, now);\n if (due.length === 0) return 0;\n const { speakNote } = await import('./heartbeat.js');\n for (const r of due) {\n speakNote(`⏰ Reminder: ${r.message}`);\n r.firedAt = new Date(now).toISOString();\n logger.info(COMPONENT, `Fired reminder ${r.id}: ${r.message}`);\n }\n writeAll(all);\n return due.length;\n}\n\n/** Register the 60s due-check on the daemon (same lifecycle as heartbeat). */\nexport function registerReminderWatcher(): void {\n import('./daemon.js').then(({ registerWatcher }) => {\n registerWatcher('reminders', async () => { await checkReminders(); }, CHECK_INTERVAL_MS);\n logger.info(COMPONENT, 'Reminder watcher registered (every 60s)');\n }).catch(err => logger.warn(COMPONENT, `Register failed: ${(err as Error).message}`));\n // The daemon defaults to disabled — reminders must fire regardless, so run\n // a self-owned timer when the daemon never starts (v7.0 review finding).\n import('../config/config.js').then(({ loadConfig }) => {\n const cfg = loadConfig() as { daemon?: { enabled?: boolean } };\n if (cfg.daemon?.enabled !== true) {\n setInterval(() => { checkReminders().catch(() => {}); }, CHECK_INTERVAL_MS).unref();\n logger.info(COMPONENT, 'Daemon disabled — reminders running on their own 60s timer');\n }\n }).catch(() => {});\n}\n"],"mappings":"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","names":[]}
|