@clawpump/claw-agent 0.1.5 → 0.1.7

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 (1212) hide show
  1. package/agent/.dockerignore +67 -0
  2. package/agent/.envrc +1 -1
  3. package/agent/.gitattributes +8 -0
  4. package/agent/AGENTS.md +216 -4
  5. package/agent/CONTRIBUTING.md +46 -8
  6. package/agent/Dockerfile +78 -35
  7. package/agent/MANIFEST.in +2 -0
  8. package/agent/README.md +12 -5
  9. package/agent/README.ur-pk.md +261 -0
  10. package/agent/README.zh-CN.md +11 -8
  11. package/agent/SECURITY.md +5 -4
  12. package/agent/acp_adapter/provenance.py +127 -0
  13. package/agent/acp_adapter/server.py +112 -5
  14. package/agent/acp_adapter/session.py +1 -6
  15. package/agent/acp_registry/agent.json +2 -2
  16. package/agent/agent/account_usage.py +313 -1
  17. package/agent/agent/agent_init.py +140 -37
  18. package/agent/agent/agent_runtime_helpers.py +342 -83
  19. package/agent/agent/anthropic_adapter.py +320 -33
  20. package/agent/agent/auxiliary_client.py +525 -105
  21. package/agent/agent/background_review.py +157 -19
  22. package/agent/agent/bedrock_adapter.py +71 -6
  23. package/agent/agent/billing_view.py +295 -0
  24. package/agent/agent/chat_completion_helpers.py +229 -4
  25. package/agent/agent/codex_responses_adapter.py +86 -10
  26. package/agent/agent/codex_runtime.py +153 -1
  27. package/agent/agent/coding_context.py +738 -0
  28. package/agent/agent/context_compressor.py +392 -44
  29. package/agent/agent/context_references.py +34 -1
  30. package/agent/agent/conversation_compression.py +159 -22
  31. package/agent/agent/conversation_loop.py +643 -908
  32. package/agent/agent/copilot_acp_client.py +4 -11
  33. package/agent/agent/credential_pool.py +5 -3
  34. package/agent/agent/credits_tracker.py +794 -0
  35. package/agent/agent/curator.py +91 -18
  36. package/agent/agent/curator_backup.py +26 -10
  37. package/agent/agent/display.py +42 -1
  38. package/agent/agent/error_classifier.py +52 -3
  39. package/agent/agent/errors.py +3 -0
  40. package/agent/agent/file_safety.py +0 -17
  41. package/agent/agent/gemini_native_adapter.py +31 -1
  42. package/agent/agent/i18n.py +48 -4
  43. package/agent/agent/image_gen_provider.py +74 -5
  44. package/agent/agent/image_routing.py +29 -0
  45. package/agent/agent/insights.py +8 -17
  46. package/agent/agent/lsp/install.py +3 -0
  47. package/agent/agent/memory_manager.py +326 -31
  48. package/agent/agent/message_content.py +50 -0
  49. package/agent/agent/model_metadata.py +214 -3
  50. package/agent/agent/moonshot_schema.py +8 -1
  51. package/agent/agent/onboarding.py +60 -0
  52. package/agent/agent/prompt_builder.py +327 -37
  53. package/agent/agent/redact.py +1 -0
  54. package/agent/agent/runtime_cwd.py +34 -5
  55. package/agent/agent/secret_scope.py +205 -0
  56. package/agent/agent/secret_sources/bitwarden.py +34 -2
  57. package/agent/agent/skill_commands.py +90 -1
  58. package/agent/agent/skill_preprocessing.py +1 -0
  59. package/agent/agent/skill_utils.py +209 -36
  60. package/agent/agent/ssl_guard.py +94 -0
  61. package/agent/agent/system_prompt.py +133 -5
  62. package/agent/agent/tool_executor.py +496 -70
  63. package/agent/agent/transports/anthropic.py +83 -21
  64. package/agent/agent/transports/chat_completions.py +94 -5
  65. package/agent/agent/transports/codex.py +67 -2
  66. package/agent/agent/transports/codex_app_server.py +1 -0
  67. package/agent/agent/transports/codex_app_server_session.py +30 -0
  68. package/agent/agent/transports/types.py +12 -0
  69. package/agent/agent/turn_context.py +408 -0
  70. package/agent/agent/turn_finalizer.py +428 -0
  71. package/agent/agent/turn_retry_state.py +68 -0
  72. package/agent/agent/usage_pricing.py +3 -0
  73. package/agent/apps/bootstrap-installer/package.json +6 -5
  74. package/agent/apps/bootstrap-installer/src/routes/failure.tsx +12 -5
  75. package/agent/apps/bootstrap-installer/src/routes/progress.tsx +1 -3
  76. package/agent/apps/bootstrap-installer/src/store.ts +3 -2
  77. package/agent/apps/bootstrap-installer/src-tauri/src/bootstrap.rs +172 -7
  78. package/agent/apps/bootstrap-installer/src-tauri/src/events.rs +14 -1
  79. package/agent/apps/bootstrap-installer/src-tauri/src/paths.rs +29 -0
  80. package/agent/apps/bootstrap-installer/src-tauri/src/powershell.rs +93 -3
  81. package/agent/apps/bootstrap-installer/src-tauri/src/update.rs +695 -39
  82. package/agent/apps/bootstrap-installer/tsconfig.json +3 -4
  83. package/agent/apps/desktop/DESIGN.md +167 -0
  84. package/agent/apps/desktop/README.md +20 -16
  85. package/agent/apps/desktop/assets/icon.icns +0 -0
  86. package/agent/apps/desktop/assets/icon.ico +0 -0
  87. package/agent/apps/desktop/assets/icon.png +0 -0
  88. package/agent/apps/desktop/electron/backend-env.cjs +112 -0
  89. package/agent/apps/desktop/electron/backend-env.test.cjs +111 -0
  90. package/agent/apps/desktop/electron/backend-probes.test.cjs +3 -1
  91. package/agent/apps/desktop/electron/backend-ready.cjs +66 -0
  92. package/agent/apps/desktop/electron/bootstrap-platform.cjs +52 -0
  93. package/agent/apps/desktop/electron/bootstrap-platform.test.cjs +59 -1
  94. package/agent/apps/desktop/electron/bootstrap-runner.cjs +176 -38
  95. package/agent/apps/desktop/electron/bootstrap-runner.test.cjs +112 -1
  96. package/agent/apps/desktop/electron/connection-config.cjs +288 -0
  97. package/agent/apps/desktop/electron/connection-config.test.cjs +396 -0
  98. package/agent/apps/desktop/electron/dashboard-token.cjs +99 -0
  99. package/agent/apps/desktop/electron/dashboard-token.test.cjs +142 -0
  100. package/agent/apps/desktop/electron/desktop-uninstall.cjs +232 -0
  101. package/agent/apps/desktop/electron/desktop-uninstall.test.cjs +246 -0
  102. package/agent/apps/desktop/electron/entitlements.mac.inherit.plist +2 -0
  103. package/agent/apps/desktop/electron/fs-read-dir.cjs +109 -0
  104. package/agent/apps/desktop/electron/fs-read-dir.test.cjs +364 -0
  105. package/agent/apps/desktop/electron/gateway-ws-probe.cjs +188 -0
  106. package/agent/apps/desktop/electron/gateway-ws-probe.test.cjs +122 -0
  107. package/agent/apps/desktop/electron/git-root.cjs +54 -0
  108. package/agent/apps/desktop/electron/git-root.test.cjs +40 -0
  109. package/agent/apps/desktop/electron/git-worktrees.cjs +174 -0
  110. package/agent/apps/desktop/electron/hardening.cjs +123 -28
  111. package/agent/apps/desktop/electron/hardening.test.cjs +163 -0
  112. package/agent/apps/desktop/electron/main.cjs +3121 -331
  113. package/agent/apps/desktop/electron/oauth-net-request.cjs +20 -0
  114. package/agent/apps/desktop/electron/oauth-net-request.test.cjs +34 -0
  115. package/agent/apps/desktop/electron/preload.cjs +52 -2
  116. package/agent/apps/desktop/electron/session-windows.cjs +124 -0
  117. package/agent/apps/desktop/electron/session-windows.test.cjs +199 -0
  118. package/agent/apps/desktop/electron/update-rebuild.cjs +29 -0
  119. package/agent/apps/desktop/electron/update-rebuild.test.cjs +55 -0
  120. package/agent/apps/desktop/electron/update-remote.cjs +56 -0
  121. package/agent/apps/desktop/electron/update-remote.test.cjs +78 -0
  122. package/agent/apps/desktop/electron/vscode-marketplace.cjs +331 -0
  123. package/agent/apps/desktop/electron/vscode-marketplace.test.cjs +113 -0
  124. package/agent/apps/desktop/electron/windows-child-process.test.cjs +57 -0
  125. package/agent/apps/desktop/electron/windows-user-env.cjs +76 -0
  126. package/agent/apps/desktop/electron/windows-user-env.test.cjs +90 -0
  127. package/agent/apps/desktop/electron/workspace-cwd.cjs +38 -0
  128. package/agent/apps/desktop/electron/workspace-cwd.test.cjs +45 -0
  129. package/agent/apps/desktop/eslint.config.mjs +0 -3
  130. package/agent/apps/desktop/index.html +27 -2
  131. package/agent/apps/desktop/package.json +31 -11
  132. package/agent/apps/desktop/pr-assets/session-source-folders.png +0 -0
  133. package/agent/apps/desktop/public/apple-touch-icon.png +0 -0
  134. package/agent/apps/desktop/public/nous-girl.jpg +0 -0
  135. package/agent/apps/desktop/scripts/assert-dist-built.cjs +70 -0
  136. package/agent/apps/desktop/scripts/assert-dist-built.test.cjs +84 -0
  137. package/agent/apps/desktop/scripts/before-pack.cjs +78 -0
  138. package/agent/apps/desktop/scripts/before-pack.test.cjs +53 -0
  139. package/agent/apps/desktop/scripts/diag-scroll-reset.mjs +229 -0
  140. package/agent/apps/desktop/scripts/patch-electron-builder-mac-binary.cjs +64 -0
  141. package/agent/apps/desktop/scripts/run-electron-builder.cjs +57 -0
  142. package/agent/apps/desktop/src/app/agents/index.tsx +53 -45
  143. package/agent/apps/desktop/src/app/artifacts/index.tsx +102 -83
  144. package/agent/apps/desktop/src/app/chat/chat-drop-overlay.tsx +29 -8
  145. package/agent/apps/desktop/src/app/chat/chat-swap-overlay.tsx +47 -0
  146. package/agent/apps/desktop/src/app/chat/composer/attachments.tsx +81 -45
  147. package/agent/apps/desktop/src/app/chat/composer/completion-drawer.tsx +13 -24
  148. package/agent/apps/desktop/src/app/chat/composer/context-menu.tsx +138 -88
  149. package/agent/apps/desktop/src/app/chat/composer/controls.tsx +138 -90
  150. package/agent/apps/desktop/src/app/chat/composer/enter-submit-dom-race.test.tsx +218 -0
  151. package/agent/apps/desktop/src/app/chat/composer/focus.ts +32 -0
  152. package/agent/apps/desktop/src/app/chat/composer/help-hint.tsx +38 -25
  153. package/agent/apps/desktop/src/app/chat/composer/hooks/use-live-completion-adapter.ts +7 -0
  154. package/agent/apps/desktop/src/app/chat/composer/hooks/use-mic-recorder.ts +22 -12
  155. package/agent/apps/desktop/src/app/chat/composer/hooks/use-slash-completions.ts +142 -14
  156. package/agent/apps/desktop/src/app/chat/composer/hooks/use-voice-conversation.ts +14 -11
  157. package/agent/apps/desktop/src/app/chat/composer/hooks/use-voice-recorder.ts +9 -6
  158. package/agent/apps/desktop/src/app/chat/composer/ime-composition-dom-repro.test.tsx +108 -0
  159. package/agent/apps/desktop/src/app/chat/composer/index.tsx +930 -180
  160. package/agent/apps/desktop/src/app/chat/composer/inline-refs.ts +136 -32
  161. package/agent/apps/desktop/src/app/chat/composer/model-pill.tsx +86 -0
  162. package/agent/apps/desktop/src/app/chat/composer/queue-panel.tsx +54 -75
  163. package/agent/apps/desktop/src/app/chat/composer/rich-editor.test.ts +117 -1
  164. package/agent/apps/desktop/src/app/chat/composer/rich-editor.ts +117 -6
  165. package/agent/apps/desktop/src/app/chat/composer/slash-nav-dom-repro.test.tsx +186 -0
  166. package/agent/apps/desktop/src/app/chat/composer/status-stack/index.tsx +202 -0
  167. package/agent/apps/desktop/src/app/chat/composer/status-stack/status-row.tsx +155 -0
  168. package/agent/apps/desktop/src/app/chat/composer/text-utils.test.ts +104 -0
  169. package/agent/apps/desktop/src/app/chat/composer/text-utils.ts +37 -9
  170. package/agent/apps/desktop/src/app/chat/composer/trigger-popover.test.tsx +50 -0
  171. package/agent/apps/desktop/src/app/chat/composer/trigger-popover.tsx +105 -40
  172. package/agent/apps/desktop/src/app/chat/composer/types.ts +5 -0
  173. package/agent/apps/desktop/src/app/chat/composer/url-dialog.tsx +11 -15
  174. package/agent/apps/desktop/src/app/chat/composer/voice-activity.tsx +8 -4
  175. package/agent/apps/desktop/src/app/chat/hooks/use-composer-actions.test.ts +57 -0
  176. package/agent/apps/desktop/src/app/chat/hooks/use-composer-actions.ts +70 -16
  177. package/agent/apps/desktop/src/app/chat/hooks/use-file-drop-zone.ts +52 -16
  178. package/agent/apps/desktop/src/app/chat/index.tsx +234 -81
  179. package/agent/apps/desktop/src/app/chat/perf-probe.tsx +69 -21
  180. package/agent/apps/desktop/src/app/chat/right-rail/preview-console.tsx +44 -40
  181. package/agent/apps/desktop/src/app/chat/right-rail/preview-file.tsx +71 -25
  182. package/agent/apps/desktop/src/app/chat/right-rail/preview-pane.test.tsx +40 -1
  183. package/agent/apps/desktop/src/app/chat/right-rail/preview-pane.tsx +55 -53
  184. package/agent/apps/desktop/src/app/chat/right-rail/preview.tsx +35 -17
  185. package/agent/apps/desktop/src/app/chat/scroll-to-bottom-button.test.tsx +67 -0
  186. package/agent/apps/desktop/src/app/chat/scroll-to-bottom-button.tsx +74 -0
  187. package/agent/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx +356 -0
  188. package/agent/apps/desktop/src/app/chat/sidebar/index.tsx +1189 -364
  189. package/agent/apps/desktop/src/app/chat/sidebar/load-more-row.tsx +30 -0
  190. package/agent/apps/desktop/src/app/chat/sidebar/order.test.ts +21 -0
  191. package/agent/apps/desktop/src/app/chat/sidebar/order.ts +17 -0
  192. package/agent/apps/desktop/src/app/chat/sidebar/profile-switcher.tsx +524 -0
  193. package/agent/apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx +80 -45
  194. package/agent/apps/desktop/src/app/chat/sidebar/session-row.tsx +120 -25
  195. package/agent/apps/desktop/src/app/chat/sidebar/virtual-session-list.tsx +7 -13
  196. package/agent/apps/desktop/src/app/chat/sidebar/workspace-groups.test.ts +149 -0
  197. package/agent/apps/desktop/src/app/chat/sidebar/workspace-groups.ts +326 -0
  198. package/agent/apps/desktop/src/app/chat/thread-loading.ts +7 -2
  199. package/agent/apps/desktop/src/app/command-center/index.tsx +320 -581
  200. package/agent/apps/desktop/src/app/command-palette/index.tsx +681 -0
  201. package/agent/apps/desktop/src/app/command-palette/marketplace-theme-page.tsx +157 -0
  202. package/agent/apps/desktop/src/app/cron/index.tsx +392 -324
  203. package/agent/apps/desktop/src/app/cron/job-state.ts +29 -0
  204. package/agent/apps/desktop/src/app/desktop-controller.tsx +618 -123
  205. package/agent/apps/desktop/src/app/floating-hud.ts +22 -0
  206. package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-boot.test.tsx +265 -0
  207. package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts +260 -14
  208. package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-request.ts +48 -4
  209. package/agent/apps/desktop/src/app/hooks/use-keybinds.ts +270 -0
  210. package/agent/apps/desktop/src/app/hooks/use-refresh-hotkey.ts +45 -0
  211. package/agent/apps/desktop/src/app/layout-constants.ts +19 -0
  212. package/agent/apps/desktop/src/app/messaging/index.tsx +136 -241
  213. package/agent/apps/desktop/src/app/messaging/platform-icon.tsx +95 -0
  214. package/agent/apps/desktop/src/app/model-visibility-overlay.tsx +31 -0
  215. package/agent/apps/desktop/src/app/overlays/overlay-search-input.tsx +18 -62
  216. package/agent/apps/desktop/src/app/overlays/overlay-split-layout.tsx +59 -7
  217. package/agent/apps/desktop/src/app/overlays/overlay-view.tsx +9 -5
  218. package/agent/apps/desktop/src/app/page-search-shell.tsx +42 -20
  219. package/agent/apps/desktop/src/app/profiles/create-profile-dialog.tsx +165 -0
  220. package/agent/apps/desktop/src/app/profiles/delete-profile-dialog.tsx +65 -0
  221. package/agent/apps/desktop/src/app/profiles/index.tsx +174 -199
  222. package/agent/apps/desktop/src/app/profiles/rename-profile-dialog.tsx +125 -0
  223. package/agent/apps/desktop/src/app/right-sidebar/files/dnd-manager.ts +27 -0
  224. package/agent/apps/desktop/src/app/right-sidebar/files/ipc.test.ts +100 -0
  225. package/agent/apps/desktop/src/app/right-sidebar/files/ipc.ts +12 -18
  226. package/agent/apps/desktop/src/app/right-sidebar/files/remote-picker.tsx +177 -0
  227. package/agent/apps/desktop/src/app/right-sidebar/files/tree.tsx +35 -21
  228. package/agent/apps/desktop/src/app/right-sidebar/files/use-project-tree.test.ts +75 -3
  229. package/agent/apps/desktop/src/app/right-sidebar/files/use-project-tree.ts +152 -5
  230. package/agent/apps/desktop/src/app/right-sidebar/index.test.tsx +75 -0
  231. package/agent/apps/desktop/src/app/right-sidebar/index.tsx +166 -129
  232. package/agent/apps/desktop/src/app/right-sidebar/store.ts +19 -4
  233. package/agent/apps/desktop/src/app/right-sidebar/terminal/buffer.ts +65 -0
  234. package/agent/apps/desktop/src/app/right-sidebar/terminal/index.tsx +29 -34
  235. package/agent/apps/desktop/src/app/right-sidebar/terminal/persistent.tsx +18 -6
  236. package/agent/apps/desktop/src/app/right-sidebar/terminal/selection.ts +93 -32
  237. package/agent/apps/desktop/src/app/right-sidebar/terminal/use-terminal-session.ts +381 -119
  238. package/agent/apps/desktop/src/app/routes.ts +9 -0
  239. package/agent/apps/desktop/src/app/session/hooks/use-cwd-actions.ts +17 -7
  240. package/agent/apps/desktop/src/app/session/hooks/use-message-stream.ts +365 -47
  241. package/agent/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx +198 -0
  242. package/agent/apps/desktop/src/app/session/hooks/use-model-controls.ts +70 -34
  243. package/agent/apps/desktop/src/app/session/hooks/use-prompt-actions.test.tsx +1061 -0
  244. package/agent/apps/desktop/src/app/session/hooks/use-prompt-actions.ts +1143 -165
  245. package/agent/apps/desktop/src/app/session/hooks/use-route-resume.test.tsx +341 -2
  246. package/agent/apps/desktop/src/app/session/hooks/use-route-resume.ts +176 -5
  247. package/agent/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx +259 -0
  248. package/agent/apps/desktop/src/app/session/hooks/use-session-actions.ts +452 -149
  249. package/agent/apps/desktop/src/app/session/hooks/use-session-state-cache.test.tsx +327 -0
  250. package/agent/apps/desktop/src/app/session/hooks/use-session-state-cache.ts +133 -4
  251. package/agent/apps/desktop/src/app/session-picker-overlay.tsx +32 -0
  252. package/agent/apps/desktop/src/app/session-switcher.tsx +107 -0
  253. package/agent/apps/desktop/src/app/settings/about-settings.tsx +45 -36
  254. package/agent/apps/desktop/src/app/settings/appearance-settings.tsx +243 -162
  255. package/agent/apps/desktop/src/app/settings/config-settings.tsx +86 -66
  256. package/agent/apps/desktop/src/app/settings/constants.ts +459 -122
  257. package/agent/apps/desktop/src/app/settings/credential-key-ui.tsx +373 -0
  258. package/agent/apps/desktop/src/app/settings/env-credentials.tsx +198 -0
  259. package/agent/apps/desktop/src/app/settings/env-var-actions-menu.tsx +136 -0
  260. package/agent/apps/desktop/src/app/settings/field-copy.ts +56 -0
  261. package/agent/apps/desktop/src/app/settings/gateway-settings.tsx +385 -72
  262. package/agent/apps/desktop/src/app/settings/helpers.test.ts +156 -1
  263. package/agent/apps/desktop/src/app/settings/helpers.ts +30 -2
  264. package/agent/apps/desktop/src/app/settings/index.tsx +118 -84
  265. package/agent/apps/desktop/src/app/settings/keys-settings.tsx +62 -419
  266. package/agent/apps/desktop/src/app/settings/mcp-settings.tsx +65 -60
  267. package/agent/apps/desktop/src/app/settings/model-settings.test.tsx +129 -5
  268. package/agent/apps/desktop/src/app/settings/model-settings.tsx +370 -65
  269. package/agent/apps/desktop/src/app/settings/notifications-settings.tsx +150 -0
  270. package/agent/apps/desktop/src/app/settings/primitives.tsx +5 -11
  271. package/agent/apps/desktop/src/app/settings/provider-config-panel.test.tsx +142 -0
  272. package/agent/apps/desktop/src/app/settings/provider-config-panel.tsx +182 -0
  273. package/agent/apps/desktop/src/app/settings/providers-settings.test.tsx +171 -0
  274. package/agent/apps/desktop/src/app/settings/providers-settings.tsx +471 -0
  275. package/agent/apps/desktop/src/app/settings/sessions-settings.tsx +183 -71
  276. package/agent/apps/desktop/src/app/settings/toolset-config-panel.test.tsx +135 -1
  277. package/agent/apps/desktop/src/app/settings/toolset-config-panel.tsx +180 -57
  278. package/agent/apps/desktop/src/app/settings/types.ts +9 -6
  279. package/agent/apps/desktop/src/app/settings/uninstall-section.tsx +185 -0
  280. package/agent/apps/desktop/src/app/settings/use-deep-link-highlight.ts +60 -0
  281. package/agent/apps/desktop/src/app/shell/app-shell.tsx +59 -13
  282. package/agent/apps/desktop/src/app/shell/gateway-menu-panel.tsx +37 -32
  283. package/agent/apps/desktop/src/app/shell/hooks/use-overlay-routing.ts +6 -3
  284. package/agent/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx +212 -53
  285. package/agent/apps/desktop/src/app/shell/keybind-panel.tsx +215 -0
  286. package/agent/apps/desktop/src/app/shell/model-edit-submenu.test.tsx +84 -0
  287. package/agent/apps/desktop/src/app/shell/model-edit-submenu.tsx +244 -0
  288. package/agent/apps/desktop/src/app/shell/model-menu-panel.tsx +392 -0
  289. package/agent/apps/desktop/src/app/shell/statusbar-controls.tsx +23 -33
  290. package/agent/apps/desktop/src/app/shell/titlebar-controls.tsx +79 -95
  291. package/agent/apps/desktop/src/app/shell/titlebar.ts +8 -2
  292. package/agent/apps/desktop/src/app/skills/index.test.tsx +11 -0
  293. package/agent/apps/desktop/src/app/skills/index.tsx +79 -64
  294. package/agent/apps/desktop/src/app/types.ts +85 -0
  295. package/agent/apps/desktop/src/app/updates-overlay.tsx +110 -105
  296. package/agent/apps/desktop/src/components/assistant-ui/ansi-text.tsx +34 -0
  297. package/agent/apps/desktop/src/components/assistant-ui/block-direction.test.tsx +129 -0
  298. package/agent/apps/desktop/src/components/assistant-ui/clarify-tool.tsx +102 -81
  299. package/agent/apps/desktop/src/components/assistant-ui/directive-text.tsx +92 -15
  300. package/agent/apps/desktop/src/components/assistant-ui/markdown-text.test.ts +38 -0
  301. package/agent/apps/desktop/src/components/assistant-ui/markdown-text.tsx +304 -45
  302. package/agent/apps/desktop/src/components/assistant-ui/message-render-boundary.test.tsx +80 -0
  303. package/agent/apps/desktop/src/components/assistant-ui/message-render-boundary.tsx +48 -0
  304. package/agent/apps/desktop/src/components/assistant-ui/streaming.test.tsx +142 -90
  305. package/agent/apps/desktop/src/components/assistant-ui/thread-list.tsx +337 -0
  306. package/agent/apps/desktop/src/components/assistant-ui/thread.tsx +667 -190
  307. package/agent/apps/desktop/src/components/assistant-ui/tool-approval-group.test.tsx +299 -0
  308. package/agent/apps/desktop/src/components/assistant-ui/tool-approval.test.tsx +133 -0
  309. package/agent/apps/desktop/src/components/assistant-ui/tool-approval.tsx +239 -0
  310. package/agent/apps/desktop/src/components/assistant-ui/tool-fallback-model.test.ts +31 -0
  311. package/agent/apps/desktop/src/components/assistant-ui/tool-fallback-model.ts +152 -134
  312. package/agent/apps/desktop/src/components/assistant-ui/tool-fallback.tsx +142 -150
  313. package/agent/apps/desktop/src/components/assistant-ui/tooltip-icon-button.tsx +14 -12
  314. package/agent/apps/desktop/src/components/assistant-ui/user-message-edit.test.tsx +141 -0
  315. package/agent/apps/desktop/src/components/assistant-ui/user-message-text.tsx +152 -0
  316. package/agent/apps/desktop/src/components/boot-failure-overlay.tsx +150 -33
  317. package/agent/apps/desktop/src/components/boot-failure-reauth.test.ts +100 -0
  318. package/agent/apps/desktop/src/components/boot-failure-reauth.ts +81 -0
  319. package/agent/apps/desktop/src/components/brand-mark.tsx +19 -0
  320. package/agent/apps/desktop/src/components/chat/code-card.tsx +1 -1
  321. package/agent/apps/desktop/src/components/chat/composer-dock.ts +31 -0
  322. package/agent/apps/desktop/src/components/chat/diff-lines.tsx +1 -1
  323. package/agent/apps/desktop/src/components/chat/disclosure-row.tsx +13 -3
  324. package/agent/apps/desktop/src/components/chat/expandable-block.tsx +52 -0
  325. package/agent/apps/desktop/src/components/chat/generated-image-result.tsx +174 -0
  326. package/agent/apps/desktop/src/components/chat/image-generation-placeholder.tsx +70 -37
  327. package/agent/apps/desktop/src/components/chat/intro.tsx +8 -7
  328. package/agent/apps/desktop/src/components/chat/preview-attachment.tsx +4 -2
  329. package/agent/apps/desktop/src/components/chat/shiki-highlighter.test.ts +37 -0
  330. package/agent/apps/desktop/src/components/chat/shiki-highlighter.tsx +96 -22
  331. package/agent/apps/desktop/src/components/chat/status-row.tsx +70 -0
  332. package/agent/apps/desktop/src/components/chat/status-section.tsx +42 -0
  333. package/agent/apps/desktop/src/components/chat/terminal-output.tsx +54 -0
  334. package/agent/apps/desktop/src/components/chat/zoomable-image.tsx +70 -109
  335. package/agent/apps/desktop/src/components/desktop-install-overlay.tsx +154 -84
  336. package/agent/apps/desktop/src/components/desktop-onboarding-overlay.test.tsx +38 -8
  337. package/agent/apps/desktop/src/components/desktop-onboarding-overlay.tsx +789 -233
  338. package/agent/apps/desktop/src/components/error-boundary.tsx +77 -0
  339. package/agent/apps/desktop/src/components/gateway-connecting-overlay.test.tsx +144 -0
  340. package/agent/apps/desktop/src/components/gateway-connecting-overlay.tsx +7 -1
  341. package/agent/apps/desktop/src/components/haptics-provider.tsx +24 -0
  342. package/agent/apps/desktop/src/components/language-switcher.test.tsx +53 -0
  343. package/agent/apps/desktop/src/components/language-switcher.tsx +175 -0
  344. package/agent/apps/desktop/src/components/model-picker.tsx +42 -40
  345. package/agent/apps/desktop/src/components/model-visibility-dialog.tsx +166 -0
  346. package/agent/apps/desktop/src/components/notifications.tsx +48 -27
  347. package/agent/apps/desktop/src/components/pane-shell/index.ts +1 -1
  348. package/agent/apps/desktop/src/components/pane-shell/pane-shell.tsx +146 -9
  349. package/agent/apps/desktop/src/components/prompt-overlays.tsx +234 -0
  350. package/agent/apps/desktop/src/components/session-picker.tsx +108 -0
  351. package/agent/apps/desktop/src/components/ui/action-status.tsx +25 -0
  352. package/agent/apps/desktop/src/components/ui/badge.tsx +35 -0
  353. package/agent/apps/desktop/src/components/ui/button.tsx +37 -13
  354. package/agent/apps/desktop/src/components/ui/confirm-dialog.tsx +109 -0
  355. package/agent/apps/desktop/src/components/ui/control.ts +25 -0
  356. package/agent/apps/desktop/src/components/ui/copy-button.test.tsx +36 -0
  357. package/agent/apps/desktop/src/components/ui/copy-button.tsx +38 -27
  358. package/agent/apps/desktop/src/components/ui/dialog.tsx +39 -11
  359. package/agent/apps/desktop/src/components/ui/dropdown-menu.tsx +98 -24
  360. package/agent/apps/desktop/src/components/ui/error-state.tsx +50 -0
  361. package/agent/apps/desktop/src/components/ui/fade-text.tsx +9 -2
  362. package/agent/apps/desktop/src/components/ui/{braille-spinner.tsx → glyph-spinner.tsx} +15 -13
  363. package/agent/apps/desktop/src/components/ui/input.tsx +5 -2
  364. package/agent/apps/desktop/src/components/ui/kbd.tsx +83 -12
  365. package/agent/apps/desktop/src/components/ui/log-view.tsx +19 -0
  366. package/agent/apps/desktop/src/components/ui/pagination.tsx +12 -5
  367. package/agent/apps/desktop/src/components/ui/popover.tsx +44 -0
  368. package/agent/apps/desktop/src/components/ui/search-field.tsx +80 -0
  369. package/agent/apps/desktop/src/components/ui/segmented-control.tsx +51 -0
  370. package/agent/apps/desktop/src/components/ui/select.tsx +10 -3
  371. package/agent/apps/desktop/src/components/ui/sheet.tsx +8 -2
  372. package/agent/apps/desktop/src/components/ui/sidebar.tsx +18 -25
  373. package/agent/apps/desktop/src/components/ui/switch.tsx +38 -15
  374. package/agent/apps/desktop/src/components/ui/textarea.tsx +4 -11
  375. package/agent/apps/desktop/src/components/ui/tool-icon.tsx +65 -0
  376. package/agent/apps/desktop/src/components/ui/tooltip.tsx +31 -4
  377. package/agent/apps/desktop/src/fonts/JetBrainsMono-Bold.woff2 +0 -0
  378. package/agent/apps/desktop/src/fonts/JetBrainsMono-Italic.woff2 +0 -0
  379. package/agent/apps/desktop/src/fonts/JetBrainsMono-Regular.woff2 +0 -0
  380. package/agent/apps/desktop/src/global.d.ts +181 -4
  381. package/agent/apps/desktop/src/hermes.test.ts +60 -0
  382. package/agent/apps/desktop/src/hermes.ts +190 -13
  383. package/agent/apps/desktop/src/hooks/use-image-download.ts +85 -0
  384. package/agent/apps/desktop/src/hooks/use-resize-observer.ts +13 -4
  385. package/agent/apps/desktop/src/hooks/use-worktree-info.ts +68 -0
  386. package/agent/apps/desktop/src/i18n/catalog.ts +12 -0
  387. package/agent/apps/desktop/src/i18n/context.test.tsx +232 -0
  388. package/agent/apps/desktop/src/i18n/context.tsx +183 -0
  389. package/agent/apps/desktop/src/i18n/define-locale.ts +41 -0
  390. package/agent/apps/desktop/src/i18n/en.ts +1921 -0
  391. package/agent/apps/desktop/src/i18n/index.ts +20 -0
  392. package/agent/apps/desktop/src/i18n/ja.ts +2053 -0
  393. package/agent/apps/desktop/src/i18n/languages.test.ts +43 -0
  394. package/agent/apps/desktop/src/i18n/languages.ts +86 -0
  395. package/agent/apps/desktop/src/i18n/runtime.test.ts +75 -0
  396. package/agent/apps/desktop/src/i18n/runtime.ts +53 -0
  397. package/agent/apps/desktop/src/i18n/types.ts +1559 -0
  398. package/agent/apps/desktop/src/i18n/zh-hant.ts +1992 -0
  399. package/agent/apps/desktop/src/i18n/zh.ts +2099 -0
  400. package/agent/apps/desktop/src/lib/ansi.test.ts +123 -0
  401. package/agent/apps/desktop/src/lib/ansi.ts +186 -0
  402. package/agent/apps/desktop/src/lib/chat-messages.test.ts +79 -0
  403. package/agent/apps/desktop/src/lib/chat-messages.ts +68 -29
  404. package/agent/apps/desktop/src/lib/chat-runtime.test.ts +65 -1
  405. package/agent/apps/desktop/src/lib/chat-runtime.ts +39 -3
  406. package/agent/apps/desktop/src/lib/completion-sound.ts +519 -0
  407. package/agent/apps/desktop/src/lib/desktop-fs.test.ts +116 -0
  408. package/agent/apps/desktop/src/lib/desktop-fs.ts +113 -0
  409. package/agent/apps/desktop/src/lib/desktop-slash-commands.test.ts +89 -6
  410. package/agent/apps/desktop/src/lib/desktop-slash-commands.ts +270 -131
  411. package/agent/apps/desktop/src/lib/external-link.test.tsx +27 -0
  412. package/agent/apps/desktop/src/lib/external-link.tsx +9 -2
  413. package/agent/apps/desktop/src/lib/gateway-events.test.ts +27 -0
  414. package/agent/apps/desktop/src/lib/gateway-events.ts +16 -0
  415. package/agent/apps/desktop/src/lib/gateway-ws-url.test.ts +78 -0
  416. package/agent/apps/desktop/src/lib/gateway-ws-url.ts +91 -0
  417. package/agent/apps/desktop/src/lib/generated-images.test.ts +97 -0
  418. package/agent/apps/desktop/src/lib/generated-images.ts +116 -0
  419. package/agent/apps/desktop/src/lib/haptics.ts +17 -0
  420. package/agent/apps/desktop/src/lib/icons.ts +10 -2
  421. package/agent/apps/desktop/src/lib/keybinds/actions.ts +137 -0
  422. package/agent/apps/desktop/src/lib/keybinds/combo.test.ts +86 -0
  423. package/agent/apps/desktop/src/lib/keybinds/combo.ts +195 -0
  424. package/agent/apps/desktop/src/lib/local-preview.ts +23 -2
  425. package/agent/apps/desktop/src/lib/markdown-preprocess.ts +20 -7
  426. package/agent/apps/desktop/src/lib/media.remote.test.ts +90 -0
  427. package/agent/apps/desktop/src/lib/media.ts +40 -1
  428. package/agent/apps/desktop/src/lib/model-status-label.test.ts +59 -0
  429. package/agent/apps/desktop/src/lib/model-status-label.ts +122 -0
  430. package/agent/apps/desktop/src/lib/mutable-ref.ts +6 -0
  431. package/agent/apps/desktop/src/lib/profile-color.ts +58 -0
  432. package/agent/apps/desktop/src/lib/query-client.ts +13 -0
  433. package/agent/apps/desktop/src/lib/remend-tail.test.ts +105 -0
  434. package/agent/apps/desktop/src/lib/remend-tail.ts +108 -0
  435. package/agent/apps/desktop/src/lib/session-export.ts +6 -3
  436. package/agent/apps/desktop/src/lib/session-ids.test.ts +44 -0
  437. package/agent/apps/desktop/src/lib/session-ids.ts +26 -0
  438. package/agent/apps/desktop/src/lib/session-search.test.ts +66 -0
  439. package/agent/apps/desktop/src/lib/session-search.ts +21 -0
  440. package/agent/apps/desktop/src/lib/session-source.ts +126 -0
  441. package/agent/apps/desktop/src/lib/storage.test.ts +25 -0
  442. package/agent/apps/desktop/src/lib/storage.ts +35 -1
  443. package/agent/apps/desktop/src/lib/todos.test.ts +46 -1
  444. package/agent/apps/desktop/src/lib/todos.ts +37 -0
  445. package/agent/apps/desktop/src/lib/tool-result-summary.ts +5 -1
  446. package/agent/apps/desktop/src/lib/update-copy.test.ts +38 -0
  447. package/agent/apps/desktop/src/lib/update-copy.ts +44 -0
  448. package/agent/apps/desktop/src/lib/use-enter-animation.ts +2 -2
  449. package/agent/apps/desktop/src/lib/yolo-session.ts +50 -0
  450. package/agent/apps/desktop/src/main.tsx +19 -19
  451. package/agent/apps/desktop/src/store/boot.ts +4 -3
  452. package/agent/apps/desktop/src/store/clarify.test.ts +81 -0
  453. package/agent/apps/desktop/src/store/clarify.ts +50 -13
  454. package/agent/apps/desktop/src/store/command-palette.ts +20 -0
  455. package/agent/apps/desktop/src/store/compaction.test.ts +53 -0
  456. package/agent/apps/desktop/src/store/compaction.ts +38 -0
  457. package/agent/apps/desktop/src/store/completion-sound.ts +32 -0
  458. package/agent/apps/desktop/src/store/composer-input-history.test.ts +147 -0
  459. package/agent/apps/desktop/src/store/composer-input-history.ts +158 -0
  460. package/agent/apps/desktop/src/store/composer-queue.test.ts +68 -0
  461. package/agent/apps/desktop/src/store/composer-queue.ts +76 -0
  462. package/agent/apps/desktop/src/store/composer-status.test.ts +99 -0
  463. package/agent/apps/desktop/src/store/composer-status.ts +277 -0
  464. package/agent/apps/desktop/src/store/composer.test.ts +106 -0
  465. package/agent/apps/desktop/src/store/composer.ts +116 -0
  466. package/agent/apps/desktop/src/store/cron.ts +19 -0
  467. package/agent/apps/desktop/src/store/gateway.ts +280 -6
  468. package/agent/apps/desktop/src/store/keybinds.ts +143 -0
  469. package/agent/apps/desktop/src/store/layout.ts +107 -9
  470. package/agent/apps/desktop/src/store/model-presets.test.ts +51 -0
  471. package/agent/apps/desktop/src/store/model-presets.ts +86 -0
  472. package/agent/apps/desktop/src/store/model-visibility.test.ts +99 -0
  473. package/agent/apps/desktop/src/store/model-visibility.ts +161 -0
  474. package/agent/apps/desktop/src/store/native-notifications.test.ts +192 -0
  475. package/agent/apps/desktop/src/store/native-notifications.ts +203 -0
  476. package/agent/apps/desktop/src/store/notifications.ts +10 -7
  477. package/agent/apps/desktop/src/store/onboarding.test.ts +271 -1
  478. package/agent/apps/desktop/src/store/onboarding.ts +268 -38
  479. package/agent/apps/desktop/src/store/preview.ts +10 -1
  480. package/agent/apps/desktop/src/store/profile.test.ts +89 -0
  481. package/agent/apps/desktop/src/store/profile.ts +395 -0
  482. package/agent/apps/desktop/src/store/prompts.test.ts +127 -0
  483. package/agent/apps/desktop/src/store/prompts.ts +117 -0
  484. package/agent/apps/desktop/src/store/session-switcher.test.ts +115 -0
  485. package/agent/apps/desktop/src/store/session-switcher.ts +128 -0
  486. package/agent/apps/desktop/src/store/session-sync.ts +25 -0
  487. package/agent/apps/desktop/src/store/session.test.ts +268 -2
  488. package/agent/apps/desktop/src/store/session.ts +392 -18
  489. package/agent/apps/desktop/src/store/subagents.ts +3 -0
  490. package/agent/apps/desktop/src/store/system-actions.ts +48 -0
  491. package/agent/apps/desktop/src/store/thread-scroll.ts +58 -5
  492. package/agent/apps/desktop/src/store/todos.test.ts +47 -0
  493. package/agent/apps/desktop/src/store/todos.ts +64 -0
  494. package/agent/apps/desktop/src/store/tool-dismiss.ts +45 -0
  495. package/agent/apps/desktop/src/store/translucency.ts +38 -0
  496. package/agent/apps/desktop/src/store/updates.test.ts +187 -2
  497. package/agent/apps/desktop/src/store/updates.ts +268 -18
  498. package/agent/apps/desktop/src/store/windows.test.ts +143 -0
  499. package/agent/apps/desktop/src/store/windows.ts +115 -0
  500. package/agent/apps/desktop/src/styles.css +510 -119
  501. package/agent/apps/desktop/src/themes/color.ts +142 -0
  502. package/agent/apps/desktop/src/themes/context.tsx +128 -75
  503. package/agent/apps/desktop/src/themes/install.test.ts +119 -0
  504. package/agent/apps/desktop/src/themes/install.ts +95 -0
  505. package/agent/apps/desktop/src/themes/presets.test.ts +33 -0
  506. package/agent/apps/desktop/src/themes/presets.ts +13 -4
  507. package/agent/apps/desktop/src/themes/profile-theme.test.ts +41 -0
  508. package/agent/apps/desktop/src/themes/types.ts +35 -0
  509. package/agent/apps/desktop/src/themes/user-themes.test.ts +63 -0
  510. package/agent/apps/desktop/src/themes/user-themes.ts +122 -0
  511. package/agent/apps/desktop/src/themes/vscode.test.ts +171 -0
  512. package/agent/apps/desktop/src/themes/vscode.ts +343 -0
  513. package/agent/apps/desktop/src/types/hermes.ts +138 -1
  514. package/agent/apps/desktop/tsconfig.json +2 -2
  515. package/agent/apps/desktop/vite.config.ts +18 -0
  516. package/agent/apps/shared/package.json +1 -1
  517. package/agent/apps/shared/src/json-rpc-gateway.ts +63 -2
  518. package/agent/apps/shared/tsconfig.json +2 -2
  519. package/agent/cli-config.yaml.example +78 -1
  520. package/agent/cli.py +2177 -3162
  521. package/agent/cron/blueprint_catalog.py +713 -0
  522. package/agent/cron/jobs.py +226 -110
  523. package/agent/cron/scheduler.py +468 -193
  524. package/agent/cron/scheduler_provider.py +177 -0
  525. package/agent/cron/scripts/__init__.py +1 -0
  526. package/agent/cron/scripts/classify_items.py +226 -0
  527. package/agent/cron/suggestion_catalog.py +154 -0
  528. package/agent/cron/suggestions.py +257 -0
  529. package/agent/docs/chronos-managed-cron-contract.md +196 -0
  530. package/agent/docs/design/profile-builder.md +146 -0
  531. package/agent/docs/middleware/README.md +260 -0
  532. package/agent/docs/observability/README.md +316 -0
  533. package/agent/docs/plans/2026-06-09-003-fix-telegram-stream-overflow-continuations-plan.md +240 -0
  534. package/agent/docs/rca-ssl-cacert-post-git-pull.md +54 -0
  535. package/agent/docs/relay-connector-contract.md +285 -0
  536. package/agent/gateway/authz_mixin.py +536 -0
  537. package/agent/gateway/channel_directory.py +65 -3
  538. package/agent/gateway/config.py +222 -12
  539. package/agent/gateway/display_config.py +10 -0
  540. package/agent/gateway/hooks.py +17 -0
  541. package/agent/gateway/kanban_watchers.py +1146 -0
  542. package/agent/gateway/message_timestamps.py +166 -0
  543. package/agent/gateway/platforms/ADDING_A_PLATFORM.md +29 -0
  544. package/agent/gateway/platforms/api_server.py +216 -38
  545. package/agent/gateway/platforms/base.py +210 -58
  546. package/agent/gateway/platforms/email.py +122 -12
  547. package/agent/gateway/platforms/feishu.py +80 -11
  548. package/agent/gateway/platforms/feishu_meeting_invite.py +212 -0
  549. package/agent/gateway/platforms/matrix.py +1498 -297
  550. package/agent/gateway/platforms/qqbot/adapter.py +6 -0
  551. package/agent/gateway/platforms/signal.py +8 -0
  552. package/agent/gateway/platforms/slack.py +308 -12
  553. package/agent/gateway/platforms/telegram.py +831 -24
  554. package/agent/gateway/platforms/webhook.py +109 -21
  555. package/agent/gateway/platforms/weixin.py +113 -2
  556. package/agent/gateway/platforms/whatsapp.py +94 -288
  557. package/agent/gateway/platforms/whatsapp_cloud.py +1956 -0
  558. package/agent/gateway/platforms/whatsapp_common.py +367 -0
  559. package/agent/gateway/platforms/yuanbao.py +608 -191
  560. package/agent/gateway/platforms/yuanbao_proto.py +232 -23
  561. package/agent/gateway/relay/__init__.py +375 -0
  562. package/agent/gateway/relay/adapter.py +222 -0
  563. package/agent/gateway/relay/auth.py +168 -0
  564. package/agent/gateway/relay/descriptor.py +118 -0
  565. package/agent/gateway/relay/transport.py +101 -0
  566. package/agent/gateway/relay/ws_transport.py +327 -0
  567. package/agent/gateway/response_filters.py +53 -0
  568. package/agent/gateway/rich_sent_store.py +80 -0
  569. package/agent/gateway/run.py +2940 -5001
  570. package/agent/gateway/session.py +109 -8
  571. package/agent/gateway/session_context.py +22 -4
  572. package/agent/gateway/slash_commands.py +3854 -0
  573. package/agent/gateway/status.py +141 -21
  574. package/agent/gateway/stream_consumer.py +288 -31
  575. package/agent/hermes-already-has-routines.md +1 -1
  576. package/agent/hermes_cli/__init__.py +62 -17
  577. package/agent/hermes_cli/_parser.py +30 -0
  578. package/agent/hermes_cli/_subprocess_compat.py +61 -0
  579. package/agent/hermes_cli/active_sessions.py +320 -0
  580. package/agent/hermes_cli/auth.py +707 -59
  581. package/agent/hermes_cli/auth_commands.py +39 -22
  582. package/agent/hermes_cli/backup.py +109 -7
  583. package/agent/hermes_cli/banner.py +88 -0
  584. package/agent/hermes_cli/blueprint_cmd.py +318 -0
  585. package/agent/hermes_cli/cli_agent_setup_mixin.py +684 -0
  586. package/agent/hermes_cli/cli_commands_mixin.py +2293 -0
  587. package/agent/hermes_cli/commands.py +215 -91
  588. package/agent/hermes_cli/config.py +967 -130
  589. package/agent/hermes_cli/container_boot.py +76 -11
  590. package/agent/hermes_cli/cron.py +5 -11
  591. package/agent/hermes_cli/curator.py +21 -0
  592. package/agent/hermes_cli/dashboard_auth/__init__.py +2 -0
  593. package/agent/hermes_cli/dashboard_auth/base.py +62 -0
  594. package/agent/hermes_cli/dashboard_auth/cookies.py +32 -19
  595. package/agent/hermes_cli/dashboard_auth/login_page.py +156 -6
  596. package/agent/hermes_cli/dashboard_auth/middleware.py +28 -4
  597. package/agent/hermes_cli/dashboard_auth/prefix.py +46 -2
  598. package/agent/hermes_cli/dashboard_auth/public_paths.py +6 -0
  599. package/agent/hermes_cli/dashboard_auth/routes.py +158 -2
  600. package/agent/hermes_cli/dashboard_auth/ws_tickets.py +85 -11
  601. package/agent/hermes_cli/dashboard_register.py +427 -0
  602. package/agent/hermes_cli/debug.py +155 -50
  603. package/agent/hermes_cli/doctor.py +255 -14
  604. package/agent/hermes_cli/dump.py +60 -6
  605. package/agent/hermes_cli/env_loader.py +33 -0
  606. package/agent/hermes_cli/gateway.py +755 -103
  607. package/agent/hermes_cli/gateway_enroll.py +250 -0
  608. package/agent/hermes_cli/gateway_windows.py +254 -11
  609. package/agent/hermes_cli/gui_uninstall.py +285 -0
  610. package/agent/hermes_cli/inventory.py +105 -4
  611. package/agent/hermes_cli/kanban.py +58 -71
  612. package/agent/hermes_cli/kanban_db.py +391 -14
  613. package/agent/hermes_cli/kanban_decompose.py +2 -2
  614. package/agent/hermes_cli/kanban_specify.py +3 -1
  615. package/agent/hermes_cli/logs.py +2 -0
  616. package/agent/hermes_cli/main.py +2889 -5287
  617. package/agent/hermes_cli/managed_scope.py +214 -0
  618. package/agent/hermes_cli/managed_uv.py +254 -0
  619. package/agent/hermes_cli/mcp_catalog.py +6 -3
  620. package/agent/hermes_cli/mcp_config.py +145 -21
  621. package/agent/hermes_cli/mcp_security.py +96 -0
  622. package/agent/hermes_cli/mcp_startup.py +32 -3
  623. package/agent/hermes_cli/memory_providers.py +149 -0
  624. package/agent/hermes_cli/memory_setup.py +97 -42
  625. package/agent/hermes_cli/middleware.py +313 -0
  626. package/agent/hermes_cli/model_catalog.py +31 -0
  627. package/agent/hermes_cli/model_cost_guard.py +134 -0
  628. package/agent/hermes_cli/model_normalize.py +2 -1
  629. package/agent/hermes_cli/model_setup_flows.py +2759 -0
  630. package/agent/hermes_cli/model_switch.py +242 -27
  631. package/agent/hermes_cli/models.py +284 -44
  632. package/agent/hermes_cli/nous_account.py +33 -6
  633. package/agent/hermes_cli/nous_billing.py +406 -0
  634. package/agent/hermes_cli/nous_subscription.py +202 -5
  635. package/agent/hermes_cli/platforms.py +1 -0
  636. package/agent/hermes_cli/plugins.py +218 -18
  637. package/agent/hermes_cli/plugins_cmd.py +249 -105
  638. package/agent/hermes_cli/portal_cli.py +56 -16
  639. package/agent/hermes_cli/profile_distribution.py +6 -1
  640. package/agent/hermes_cli/profiles.py +283 -32
  641. package/agent/hermes_cli/provider_catalog.py +170 -0
  642. package/agent/hermes_cli/providers.py +4 -1
  643. package/agent/hermes_cli/pty_bridge.py +53 -4
  644. package/agent/hermes_cli/runtime_provider.py +216 -34
  645. package/agent/hermes_cli/secret_prompt.py +4 -4
  646. package/agent/hermes_cli/secrets_cli.py +24 -0
  647. package/agent/hermes_cli/send_cmd.py +28 -2
  648. package/agent/hermes_cli/service_manager.py +166 -19
  649. package/agent/hermes_cli/session_listing.py +97 -0
  650. package/agent/hermes_cli/setup.py +158 -94
  651. package/agent/hermes_cli/setup_whatsapp_cloud.py +541 -0
  652. package/agent/hermes_cli/skills_config.py +8 -2
  653. package/agent/hermes_cli/skills_hub.py +149 -7
  654. package/agent/hermes_cli/status.py +2 -2
  655. package/agent/hermes_cli/subcommands/__init__.py +18 -0
  656. package/agent/hermes_cli/subcommands/_shared.py +29 -0
  657. package/agent/hermes_cli/subcommands/acp.py +52 -0
  658. package/agent/hermes_cli/subcommands/auth.py +109 -0
  659. package/agent/hermes_cli/subcommands/backup.py +38 -0
  660. package/agent/hermes_cli/subcommands/claw.py +92 -0
  661. package/agent/hermes_cli/subcommands/config.py +49 -0
  662. package/agent/hermes_cli/subcommands/cron.py +163 -0
  663. package/agent/hermes_cli/subcommands/dashboard.py +143 -0
  664. package/agent/hermes_cli/subcommands/debug.py +77 -0
  665. package/agent/hermes_cli/subcommands/doctor.py +35 -0
  666. package/agent/hermes_cli/subcommands/dump.py +28 -0
  667. package/agent/hermes_cli/subcommands/gateway.py +332 -0
  668. package/agent/hermes_cli/subcommands/gui.py +63 -0
  669. package/agent/hermes_cli/subcommands/hooks.py +77 -0
  670. package/agent/hermes_cli/subcommands/import_cmd.py +31 -0
  671. package/agent/hermes_cli/subcommands/insights.py +25 -0
  672. package/agent/hermes_cli/subcommands/login.py +78 -0
  673. package/agent/hermes_cli/subcommands/logout.py +28 -0
  674. package/agent/hermes_cli/subcommands/logs.py +78 -0
  675. package/agent/hermes_cli/subcommands/mcp.py +108 -0
  676. package/agent/hermes_cli/subcommands/memory.py +53 -0
  677. package/agent/hermes_cli/subcommands/model.py +72 -0
  678. package/agent/hermes_cli/subcommands/pairing.py +36 -0
  679. package/agent/hermes_cli/subcommands/plugins.py +94 -0
  680. package/agent/hermes_cli/subcommands/postinstall.py +23 -0
  681. package/agent/hermes_cli/subcommands/profile.py +203 -0
  682. package/agent/hermes_cli/subcommands/prompt_size.py +36 -0
  683. package/agent/hermes_cli/subcommands/security.py +62 -0
  684. package/agent/hermes_cli/subcommands/setup.py +58 -0
  685. package/agent/hermes_cli/subcommands/skills.py +298 -0
  686. package/agent/hermes_cli/subcommands/slack.py +60 -0
  687. package/agent/hermes_cli/subcommands/status.py +28 -0
  688. package/agent/hermes_cli/subcommands/tools.py +95 -0
  689. package/agent/hermes_cli/subcommands/uninstall.py +41 -0
  690. package/agent/hermes_cli/subcommands/update.py +70 -0
  691. package/agent/hermes_cli/subcommands/version.py +18 -0
  692. package/agent/hermes_cli/subcommands/webhook.py +76 -0
  693. package/agent/hermes_cli/subcommands/whatsapp.py +22 -0
  694. package/agent/hermes_cli/suggestions_cmd.py +153 -0
  695. package/agent/hermes_cli/telegram_managed_bot.py +358 -0
  696. package/agent/hermes_cli/tips.py +3 -4
  697. package/agent/hermes_cli/tools_config.py +155 -28
  698. package/agent/hermes_cli/uninstall.py +231 -35
  699. package/agent/hermes_cli/web_server.py +6190 -973
  700. package/agent/hermes_cli/win_pty_bridge.py +179 -0
  701. package/agent/hermes_cli/write_approval_commands.py +209 -0
  702. package/agent/hermes_constants.py +164 -33
  703. package/agent/hermes_logging.py +74 -2
  704. package/agent/hermes_state.py +919 -106
  705. package/agent/hermes_time.py +20 -0
  706. package/agent/locales/af.yaml +23 -0
  707. package/agent/locales/de.yaml +23 -0
  708. package/agent/locales/en.yaml +20 -0
  709. package/agent/locales/es.yaml +23 -0
  710. package/agent/locales/fr.yaml +23 -0
  711. package/agent/locales/ga.yaml +23 -0
  712. package/agent/locales/hu.yaml +23 -0
  713. package/agent/locales/it.yaml +23 -0
  714. package/agent/locales/ja.yaml +23 -0
  715. package/agent/locales/ko.yaml +23 -0
  716. package/agent/locales/pt.yaml +23 -0
  717. package/agent/locales/ru.yaml +23 -0
  718. package/agent/locales/tr.yaml +23 -0
  719. package/agent/locales/uk.yaml +23 -0
  720. package/agent/locales/zh-hant.yaml +23 -0
  721. package/agent/locales/zh.yaml +23 -0
  722. package/agent/model_tools.py +204 -40
  723. package/agent/optional-mcps/clawpump/manifest.yaml +4 -2
  724. package/agent/optional-mcps/clawpump-stdio/manifest.yaml +2 -0
  725. package/agent/optional-mcps/unreal-engine/manifest.yaml +54 -0
  726. package/agent/optional-skills/blockchain/hyperliquid/SKILL.md +2 -2
  727. package/agent/optional-skills/blockchain/hyperliquid/scripts/hyperliquid_client.py +1 -1
  728. package/agent/optional-skills/creative/kanban-video-orchestrator/SKILL.md +1 -1
  729. package/agent/optional-skills/creative/kanban-video-orchestrator/assets/setup.sh.tmpl +4 -3
  730. package/agent/optional-skills/creative/kanban-video-orchestrator/references/kanban-setup.md +6 -4
  731. package/agent/optional-skills/creative/kanban-video-orchestrator/references/tool-matrix.md +2 -2
  732. package/agent/{skills/software-development → optional-skills/devops}/hermes-s6-container-supervision/SKILL.md +2 -0
  733. package/agent/optional-skills/devops/watchers/SKILL.md +1 -1
  734. package/agent/optional-skills/devops/watchers/scripts/watch_github.py +2 -1
  735. package/agent/optional-skills/payments/mpp-agent/SKILL.md +124 -0
  736. package/agent/optional-skills/payments/stripe-link-cli/SKILL.md +184 -0
  737. package/agent/optional-skills/payments/stripe-projects/SKILL.md +120 -0
  738. package/agent/optional-skills/productivity/canvas/SKILL.md +1 -1
  739. package/agent/optional-skills/productivity/canvas/scripts/canvas_api.py +4 -1
  740. package/agent/optional-skills/productivity/shop/SKILL.md +224 -0
  741. package/agent/optional-skills/productivity/shop/references/catalog-mcp.md +236 -0
  742. package/agent/optional-skills/productivity/shop/references/direct-api.md +278 -0
  743. package/agent/optional-skills/productivity/shop/references/legal.md +3 -0
  744. package/agent/optional-skills/productivity/shop/references/safety.md +36 -0
  745. package/agent/optional-skills/productivity/shopify/SKILL.md +1 -1
  746. package/agent/optional-skills/productivity/siyuan/SKILL.md +1 -1
  747. package/agent/optional-skills/productivity/telephony/SKILL.md +4 -4
  748. package/agent/optional-skills/productivity/telephony/scripts/telephony.py +15 -15
  749. package/agent/optional-skills/security/1password/SKILL.md +1 -1
  750. package/agent/{skills/red-teaming → optional-skills/security}/godmode/SKILL.md +3 -4
  751. package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/auto_jailbreak.py +3 -1
  752. package/agent/optional-skills/software-development/rest-graphql-debug/SKILL.md +1 -1
  753. package/agent/{skills → optional-skills}/software-development/subagent-driven-development/SKILL.md +5 -5
  754. package/agent/package-lock.json +4082 -7907
  755. package/agent/package.json +18 -3
  756. package/agent/plugins/browser/firecrawl/provider.py +4 -1
  757. package/agent/plugins/cron/__init__.py +344 -0
  758. package/agent/plugins/cron/chronos/__init__.py +241 -0
  759. package/agent/plugins/cron/chronos/_nas_client.py +123 -0
  760. package/agent/plugins/cron/chronos/plugin.yaml +9 -0
  761. package/agent/plugins/cron/chronos/verify.py +103 -0
  762. package/agent/plugins/dashboard_auth/basic/__init__.py +491 -0
  763. package/agent/plugins/dashboard_auth/basic/plugin.yaml +7 -0
  764. package/agent/plugins/dashboard_auth/nous/__init__.py +12 -14
  765. package/agent/plugins/dashboard_auth/self_hosted/__init__.py +736 -0
  766. package/agent/plugins/dashboard_auth/self_hosted/plugin.yaml +8 -0
  767. package/agent/plugins/disk-cleanup/disk_cleanup.py +100 -20
  768. package/agent/plugins/google_meet/audio_bridge.py +4 -0
  769. package/agent/plugins/google_meet/meet_bot.py +7 -1
  770. package/agent/plugins/hermes-achievements/dashboard/dist/index.js +9 -15
  771. package/agent/plugins/image_gen/fal/__init__.py +35 -6
  772. package/agent/plugins/image_gen/krea/__init__.py +56 -13
  773. package/agent/plugins/image_gen/openai/__init__.py +122 -24
  774. package/agent/plugins/image_gen/openai-codex/__init__.py +28 -2
  775. package/agent/plugins/image_gen/xai/__init__.py +92 -12
  776. package/agent/plugins/kanban/dashboard/dist/index.js +63 -48
  777. package/agent/plugins/kanban/dashboard/plugin_api.py +39 -35
  778. package/agent/plugins/memory/__init__.py +48 -5
  779. package/agent/plugins/memory/byterover/__init__.py +1 -0
  780. package/agent/plugins/memory/hindsight/README.md +1 -1
  781. package/agent/plugins/memory/hindsight/__init__.py +138 -24
  782. package/agent/plugins/memory/hindsight/plugin.yaml +1 -1
  783. package/agent/plugins/memory/honcho/README.md +13 -10
  784. package/agent/plugins/memory/honcho/cli.py +247 -122
  785. package/agent/plugins/memory/honcho/client.py +112 -102
  786. package/agent/plugins/memory/openviking/README.md +12 -1
  787. package/agent/plugins/memory/openviking/__init__.py +2281 -107
  788. package/agent/plugins/memory/openviking/plugin.yaml +1 -2
  789. package/agent/plugins/memory/supermemory/README.md +22 -10
  790. package/agent/plugins/memory/supermemory/__init__.py +142 -37
  791. package/agent/plugins/memory/supermemory/plugin.yaml +1 -1
  792. package/agent/plugins/model-providers/anthropic/__init__.py +1 -0
  793. package/agent/plugins/model-providers/bedrock/__init__.py +1 -0
  794. package/agent/plugins/model-providers/copilot-acp/__init__.py +1 -0
  795. package/agent/plugins/model-providers/custom/__init__.py +8 -2
  796. package/agent/plugins/model-providers/kimi-coding/__init__.py +16 -7
  797. package/agent/plugins/model-providers/minimax/__init__.py +60 -8
  798. package/agent/plugins/model-providers/opencode-zen/__init__.py +12 -3
  799. package/agent/plugins/model-providers/openrouter/__init__.py +75 -4
  800. package/agent/plugins/model-providers/xiaomi/__init__.py +2 -0
  801. package/agent/plugins/model-providers/zai/__init__.py +1 -0
  802. package/agent/plugins/observability/langfuse/__init__.py +147 -14
  803. package/agent/plugins/observability/nemo_relay/README.md +559 -0
  804. package/agent/plugins/observability/nemo_relay/__init__.py +962 -0
  805. package/agent/plugins/observability/nemo_relay/plugin.yaml +20 -0
  806. package/agent/plugins/platforms/discord/adapter.py +932 -61
  807. package/agent/plugins/platforms/discord/voice_mixer.py +379 -0
  808. package/agent/plugins/platforms/google_chat/adapter.py +9 -3
  809. package/agent/plugins/platforms/google_chat/oauth.py +1 -1
  810. package/agent/plugins/platforms/homeassistant/__init__.py +3 -0
  811. package/agent/{gateway/platforms/homeassistant.py → plugins/platforms/homeassistant/adapter.py} +128 -0
  812. package/agent/plugins/platforms/homeassistant/plugin.yaml +22 -0
  813. package/agent/plugins/platforms/irc/adapter.py +4 -1
  814. package/agent/plugins/platforms/line/adapter.py +16 -1
  815. package/agent/plugins/platforms/mattermost/adapter.py +100 -24
  816. package/agent/plugins/platforms/photon/README.md +179 -0
  817. package/agent/plugins/platforms/photon/__init__.py +4 -0
  818. package/agent/plugins/platforms/photon/adapter.py +1586 -0
  819. package/agent/plugins/platforms/photon/auth.py +1046 -0
  820. package/agent/plugins/platforms/photon/cli.py +439 -0
  821. package/agent/plugins/platforms/photon/plugin.yaml +88 -0
  822. package/agent/plugins/platforms/photon/sidecar/README.md +52 -0
  823. package/agent/plugins/platforms/photon/sidecar/index.mjs +720 -0
  824. package/agent/plugins/platforms/photon/sidecar/package-lock.json +1730 -0
  825. package/agent/plugins/platforms/photon/sidecar/package.json +25 -0
  826. package/agent/plugins/platforms/photon/sidecar/patch-spectrum-mixed-attachments.mjs +155 -0
  827. package/agent/plugins/platforms/raft/__init__.py +3 -0
  828. package/agent/plugins/platforms/raft/adapter.py +774 -0
  829. package/agent/plugins/platforms/raft/plugin.yaml +19 -0
  830. package/agent/plugins/platforms/simplex/adapter.py +777 -220
  831. package/agent/plugins/platforms/simplex/plugin.yaml +21 -2
  832. package/agent/plugins/platforms/teams/adapter.py +175 -5
  833. package/agent/plugins/plugin_utils.py +135 -0
  834. package/agent/plugins/video_gen/fal/__init__.py +10 -3
  835. package/agent/plugins/web/searxng/provider.py +15 -2
  836. package/agent/plugins/web/xai/provider.py +2 -2
  837. package/agent/providers/base.py +22 -3
  838. package/agent/pyproject.toml +115 -21
  839. package/agent/run_agent.py +733 -39
  840. package/agent/scripts/build_skills_index.py +51 -19
  841. package/agent/scripts/check_subprocess_stdin.py +177 -0
  842. package/agent/scripts/contributor_audit.py +2 -0
  843. package/agent/scripts/docker_config_migrate.py +67 -0
  844. package/agent/scripts/install.cmd +3 -3
  845. package/agent/scripts/install.ps1 +580 -154
  846. package/agent/scripts/install.sh +402 -185
  847. package/agent/scripts/lib/node-bootstrap.sh +39 -4
  848. package/agent/scripts/release.py +183 -0
  849. package/agent/scripts/run_tests.sh +1 -0
  850. package/agent/scripts/run_tests_parallel.py +18 -23
  851. package/agent/scripts/whatsapp-bridge/bridge.js +25 -4
  852. package/agent/setup.py +59 -0
  853. package/agent/skills/autonomous-ai-agents/codex/SKILL.md +19 -0
  854. package/agent/skills/autonomous-ai-agents/hermes-agent/SKILL.md +10 -3
  855. package/agent/skills/{mcp/native-mcp/SKILL.md → autonomous-ai-agents/hermes-agent/references/native-mcp.md} +0 -13
  856. package/agent/skills/{devops/webhook-subscriptions/SKILL.md → autonomous-ai-agents/hermes-agent/references/webhooks.md} +1 -11
  857. package/agent/skills/clawpump/SKILL.md +4 -1
  858. package/agent/skills/devops/kanban-orchestrator/SKILL.md +1 -0
  859. package/agent/skills/devops/kanban-worker/SKILL.md +1 -0
  860. package/agent/skills/github/github-auth/SKILL.md +2 -2
  861. package/agent/skills/github/github-auth/scripts/gh-env.sh +2 -2
  862. package/agent/skills/github/github-code-review/SKILL.md +2 -2
  863. package/agent/skills/github/github-issues/SKILL.md +2 -2
  864. package/agent/skills/github/github-pr-workflow/SKILL.md +2 -2
  865. package/agent/skills/github/github-repo-management/SKILL.md +2 -2
  866. package/agent/skills/media/gif-search/SKILL.md +1 -1
  867. package/agent/skills/media/youtube-content/SKILL.md +10 -7
  868. package/agent/skills/media/youtube-content/scripts/fetch_transcript.py +3 -3
  869. package/agent/skills/note-taking/obsidian/SKILL.md +1 -1
  870. package/agent/skills/productivity/airtable/SKILL.md +2 -2
  871. package/agent/skills/productivity/google-workspace/scripts/setup.py +33 -7
  872. package/agent/skills/productivity/notion/SKILL.md +2 -2
  873. package/agent/skills/productivity/teams-meeting-pipeline/SKILL.md +1 -1
  874. package/agent/skills/research/llm-wiki/SKILL.md +1 -1
  875. package/agent/skills/social-media/xurl/SKILL.md +9 -0
  876. package/agent/skills/software-development/hermes-agent-skill-authoring/SKILL.md +1 -1
  877. package/agent/skills/software-development/plan/SKILL.md +285 -5
  878. package/agent/skills/software-development/requesting-code-review/SKILL.md +2 -2
  879. package/agent/skills/software-development/simplify-code/SKILL.md +212 -0
  880. package/agent/skills/software-development/spike/SKILL.md +2 -2
  881. package/agent/skills/software-development/systematic-debugging/SKILL.md +1 -1
  882. package/agent/skills/software-development/test-driven-development/SKILL.md +1 -1
  883. package/agent/tools/approval.py +302 -4
  884. package/agent/tools/async_delegation.py +386 -0
  885. package/agent/tools/blueprints.py +325 -0
  886. package/agent/tools/browser_cdp_tool.py +3 -3
  887. package/agent/tools/browser_tool.py +34 -6
  888. package/agent/tools/checkpoint_manager.py +31 -1
  889. package/agent/tools/clarify_tool.py +55 -5
  890. package/agent/tools/code_execution_tool.py +31 -14
  891. package/agent/tools/computer_use/cua_backend.py +81 -3
  892. package/agent/tools/computer_use/tool.py +79 -5
  893. package/agent/tools/computer_use/vision_routing.py +55 -3
  894. package/agent/tools/credential_files.py +31 -12
  895. package/agent/tools/cronjob_tools.py +30 -20
  896. package/agent/tools/delegate_tool.py +356 -31
  897. package/agent/tools/env_probe.py +1 -0
  898. package/agent/tools/environments/docker.py +163 -8
  899. package/agent/tools/environments/file_sync.py +2 -1
  900. package/agent/tools/environments/local.py +74 -23
  901. package/agent/tools/environments/singularity.py +4 -1
  902. package/agent/tools/environments/ssh.py +78 -11
  903. package/agent/tools/file_operations.py +277 -41
  904. package/agent/tools/file_tools.py +166 -28
  905. package/agent/tools/image_generation_tool.py +515 -29
  906. package/agent/tools/kanban_tools.py +99 -0
  907. package/agent/tools/lazy_deps.py +33 -2
  908. package/agent/tools/mcp_oauth.py +5 -5
  909. package/agent/tools/mcp_oauth_manager.py +7 -5
  910. package/agent/tools/mcp_tool.py +840 -33
  911. package/agent/tools/memory_tool.py +335 -38
  912. package/agent/tools/osv_check.py +15 -1
  913. package/agent/tools/process_registry.py +155 -11
  914. package/agent/tools/read_extract.py +248 -0
  915. package/agent/tools/read_terminal_tool.py +93 -0
  916. package/agent/tools/schema_sanitizer.py +38 -0
  917. package/agent/tools/send_message_tool.py +163 -49
  918. package/agent/tools/session_search_tool.py +189 -7
  919. package/agent/tools/skill_manager_tool.py +202 -3
  920. package/agent/tools/skill_usage.py +52 -4
  921. package/agent/tools/skills_hub.py +184 -44
  922. package/agent/tools/skills_sync.py +232 -5
  923. package/agent/tools/skills_tool.py +125 -11
  924. package/agent/tools/terminal_tool.py +148 -26
  925. package/agent/tools/tirith_security.py +2 -0
  926. package/agent/tools/todo_tool.py +32 -1
  927. package/agent/tools/transcription_tools.py +13 -5
  928. package/agent/tools/tts_tool.py +332 -38
  929. package/agent/tools/url_safety.py +52 -1
  930. package/agent/tools/vision_tools.py +124 -39
  931. package/agent/tools/voice_mode.py +4 -3
  932. package/agent/tools/web_tools.py +45 -15
  933. package/agent/tools/write_approval.py +493 -0
  934. package/agent/toolsets.py +34 -10
  935. package/agent/trajectory_compressor.py +81 -10
  936. package/agent/tui_gateway/entry.py +43 -6
  937. package/agent/tui_gateway/server.py +3335 -330
  938. package/agent/tui_gateway/slash_worker.py +61 -0
  939. package/agent/tui_gateway/ws.py +67 -9
  940. package/agent/ui-tui/eslint.config.mjs +0 -4
  941. package/agent/ui-tui/package.json +6 -6
  942. package/agent/ui-tui/packages/hermes-ink/package.json +1 -1
  943. package/agent/ui-tui/packages/hermes-ink/src/ink/app-mouse.test.ts +34 -1
  944. package/agent/ui-tui/packages/hermes-ink/src/ink/app-rawmode-mouse.test.ts +91 -0
  945. package/agent/ui-tui/packages/hermes-ink/src/ink/components/App.tsx +35 -2
  946. package/agent/ui-tui/packages/hermes-ink/src/ink/events/input-event.ts +4 -11
  947. package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.test.ts +23 -57
  948. package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.ts +11 -135
  949. package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.test.ts +185 -0
  950. package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.ts +37 -3
  951. package/agent/ui-tui/packages/hermes-ink/src/utils/execFileNoThrow.ts +5 -5
  952. package/agent/ui-tui/src/__tests__/appChromeStatusRule.test.tsx +217 -0
  953. package/agent/ui-tui/src/__tests__/appChromeStatusRuleDevCredits.test.tsx +73 -0
  954. package/agent/ui-tui/src/__tests__/approvalAction.test.ts +11 -0
  955. package/agent/ui-tui/src/__tests__/billingCommand.test.ts +301 -0
  956. package/agent/ui-tui/src/__tests__/blockLayout.test.ts +122 -0
  957. package/agent/ui-tui/src/__tests__/brandingMcpCount.test.ts +111 -0
  958. package/agent/ui-tui/src/__tests__/completionApply.test.ts +51 -0
  959. package/agent/ui-tui/src/__tests__/createGatewayEventHandler.test.ts +487 -2
  960. package/agent/ui-tui/src/__tests__/createSlashHandler.test.ts +54 -0
  961. package/agent/ui-tui/src/__tests__/creditsCommand.test.ts +144 -0
  962. package/agent/ui-tui/src/__tests__/gatewayClient.test.ts +120 -99
  963. package/agent/ui-tui/src/__tests__/gracefulExit.test.ts +11 -0
  964. package/agent/ui-tui/src/__tests__/memoryMonitor.test.ts +102 -0
  965. package/agent/ui-tui/src/__tests__/paths.test.ts +41 -1
  966. package/agent/ui-tui/src/__tests__/terminalModes.test.ts +22 -0
  967. package/agent/ui-tui/src/__tests__/text.test.ts +23 -0
  968. package/agent/ui-tui/src/__tests__/textInputFastEcho.test.ts +37 -0
  969. package/agent/ui-tui/src/__tests__/turnControllerNotice.test.ts +43 -0
  970. package/agent/ui-tui/src/__tests__/useInputHandlers.test.ts +38 -1
  971. package/agent/ui-tui/src/__tests__/virtualHeights.test.ts +8 -0
  972. package/agent/ui-tui/src/app/createGatewayEventHandler.ts +102 -7
  973. package/agent/ui-tui/src/app/interfaces.ts +64 -1
  974. package/agent/ui-tui/src/app/overlayStore.ts +18 -2
  975. package/agent/ui-tui/src/app/slash/commands/billing.ts +332 -0
  976. package/agent/ui-tui/src/app/slash/commands/core.ts +31 -2
  977. package/agent/ui-tui/src/app/slash/commands/credits.ts +57 -0
  978. package/agent/ui-tui/src/app/slash/commands/ops.ts +28 -0
  979. package/agent/ui-tui/src/app/slash/commands/session.ts +32 -4
  980. package/agent/ui-tui/src/app/slash/registry.ts +4 -0
  981. package/agent/ui-tui/src/app/turnController.ts +145 -2
  982. package/agent/ui-tui/src/app/uiStore.ts +2 -0
  983. package/agent/ui-tui/src/app/useInputHandlers.ts +42 -4
  984. package/agent/ui-tui/src/app/useMainApp.ts +54 -8
  985. package/agent/ui-tui/src/app/useSessionLifecycle.ts +40 -31
  986. package/agent/ui-tui/src/app/useSubmission.ts +23 -31
  987. package/agent/ui-tui/src/components/appChrome.tsx +112 -5
  988. package/agent/ui-tui/src/components/appLayout.tsx +9 -0
  989. package/agent/ui-tui/src/components/appOverlays.tsx +25 -1
  990. package/agent/ui-tui/src/components/billingOverlay.tsx +684 -0
  991. package/agent/ui-tui/src/components/branding.tsx +15 -3
  992. package/agent/ui-tui/src/components/messageLine.tsx +25 -3
  993. package/agent/ui-tui/src/components/pluginsHub.tsx +238 -0
  994. package/agent/ui-tui/src/components/prompts.tsx +31 -17
  995. package/agent/ui-tui/src/components/streamingAssistant.tsx +63 -55
  996. package/agent/ui-tui/src/components/textInput.tsx +16 -0
  997. package/agent/ui-tui/src/config/env.ts +12 -0
  998. package/agent/ui-tui/src/config/limits.ts +13 -0
  999. package/agent/ui-tui/src/domain/blockLayout.ts +146 -0
  1000. package/agent/ui-tui/src/domain/paths.ts +24 -0
  1001. package/agent/ui-tui/src/domain/slash.ts +40 -0
  1002. package/agent/ui-tui/src/entry.tsx +35 -4
  1003. package/agent/ui-tui/src/gatewayClient.ts +22 -10
  1004. package/agent/ui-tui/src/gatewayTypes.ts +130 -1
  1005. package/agent/ui-tui/src/lib/gracefulExit.ts +24 -4
  1006. package/agent/ui-tui/src/lib/memory.test.ts +162 -0
  1007. package/agent/ui-tui/src/lib/memory.ts +60 -1
  1008. package/agent/ui-tui/src/lib/memoryMonitor.ts +79 -4
  1009. package/agent/ui-tui/src/lib/osc52.ts +1 -1
  1010. package/agent/ui-tui/src/lib/text.test.ts +32 -1
  1011. package/agent/ui-tui/src/lib/text.ts +29 -2
  1012. package/agent/ui-tui/src/lib/virtualHeights.ts +13 -0
  1013. package/agent/ui-tui/src/types.ts +5 -0
  1014. package/agent/ui-tui/tsconfig.build.json +0 -1
  1015. package/agent/ui-tui/tsconfig.json +2 -1
  1016. package/agent/utils.py +66 -2
  1017. package/agent/uv.lock +300 -684
  1018. package/agent/web/index.html +2 -2
  1019. package/agent/web/package.json +11 -6
  1020. package/agent/web/public/claw-bg.webp +0 -0
  1021. package/agent/web/public/claw-logo.webp +0 -0
  1022. package/agent/web/src/App.tsx +138 -48
  1023. package/agent/web/src/components/AutomationBlueprints.tsx +225 -0
  1024. package/agent/web/src/components/Backdrop.tsx +15 -0
  1025. package/agent/web/src/components/ChatSessionList.tsx +260 -0
  1026. package/agent/web/src/components/ChatSidebar.tsx +262 -78
  1027. package/agent/web/src/components/ConfirmDialog.tsx +122 -0
  1028. package/agent/web/src/components/ModelPickerDialog.tsx +111 -16
  1029. package/agent/web/src/components/ModelReloadConfirm.tsx +40 -0
  1030. package/agent/web/src/components/ProfileScopeBanner.tsx +30 -0
  1031. package/agent/web/src/components/ProfileSwitcher.tsx +67 -0
  1032. package/agent/web/src/components/ReasoningPicker.tsx +167 -0
  1033. package/agent/web/src/components/SkillEditorDialog.tsx +215 -0
  1034. package/agent/web/src/components/ThemeSwitcher.tsx +119 -4
  1035. package/agent/web/src/components/ToolsetConfigDrawer.tsx +457 -0
  1036. package/agent/web/src/contexts/PageHeaderProvider.tsx +7 -4
  1037. package/agent/web/src/contexts/ProfileProvider.tsx +137 -0
  1038. package/agent/web/src/contexts/SystemActions.tsx +6 -8
  1039. package/agent/web/src/contexts/profile-context.ts +19 -0
  1040. package/agent/web/src/contexts/useProfileScope.ts +6 -0
  1041. package/agent/web/src/i18n/af.ts +5 -4
  1042. package/agent/web/src/i18n/de.ts +5 -4
  1043. package/agent/web/src/i18n/en.ts +58 -4
  1044. package/agent/web/src/i18n/es.ts +5 -3
  1045. package/agent/web/src/i18n/fr.ts +5 -3
  1046. package/agent/web/src/i18n/ga.ts +5 -4
  1047. package/agent/web/src/i18n/hu.ts +5 -4
  1048. package/agent/web/src/i18n/it.ts +5 -4
  1049. package/agent/web/src/i18n/ja.ts +5 -4
  1050. package/agent/web/src/i18n/ko.ts +5 -4
  1051. package/agent/web/src/i18n/pt.ts +5 -3
  1052. package/agent/web/src/i18n/ru.ts +5 -4
  1053. package/agent/web/src/i18n/tr.ts +5 -4
  1054. package/agent/web/src/i18n/types.ts +59 -1
  1055. package/agent/web/src/i18n/uk.ts +5 -3
  1056. package/agent/web/src/i18n/zh-hant.ts +5 -4
  1057. package/agent/web/src/i18n/zh.ts +5 -4
  1058. package/agent/web/src/index.css +2 -2
  1059. package/agent/web/src/lib/api.ts +819 -52
  1060. package/agent/web/src/lib/dashboard-flags.ts +16 -7
  1061. package/agent/web/src/lib/reasoning-effort.test.ts +48 -0
  1062. package/agent/web/src/lib/reasoning-effort.ts +36 -0
  1063. package/agent/web/src/lib/session-refresh.test.ts +21 -0
  1064. package/agent/web/src/lib/session-refresh.ts +26 -0
  1065. package/agent/web/src/pages/ChannelsPage.tsx +529 -68
  1066. package/agent/web/src/pages/ChatPage.tsx +249 -56
  1067. package/agent/web/src/pages/ConfigPage.tsx +11 -1
  1068. package/agent/web/src/pages/CronPage.tsx +219 -31
  1069. package/agent/web/src/pages/EnvPage.tsx +25 -6
  1070. package/agent/web/src/pages/FilesPage.tsx +525 -0
  1071. package/agent/web/src/pages/McpPage.tsx +80 -3
  1072. package/agent/web/src/pages/ModelsPage.tsx +97 -12
  1073. package/agent/web/src/pages/PluginsPage.tsx +1 -1
  1074. package/agent/web/src/pages/ProfileBuilderPage.tsx +611 -0
  1075. package/agent/web/src/pages/ProfilesPage.tsx +1038 -172
  1076. package/agent/web/src/pages/SessionsPage.tsx +144 -13
  1077. package/agent/web/src/pages/SkillsPage.tsx +851 -70
  1078. package/agent/web/src/pages/SystemPage.tsx +340 -4
  1079. package/agent/web/src/pages/WalletPage.tsx +401 -0
  1080. package/agent/web/src/pages/WebhooksPage.tsx +145 -15
  1081. package/agent/web/src/pages/X402Page.tsx +207 -0
  1082. package/agent/web/src/plugins/registry.ts +28 -11
  1083. package/agent/web/src/plugins/sdk.d.ts +160 -0
  1084. package/agent/web/src/themes/context.tsx +112 -5
  1085. package/agent/web/src/themes/fonts.ts +167 -0
  1086. package/agent/web/src/themes/index.ts +7 -0
  1087. package/agent/web/tsconfig.app.json +0 -1
  1088. package/agent/web/vite.config.ts +1 -8
  1089. package/agent/web/vitest.config.ts +16 -0
  1090. package/package.json +1 -1
  1091. package/agent/apps/desktop/package-lock.json +0 -18363
  1092. package/agent/apps/desktop/src/app/chat/composer/skin-slash-popover.tsx +0 -56
  1093. package/agent/apps/desktop/src/components/assistant-ui/thread-virtualizer.tsx +0 -382
  1094. package/agent/apps/desktop/src/components/assistant-ui/todo-tool.tsx +0 -109
  1095. package/agent/apps/desktop/src/components/chat/generated-image-context.tsx +0 -19
  1096. package/agent/optional-skills/productivity/shop-app/SKILL.md +0 -340
  1097. package/agent/skills/autonomous-ai-agents/kanban-codex-lane/SKILL.md +0 -277
  1098. package/agent/skills/autonomous-ai-agents/kanban-codex-lane/templates/pmb-codex-lane-prompt.md +0 -57
  1099. package/agent/skills/diagramming/DESCRIPTION.md +0 -3
  1100. package/agent/skills/domain/DESCRIPTION.md +0 -24
  1101. package/agent/skills/gifs/DESCRIPTION.md +0 -3
  1102. package/agent/skills/inference-sh/DESCRIPTION.md +0 -19
  1103. package/agent/skills/mcp/DESCRIPTION.md +0 -3
  1104. package/agent/skills/media/spotify/SKILL.md +0 -135
  1105. package/agent/skills/mlops/training/DESCRIPTION.md +0 -3
  1106. package/agent/skills/mlops/vector-databases/DESCRIPTION.md +0 -3
  1107. package/agent/skills/productivity/linear/SKILL.md +0 -380
  1108. package/agent/skills/productivity/linear/scripts/linear_api.py +0 -445
  1109. package/agent/skills/software-development/debugging-hermes-tui-commands/SKILL.md +0 -152
  1110. package/agent/skills/software-development/writing-plans/SKILL.md +0 -297
  1111. package/agent/ui-tui/package-lock.json +0 -7449
  1112. package/agent/ui-tui/packages/hermes-ink/package-lock.json +0 -1289
  1113. package/agent/web/package-lock.json +0 -8887
  1114. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/PORT_NOTES.md +0 -0
  1115. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/SKILL.md +0 -0
  1116. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/prompts/system.md +0 -0
  1117. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/macaron.md +0 -0
  1118. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/mono-ink.md +0 -0
  1119. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/neon.md +0 -0
  1120. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/warm.md +0 -0
  1121. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/prompt-construction.md +0 -0
  1122. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/style-presets.md +0 -0
  1123. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/blueprint.md +0 -0
  1124. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/chalkboard.md +0 -0
  1125. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/editorial.md +0 -0
  1126. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/elegant.md +0 -0
  1127. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/fantasy-animation.md +0 -0
  1128. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat-doodle.md +0 -0
  1129. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat.md +0 -0
  1130. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/ink-notes.md +0 -0
  1131. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/intuition-machine.md +0 -0
  1132. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/minimal.md +0 -0
  1133. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/nature.md +0 -0
  1134. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/notion.md +0 -0
  1135. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/pixel-art.md +0 -0
  1136. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/playful.md +0 -0
  1137. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/retro.md +0 -0
  1138. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/scientific.md +0 -0
  1139. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/screen-print.md +0 -0
  1140. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch-notes.md +0 -0
  1141. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch.md +0 -0
  1142. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vector-illustration.md +0 -0
  1143. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vintage.md +0 -0
  1144. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/warm.md +0 -0
  1145. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/watercolor.md +0 -0
  1146. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles.md +0 -0
  1147. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/usage.md +0 -0
  1148. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/workflow.md +0 -0
  1149. /package/agent/{skills → optional-skills}/creative/baoyu-comic/PORT_NOTES.md +0 -0
  1150. /package/agent/{skills → optional-skills}/creative/baoyu-comic/SKILL.md +0 -0
  1151. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/analysis-framework.md +0 -0
  1152. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/chalk.md +0 -0
  1153. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ink-brush.md +0 -0
  1154. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ligne-claire.md +0 -0
  1155. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/manga.md +0 -0
  1156. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/minimalist.md +0 -0
  1157. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/realistic.md +0 -0
  1158. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/auto-selection.md +0 -0
  1159. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/base-prompt.md +0 -0
  1160. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/character-template.md +0 -0
  1161. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/cinematic.md +0 -0
  1162. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/dense.md +0 -0
  1163. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/four-panel.md +0 -0
  1164. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/mixed.md +0 -0
  1165. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/splash.md +0 -0
  1166. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/standard.md +0 -0
  1167. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/webtoon.md +0 -0
  1168. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/ohmsha-guide.md +0 -0
  1169. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/partial-workflows.md +0 -0
  1170. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/concept-story.md +0 -0
  1171. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/four-panel.md +0 -0
  1172. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/ohmsha.md +0 -0
  1173. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/shoujo.md +0 -0
  1174. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/wuxia.md +0 -0
  1175. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/storyboard-template.md +0 -0
  1176. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/action.md +0 -0
  1177. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/dramatic.md +0 -0
  1178. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/energetic.md +0 -0
  1179. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/neutral.md +0 -0
  1180. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/romantic.md +0 -0
  1181. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/vintage.md +0 -0
  1182. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/warm.md +0 -0
  1183. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/workflow.md +0 -0
  1184. /package/agent/{skills → optional-skills}/creative/creative-ideation/SKILL.md +0 -0
  1185. /package/agent/{skills → optional-skills}/creative/creative-ideation/references/full-prompt-library.md +0 -0
  1186. /package/agent/{skills → optional-skills}/creative/pixel-art/ATTRIBUTION.md +0 -0
  1187. /package/agent/{skills → optional-skills}/creative/pixel-art/SKILL.md +0 -0
  1188. /package/agent/{skills → optional-skills}/creative/pixel-art/references/palettes.md +0 -0
  1189. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/__init__.py +0 -0
  1190. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/palettes.py +0 -0
  1191. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art.py +0 -0
  1192. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art_video.py +0 -0
  1193. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/SKILL.md +0 -0
  1194. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/analysis-modules.md +0 -0
  1195. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/methods-guide.md +0 -0
  1196. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/abliteration-config.yaml +0 -0
  1197. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/analysis-study.yaml +0 -0
  1198. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/batch-abliteration.yaml +0 -0
  1199. /package/agent/{skills → optional-skills}/mlops/research/DESCRIPTION.md +0 -0
  1200. /package/agent/{skills → optional-skills}/mlops/research/dspy/SKILL.md +0 -0
  1201. /package/agent/{skills → optional-skills}/mlops/research/dspy/references/examples.md +0 -0
  1202. /package/agent/{skills → optional-skills}/mlops/research/dspy/references/modules.md +0 -0
  1203. /package/agent/{skills → optional-skills}/mlops/research/dspy/references/optimizers.md +0 -0
  1204. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/jailbreak-templates.md +0 -0
  1205. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/refusal-detection.md +0 -0
  1206. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/godmode_race.py +0 -0
  1207. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/load_godmode.py +0 -0
  1208. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/parseltongue.py +0 -0
  1209. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill-subtle.json +0 -0
  1210. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill.json +0 -0
  1211. /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/context-budget-discipline.md +0 -0
  1212. /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/gates-taxonomy.md +0 -0
@@ -29,11 +29,85 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar
29
29
 
30
30
  logger = logging.getLogger(__name__)
31
31
 
32
+ def _delegate_from_json(col: str = "model_config") -> str:
33
+ return f"json_extract(COALESCE({col}, '{{}}'), '$._delegate_from')"
34
+
35
+
36
+ # A child session counts as a /branch (kept visible, never cascade-deleted) if
37
+ # it carries the stable marker OR the legacy end_reason heuristic holds.
38
+ _BRANCH_CHILD_SQL = (
39
+ "json_extract(COALESCE({a}.model_config, '{{}}'), '$._branched_from') IS NOT NULL"
40
+ " OR EXISTS (SELECT 1 FROM sessions p"
41
+ " WHERE p.id = {a}.parent_session_id"
42
+ " AND p.end_reason = 'branched'"
43
+ " AND {a}.started_at >= p.ended_at)"
44
+ )
45
+
46
+ _COMPRESSION_CHILD_SQL = (
47
+ "EXISTS (SELECT 1 FROM sessions p"
48
+ " WHERE p.id = {a}.parent_session_id"
49
+ " AND p.end_reason = 'compression'"
50
+ " AND {a}.started_at >= p.ended_at)"
51
+ )
52
+
53
+ # Rows that surface in pickers: roots + branch children (subagent runs and
54
+ # compression continuations stay hidden).
55
+ _LISTABLE_CHILD_SQL = f"(s.parent_session_id IS NULL OR {_BRANCH_CHILD_SQL.format(a='s')})"
56
+
57
+
58
+ def _ephemeral_child_sql(alias: str = "s") -> str:
59
+ """Subagent runs (cascade-delete targets), not branches or compression tips."""
60
+ branch = _BRANCH_CHILD_SQL.format(a=alias)
61
+ compression = _COMPRESSION_CHILD_SQL.format(a=alias)
62
+ return (
63
+ f"({alias}.parent_session_id IS NOT NULL"
64
+ f" AND NOT ({branch})"
65
+ f" AND NOT ({compression}))"
66
+ )
67
+
68
+
69
+ def _collect_delegate_child_ids(conn, parent_ids: List[str]) -> List[str]:
70
+ """Delegate-subagent ids to cascade-delete with *parent_ids*.
71
+
72
+ Only rows carrying the ``_delegate_from`` marker (set at creation, and
73
+ backfilled by the v16 migration) — generic untagged children keep the
74
+ orphan-don't-delete contract. Walks marker chains recursively so an
75
+ orchestrator subagent's own delegate children go too (FK safety).
76
+ """
77
+ df = _delegate_from_json()
78
+ found: set[str] = set()
79
+ frontier = [sid for sid in parent_ids if sid]
80
+ while frontier:
81
+ ph = ",".join("?" * len(frontier))
82
+ cursor = conn.execute(
83
+ f"SELECT id FROM sessions WHERE {df} IN ({ph}) "
84
+ f"OR (parent_session_id IN ({ph}) AND {df} IS NOT NULL)",
85
+ frontier + frontier,
86
+ )
87
+ frontier = [row["id"] for row in cursor.fetchall() if row["id"] not in found]
88
+ found.update(frontier)
89
+ return list(found)
90
+
91
+
92
+ def _delete_delegate_children(conn, parent_ids: List[str]) -> List[str]:
93
+ ids = _collect_delegate_child_ids(conn, parent_ids)
94
+ if ids:
95
+ ph = ",".join("?" * len(ids))
96
+ conn.execute(f"DELETE FROM messages WHERE session_id IN ({ph})", ids)
97
+ # FK safety: orphan any untagged stragglers pointing at a doomed row.
98
+ conn.execute(
99
+ f"UPDATE sessions SET parent_session_id = NULL "
100
+ f"WHERE parent_session_id IN ({ph})",
101
+ ids,
102
+ )
103
+ conn.execute(f"DELETE FROM sessions WHERE id IN ({ph})", ids)
104
+ return ids
105
+
32
106
  T = TypeVar("T")
33
107
 
34
108
  DEFAULT_DB_PATH = get_hermes_home() / "state.db"
35
109
 
36
- SCHEMA_VERSION = 14
110
+ SCHEMA_VERSION = 16
37
111
 
38
112
  # ---------------------------------------------------------------------------
39
113
  # WAL-compatibility fallback
@@ -226,6 +300,212 @@ def _log_wal_fallback_once(db_label: str, exc: Exception) -> None:
226
300
  exc,
227
301
  )
228
302
 
303
+ # ---------------------------------------------------------------------------
304
+ # Malformed-schema recovery
305
+ # ---------------------------------------------------------------------------
306
+ # A distinct, nastier failure class than a malformed FTS *inverted index*:
307
+ # the ``sqlite_master`` schema table itself becomes inconsistent — most
308
+ # commonly a DUPLICATE object definition, e.g. two ``CREATE VIRTUAL TABLE
309
+ # messages_fts`` rows. SQLite parses the entire schema while preparing the
310
+ # FIRST statement on a connection, so on this class *every* statement raises
311
+ # before it runs — including ``PRAGMA journal_mode`` (which is why this trips
312
+ # in ``apply_wal_with_fallback`` during ``SessionDB.__init__``, long before
313
+ # ``_init_schema`` is reached) and even ``PRAGMA integrity_check`` and a plain
314
+ # ``DROP TABLE``. The only operations that still work are
315
+ # ``PRAGMA writable_schema=ON`` plus direct ``sqlite_master`` surgery.
316
+ #
317
+ # Symptom users hit (Desktop/Dashboard show "no sessions" while 200+ JSON
318
+ # files sit on disk):
319
+ # sqlite3.DatabaseError: malformed database schema (messages_fts) -
320
+ # table messages_fts already exists
321
+ #
322
+ # The canonical ``sessions`` / ``messages`` data is intact in these cases —
323
+ # only the derived schema is broken — so recovery preserves all transcripts
324
+ # and merely rebuilds the FTS layer.
325
+ _MALFORMED_SCHEMA_MARKERS = (
326
+ "malformed database schema",
327
+ "database disk image is malformed",
328
+ )
329
+
330
+ # Process-global guard so auto-repair is attempted at most once per DB path
331
+ # per process (prevents repair loops and serialises concurrent web_server /
332
+ # gateway opens against the same malformed file).
333
+ _repair_attempted_paths: set[str] = set()
334
+ _repair_attempt_lock = threading.Lock()
335
+
336
+
337
+ def is_malformed_db_error(exc: BaseException) -> bool:
338
+ """True if *exc* is a SQLite 'malformed schema / disk image' error.
339
+
340
+ These are the corruption classes where the schema fails to parse, so
341
+ targeted ``sqlite_master`` surgery (not an ordinary FTS rebuild) is the
342
+ only recovery path.
343
+ """
344
+ if not isinstance(exc, sqlite3.DatabaseError):
345
+ return False
346
+ return any(marker in str(exc).lower() for marker in _MALFORMED_SCHEMA_MARKERS)
347
+
348
+
349
+ def _claim_repair_attempt(db_path: Path) -> bool:
350
+ """Claim the one-shot repair attempt for *db_path* in this process.
351
+
352
+ Returns True for the first caller, False afterwards. Keeps a malformed
353
+ DB from triggering an unbounded repair/reopen loop and stops concurrent
354
+ callers from racing surgery on the same file.
355
+ """
356
+ key = str(db_path)
357
+ with _repair_attempt_lock:
358
+ if key in _repair_attempted_paths:
359
+ return False
360
+ _repair_attempted_paths.add(key)
361
+ return True
362
+
363
+
364
+ def _backup_db_file(db_path: Path) -> Optional[Path]:
365
+ """Copy a (possibly malformed) DB file to a timestamped backup beside it.
366
+
367
+ Raw file copy on purpose: the DB won't open cleanly, so we preserve the
368
+ bytes exactly for forensics / manual restore. WAL and SHM sidecars are
369
+ copied too when present. Returns the backup path, or None on failure.
370
+ """
371
+ import datetime
372
+ import shutil
373
+
374
+ stamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
375
+ backup_path = db_path.with_name(f"{db_path.name}.malformed-backup-{stamp}")
376
+ try:
377
+ shutil.copy2(db_path, backup_path)
378
+ for suffix in ("-wal", "-shm"):
379
+ sidecar = db_path.with_name(db_path.name + suffix)
380
+ if sidecar.exists():
381
+ shutil.copy2(sidecar, backup_path.with_name(backup_path.name + suffix))
382
+ return backup_path
383
+ except Exception as exc: # pragma: no cover - best effort
384
+ logger.warning("Could not back up malformed DB %s: %s", db_path, exc)
385
+ return None
386
+
387
+
388
+ def _db_opens_cleanly(db_path: Path) -> Optional[str]:
389
+ """Probe a DB on a fresh connection. Returns None if healthy, else a reason.
390
+
391
+ Runs the same first-statement (``PRAGMA journal_mode``) that trips the
392
+ malformed-schema parse, then ``PRAGMA integrity_check`` and a canonical
393
+ ``sessions`` read.
394
+ """
395
+ conn = sqlite3.connect(str(db_path), isolation_level=None)
396
+ try:
397
+ conn.execute("PRAGMA journal_mode").fetchone()
398
+ rows = conn.execute("PRAGMA integrity_check").fetchall()
399
+ problems = [str(r[0]) for r in rows if r and str(r[0]).lower() != "ok"]
400
+ if problems:
401
+ return "; ".join(problems[:3])
402
+ conn.execute("SELECT COUNT(*) FROM sessions").fetchone()
403
+ return None
404
+ except sqlite3.DatabaseError as exc:
405
+ return str(exc)
406
+ finally:
407
+ conn.close()
408
+
409
+
410
+ def repair_state_db_schema(db_path: Path, *, backup: bool = True) -> Dict[str, Any]:
411
+ """Repair a state.db whose ``sqlite_master`` schema is malformed.
412
+
413
+ Handles the "duplicate object definition" / malformed-schema class where
414
+ even ``PRAGMA`` statements fail. Tries least-destructive recovery first
415
+ and escalates:
416
+
417
+ 1. **De-duplicate** ``sqlite_master`` (keep the lowest rowid per
418
+ ``type``/``name``). Fixes the canonical "table X already exists"
419
+ case and PRESERVES the existing FTS index intact.
420
+ 2. **Drop the FTS schema** (every ``messages_fts*`` object) + ``VACUUM``.
421
+ The next ``SessionDB()`` open rebuilds the FTS indexes from the
422
+ canonical ``messages`` table.
423
+
424
+ Canonical ``sessions`` / ``messages`` rows are never modified. A
425
+ timestamped raw backup is taken first unless ``backup=False``.
426
+
427
+ Returns a report dict: ``{repaired: bool, strategy: str|None,
428
+ backup_path: str|None, error: str|None}``.
429
+ """
430
+ report: Dict[str, Any] = {
431
+ "repaired": False,
432
+ "strategy": None,
433
+ "backup_path": None,
434
+ "error": None,
435
+ }
436
+
437
+ db_path = Path(db_path)
438
+ if not db_path.exists():
439
+ report["error"] = f"{db_path} does not exist"
440
+ return report
441
+
442
+ if backup:
443
+ bpath = _backup_db_file(db_path)
444
+ report["backup_path"] = str(bpath) if bpath else None
445
+
446
+ # ── Strategy 1: de-duplicate sqlite_master (keeps FTS index) ──
447
+ try:
448
+ conn = sqlite3.connect(str(db_path), isolation_level=None)
449
+ try:
450
+ conn.execute("PRAGMA writable_schema=ON")
451
+ dupes = conn.execute(
452
+ "SELECT type, name, COUNT(*) AS c, MIN(rowid) AS keep "
453
+ "FROM sqlite_master GROUP BY type, name HAVING c > 1"
454
+ ).fetchall()
455
+ for type_, name, _count, keep in dupes:
456
+ conn.execute(
457
+ "DELETE FROM sqlite_master "
458
+ "WHERE type IS ? AND name IS ? AND rowid <> ?",
459
+ (type_, name, keep),
460
+ )
461
+ conn.execute("PRAGMA writable_schema=OFF")
462
+ conn.commit()
463
+ finally:
464
+ conn.close()
465
+ if _db_opens_cleanly(db_path) is None:
466
+ report["repaired"] = True
467
+ report["strategy"] = "dedup_schema"
468
+ logger.warning(
469
+ "state.db schema repaired by de-duplicating sqlite_master "
470
+ "(FTS index preserved): %s", db_path
471
+ )
472
+ return report
473
+ except sqlite3.DatabaseError as exc:
474
+ logger.warning("state.db dedup repair pass failed: %s", exc)
475
+
476
+ # ── Strategy 2: drop all FTS schema, VACUUM, rebuild on next open ──
477
+ try:
478
+ conn = sqlite3.connect(str(db_path), isolation_level=None)
479
+ try:
480
+ conn.execute("PRAGMA writable_schema=ON")
481
+ conn.execute("DELETE FROM sqlite_master WHERE name LIKE 'messages_fts%'")
482
+ conn.execute("PRAGMA writable_schema=OFF")
483
+ conn.commit()
484
+ conn.execute("VACUUM")
485
+ finally:
486
+ conn.close()
487
+ reason = _db_opens_cleanly(db_path)
488
+ if reason is None:
489
+ report["repaired"] = True
490
+ report["strategy"] = "drop_fts_rebuild"
491
+ logger.warning(
492
+ "state.db schema repaired by dropping FTS schema; indexes "
493
+ "will rebuild from messages on next open: %s", db_path
494
+ )
495
+ return report
496
+ report["error"] = reason
497
+ except sqlite3.DatabaseError as exc:
498
+ report["error"] = str(exc)
499
+
500
+ if not report["repaired"]:
501
+ logger.error(
502
+ "state.db schema repair could not recover %s automatically "
503
+ "(backup: %s); manual restore from backup may be required.",
504
+ db_path, report["backup_path"],
505
+ )
506
+ return report
507
+
508
+
229
509
  SCHEMA_SQL = """
230
510
  CREATE TABLE IF NOT EXISTS schema_version (
231
511
  version INTEGER NOT NULL
@@ -302,6 +582,7 @@ CREATE TABLE IF NOT EXISTS compression_locks (
302
582
  );
303
583
 
304
584
  CREATE INDEX IF NOT EXISTS idx_sessions_source ON sessions(source);
585
+ CREATE INDEX IF NOT EXISTS idx_sessions_source_id ON sessions(source, id);
305
586
  CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id);
306
587
  CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at DESC);
307
588
  CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, timestamp);
@@ -396,32 +677,81 @@ class SessionDB:
396
677
  # Attempt a PASSIVE WAL checkpoint every N successful writes.
397
678
  _CHECKPOINT_EVERY_N_WRITES = 50
398
679
 
399
- def __init__(self, db_path: Path = None):
680
+ def __init__(self, db_path: Path = None, read_only: bool = False):
400
681
  self.db_path = db_path or DEFAULT_DB_PATH
401
- self.db_path.parent.mkdir(parents=True, exist_ok=True)
682
+ self.read_only = read_only
402
683
 
403
684
  self._lock = threading.Lock()
404
685
  self._write_count = 0
405
686
  self._fts_enabled = False
687
+ self._trigram_available = False
406
688
  self._fts_unavailable_warned = False
689
+ self._conn = None
407
690
  try:
408
- self._conn = sqlite3.connect(
409
- str(self.db_path),
410
- check_same_thread=False,
411
- # Short timeout application-level retry with random jitter
412
- # handles contention instead of sitting in SQLite's internal
413
- # busy handler for up to 30s.
414
- timeout=1.0,
415
- # auto-starts transactions on DML, which conflicts with our
416
- # explicit BEGIN IMMEDIATE. None = we manage transactions
417
- # ourselves.
418
- isolation_level=None,
419
- )
420
- self._conn.row_factory = sqlite3.Row
421
- apply_wal_with_fallback(self._conn, db_label="state.db")
422
- self._conn.execute("PRAGMA foreign_keys=ON")
691
+ if read_only:
692
+ # Read-only attach for cross-profile aggregation: SELECT-only,
693
+ # so we skip schema init entirely (no DDL, no FTS probe, no
694
+ # column reconcile). Crucially this takes NO write lock, so
695
+ # polling another profile's live DB on every sidebar refresh
696
+ # never contends with that profile's running backend. The DB
697
+ # must already exist + be initialised (callers guard on
698
+ # db_path.exists()); a SELECT against an empty file raises and
699
+ # the caller degrades per-profile.
700
+ self._conn = sqlite3.connect(
701
+ f"file:{self.db_path}?mode=ro",
702
+ uri=True,
703
+ check_same_thread=False,
704
+ timeout=1.0,
705
+ isolation_level=None,
706
+ )
707
+ self._conn.row_factory = sqlite3.Row
708
+ return
709
+
710
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
711
+
712
+ def _connect_and_init():
713
+ self._conn = sqlite3.connect(
714
+ str(self.db_path),
715
+ check_same_thread=False,
716
+ # Short timeout — application-level retry with random
717
+ # jitter handles contention instead of sitting in
718
+ # SQLite's internal busy handler for up to 30s.
719
+ timeout=1.0,
720
+ # auto-starts transactions on DML, which conflicts with
721
+ # our explicit BEGIN IMMEDIATE. None = we manage
722
+ # transactions ourselves.
723
+ isolation_level=None,
724
+ )
725
+ self._conn.row_factory = sqlite3.Row
726
+ apply_wal_with_fallback(self._conn, db_label="state.db")
727
+ self._conn.execute("PRAGMA foreign_keys=ON")
728
+ self._init_schema()
423
729
 
424
- self._init_schema()
730
+ try:
731
+ _connect_and_init()
732
+ except sqlite3.DatabaseError as exc:
733
+ # The malformed-schema class (e.g. a duplicate sqlite_master
734
+ # row for messages_fts) fails on the very first statement —
735
+ # before _init_schema can run — so it can't be caught at the
736
+ # FTS-rebuild layer. Recover by repairing sqlite_master in
737
+ # place (backup first; canonical sessions/messages preserved),
738
+ # then reopen once. This is what lets Desktop/Dashboard
739
+ # self-heal instead of silently showing "no sessions".
740
+ if not is_malformed_db_error(exc) or not _claim_repair_attempt(self.db_path):
741
+ raise
742
+ logger.error(
743
+ "state.db schema is malformed (%s) — attempting automatic "
744
+ "repair (a backup copy is made first).", exc,
745
+ )
746
+ try:
747
+ if self._conn is not None:
748
+ self._conn.close()
749
+ except Exception:
750
+ pass
751
+ report = repair_state_db_schema(self.db_path)
752
+ if not report.get("repaired"):
753
+ raise
754
+ _connect_and_init()
425
755
  except Exception as exc:
426
756
  # Capture the cause so /resume and friends can surface WHY the
427
757
  # session DB is unavailable instead of a bare "Session database
@@ -443,7 +773,33 @@ class SessionDB:
443
773
  @staticmethod
444
774
  def _is_fts5_unavailable_error(exc: sqlite3.OperationalError) -> bool:
445
775
  err = str(exc).lower()
446
- return "no such module" in err and "fts5" in err
776
+ if "no such module" in err and "fts5" in err:
777
+ return True
778
+ # SQLite builds that have FTS5 but lack the optional trigram tokenizer
779
+ # raise "no such tokenizer: trigram" instead of "no such module".
780
+ # Scope to trigram specifically to avoid masking unrelated tokenizer errors.
781
+ if "no such tokenizer: trigram" in err:
782
+ return True
783
+ return False
784
+
785
+ @staticmethod
786
+ def _is_trigram_unavailable_error(exc: sqlite3.OperationalError) -> bool:
787
+ """True when only the trigram tokenizer is missing (FTS5 itself works)."""
788
+ return "no such tokenizer: trigram" in str(exc).lower()
789
+
790
+ def _warn_trigram_unavailable(self, exc: sqlite3.OperationalError) -> None:
791
+ """Log once that the trigram tokenizer is missing; base FTS5 stays enabled."""
792
+ if getattr(self, "_trigram_unavailable_warned", False):
793
+ return
794
+ self._trigram_unavailable_warned = True
795
+ logger.info(
796
+ "SQLite trigram tokenizer unavailable for %s "
797
+ "(requires SQLite >= 3.34, this build is %s); "
798
+ "CJK/substring search will fall back to LIKE: %s",
799
+ self.db_path,
800
+ sqlite3.sqlite_version,
801
+ exc,
802
+ )
447
803
 
448
804
  def _warn_fts5_unavailable(self, exc: sqlite3.OperationalError) -> None:
449
805
  self._fts_enabled = False
@@ -452,12 +808,9 @@ class SessionDB:
452
808
  self._fts_unavailable_warned = True
453
809
  logger.warning(
454
810
  "SQLite FTS5 unavailable for %s; full-text session search "
455
- "disabled. This usually means Hermes is running on an "
456
- "unsupported install (e.g. a pip-installed or pip-managed "
457
- "Python whose bundled SQLite lacks FTS5) rather than a "
458
- "mainline install. Some features may be missing or behave "
459
- "differently. Install the supported way: "
460
- "https://hermes-agent.nousresearch.com (underlying error: %s)",
811
+ "disabled. Run `hermes update` to rebuild the venv with a "
812
+ "current Python (managed uv guarantees FTS5). "
813
+ "(underlying error: %s)",
461
814
  self.db_path,
462
815
  exc,
463
816
  )
@@ -492,9 +845,12 @@ class SessionDB:
492
845
  return int(row[0] if not isinstance(row, sqlite3.Row) else row[0])
493
846
 
494
847
  @staticmethod
495
- def _rebuild_fts_indexes(cursor: sqlite3.Cursor) -> None:
496
- for table_name in ("messages_fts", "messages_fts_trigram"):
497
- cursor.execute(f"DELETE FROM {table_name}")
848
+ def _rebuild_fts_indexes(
849
+ cursor: sqlite3.Cursor,
850
+ *,
851
+ include_trigram: bool = True,
852
+ ) -> None:
853
+ cursor.execute("DELETE FROM messages_fts")
498
854
  cursor.execute(
499
855
  "INSERT INTO messages_fts(rowid, content) "
500
856
  "SELECT id, "
@@ -503,6 +859,9 @@ class SessionDB:
503
859
  "COALESCE(tool_calls, '') "
504
860
  "FROM messages"
505
861
  )
862
+ if not include_trigram:
863
+ return
864
+ cursor.execute("DELETE FROM messages_fts_trigram")
506
865
  cursor.execute(
507
866
  "INSERT INTO messages_fts_trigram(rowid, content) "
508
867
  "SELECT id, "
@@ -518,7 +877,12 @@ class SessionDB:
518
877
  return True
519
878
  except sqlite3.OperationalError as exc:
520
879
  if self._is_fts5_unavailable_error(exc):
521
- self._warn_fts5_unavailable(exc)
880
+ # Only disable FTS entirely when the whole module is missing.
881
+ # A missing trigram tokenizer only affects trigram searches.
882
+ if self._is_trigram_unavailable_error(exc):
883
+ self._warn_trigram_unavailable(exc)
884
+ else:
885
+ self._warn_fts5_unavailable(exc)
522
886
  return None
523
887
  if "no such table" in str(exc).lower():
524
888
  return False
@@ -542,7 +906,13 @@ class SessionDB:
542
906
  except sqlite3.OperationalError as exc:
543
907
  if not self._is_fts5_unavailable_error(exc):
544
908
  raise
545
- self._warn_fts5_unavailable(exc)
909
+ # Only disable FTS entirely when the whole FTS5 module is missing.
910
+ # A missing specific tokenizer (e.g. trigram) means only that
911
+ # particular table cannot be created — the base FTS5 table is fine.
912
+ if self._is_trigram_unavailable_error(exc):
913
+ self._warn_trigram_unavailable(exc)
914
+ else:
915
+ self._warn_fts5_unavailable(exc)
546
916
  return False
547
917
 
548
918
  def _execute_write(self, fn: Callable[[sqlite3.Connection], T]) -> T:
@@ -598,17 +968,27 @@ class SessionDB:
598
968
  )
599
969
 
600
970
  def _try_wal_checkpoint(self) -> None:
601
- """Best-effort PASSIVE WAL checkpoint. Never blocks, never raises.
971
+ """Best-effort TRUNCATE WAL checkpoint. Never raises.
602
972
 
603
- Flushes committed WAL frames back into the main DB file for any
604
- frames that no other connection currently needs. Keeps the WAL
605
- from growing unbounded when many processes hold persistent
973
+ Flushes committed WAL frames back into the main DB file and
974
+ truncates the WAL file to zero bytes. Keeps the WAL from
975
+ growing unbounded when many processes hold persistent
606
976
  connections.
977
+
978
+ PASSIVE checkpoint was previously used here, but it never
979
+ truncates the WAL file — the file stays at its high-water
980
+ mark until an explicit TRUNCATE is called (which only
981
+ happened inside the infrequent vacuum()).
982
+
983
+ TRUNCATE may block writers briefly while checkpointing, but
984
+ _try_wal_checkpoint is called off the hot path (every 50
985
+ writes) and already runs under ``self._lock``, so the
986
+ additional hold time is negligible.
607
987
  """
608
988
  try:
609
989
  with self._lock:
610
990
  result = self._conn.execute(
611
- "PRAGMA wal_checkpoint(PASSIVE)"
991
+ "PRAGMA wal_checkpoint(TRUNCATE)"
612
992
  ).fetchone()
613
993
  if result and result[1] > 0:
614
994
  logger.debug(
@@ -621,13 +1001,13 @@ class SessionDB:
621
1001
  def close(self):
622
1002
  """Close the database connection.
623
1003
 
624
- Attempts a PASSIVE WAL checkpoint first so that exiting processes
625
- help keep the WAL file from growing unbounded.
1004
+ Attempts a TRUNCATE WAL checkpoint first so that exiting processes
1005
+ help shrink the WAL file.
626
1006
  """
627
1007
  with self._lock:
628
1008
  if self._conn:
629
1009
  try:
630
- self._conn.execute("PRAGMA wal_checkpoint(PASSIVE)")
1010
+ self._conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
631
1011
  except Exception:
632
1012
  pass
633
1013
  self._conn.close()
@@ -787,11 +1167,16 @@ class SessionDB:
787
1167
  # backfills, index changes tied to a specific version step) stay
788
1168
  # in a version-gated chain. Column additions are handled by
789
1169
  # _reconcile_columns() above and no longer need entries here.
790
- if current_version < 10:
1170
+ if current_version < 10 and SCHEMA_VERSION == 10:
791
1171
  # v10: trigram FTS5 table for CJK/substring search. The
792
1172
  # virtual table + triggers are created unconditionally via
793
1173
  # FTS_TRIGRAM_SQL below, but existing rows need a one-time
794
1174
  # backfill into the FTS index.
1175
+ #
1176
+ # Only run this when v10 itself is the target schema. Current
1177
+ # v11+ code drops and rebuilds both FTS tables below, so doing
1178
+ # the v10-only trigram backfill first only burns startup time
1179
+ # and WAL space before v11 throws the work away.
795
1180
  if fts5_available:
796
1181
  _fts_trigram_exists = self._fts_table_probe(
797
1182
  cursor, "messages_fts_trigram"
@@ -825,21 +1210,23 @@ class SessionDB:
825
1210
  except sqlite3.OperationalError as exc:
826
1211
  if not self._is_fts5_unavailable_error(exc):
827
1212
  raise
828
- self._warn_fts5_unavailable(exc)
829
- fts5_available = False
830
- fts_migrations_complete = False
1213
+ if self._is_trigram_unavailable_error(exc):
1214
+ self._warn_trigram_unavailable(exc)
1215
+ else:
1216
+ self._warn_fts5_unavailable(exc)
1217
+ fts5_available = False
1218
+ fts_migrations_complete = False
831
1219
  break
832
1220
 
833
1221
  if fts5_available:
834
1222
  # Recreate virtual tables + triggers with the new inline-mode
835
1223
  # schema that indexes content || tool_name || tool_calls.
836
- if (
837
- self._ensure_fts_schema(cursor, "messages_fts", FTS_SQL)
838
- and self._ensure_fts_schema(
839
- cursor, "messages_fts_trigram", FTS_TRIGRAM_SQL
840
- )
841
- ):
842
- # Backfill both indexes from every existing messages row.
1224
+ # Handle base and trigram independently — a missing
1225
+ # trigram tokenizer should not prevent base FTS backfill.
1226
+ base_fts_ok = self._ensure_fts_schema(
1227
+ cursor, "messages_fts", FTS_SQL
1228
+ )
1229
+ if base_fts_ok:
843
1230
  cursor.execute(
844
1231
  "INSERT INTO messages_fts(rowid, content) "
845
1232
  "SELECT id, "
@@ -848,6 +1235,10 @@ class SessionDB:
848
1235
  "COALESCE(tool_calls, '') "
849
1236
  "FROM messages"
850
1237
  )
1238
+ trigram_ok = self._ensure_fts_schema(
1239
+ cursor, "messages_fts_trigram", FTS_TRIGRAM_SQL
1240
+ )
1241
+ if trigram_ok:
851
1242
  cursor.execute(
852
1243
  "INSERT INTO messages_fts_trigram(rowid, content) "
853
1244
  "SELECT id, "
@@ -856,8 +1247,12 @@ class SessionDB:
856
1247
  "COALESCE(tool_calls, '') "
857
1248
  "FROM messages"
858
1249
  )
859
- else:
1250
+ if not base_fts_ok:
860
1251
  fts_migrations_complete = False
1252
+ # Track trigram availability for CJK LIKE fallback.
1253
+ self._trigram_available = trigram_ok
1254
+ else:
1255
+ fts_migrations_complete = False
861
1256
  else:
862
1257
  fts_migrations_complete = False
863
1258
  if current_version < 12:
@@ -872,6 +1267,32 @@ class SessionDB:
872
1267
  )
873
1268
  except sqlite3.OperationalError:
874
1269
  pass
1270
+ if current_version < 16:
1271
+ # v16: tag delegate subagent rows so pickers stay clean after
1272
+ # parent deletes that used to orphan them (parent_session_id → NULL).
1273
+ try:
1274
+ cursor.execute(
1275
+ "UPDATE sessions SET model_config = json_set("
1276
+ "COALESCE(model_config, '{}'), '$._delegate_from', parent_session_id) "
1277
+ f"WHERE parent_session_id IS NOT NULL "
1278
+ "AND json_extract(COALESCE(model_config, '{}'), '$._delegate_from') IS NULL "
1279
+ f"AND {_ephemeral_child_sql('sessions')}"
1280
+ )
1281
+ cursor.execute(
1282
+ "UPDATE sessions SET model_config = json_set("
1283
+ "COALESCE(model_config, '{}'), '$._delegate_from', '__orphaned__') "
1284
+ "WHERE parent_session_id IS NULL "
1285
+ "AND json_extract(COALESCE(model_config, '{}'), '$._delegate_from') IS NULL "
1286
+ "AND json_extract(COALESCE(model_config, '{}'), '$._branched_from') IS NULL "
1287
+ "AND title IS NULL "
1288
+ "AND message_count <= 25 "
1289
+ "AND EXISTS (SELECT 1 FROM messages m "
1290
+ " WHERE m.session_id = sessions.id AND m.role = 'tool') "
1291
+ "AND NOT EXISTS (SELECT 1 FROM sessions ch "
1292
+ " WHERE ch.parent_session_id = sessions.id)"
1293
+ )
1294
+ except sqlite3.OperationalError:
1295
+ pass
875
1296
  if current_version < SCHEMA_VERSION and fts_migrations_complete:
876
1297
  cursor.execute(
877
1298
  "UPDATE schema_version SET version = ?",
@@ -901,8 +1322,12 @@ class SessionDB:
901
1322
  trigram_enabled = self._ensure_fts_schema(
902
1323
  cursor, "messages_fts_trigram", FTS_TRIGRAM_SQL
903
1324
  )
904
- if trigram_enabled and triggers_need_repair:
905
- self._rebuild_fts_indexes(cursor)
1325
+ self._trigram_available = trigram_enabled
1326
+ if triggers_need_repair:
1327
+ self._rebuild_fts_indexes(
1328
+ cursor,
1329
+ include_trigram=trigram_enabled,
1330
+ )
906
1331
 
907
1332
  self._conn.commit()
908
1333
 
@@ -1107,6 +1532,24 @@ class SessionDB:
1107
1532
  return None
1108
1533
  return row["holder"] if isinstance(row, sqlite3.Row) else row[0]
1109
1534
 
1535
+ def update_session_meta(
1536
+ self,
1537
+ session_id: str,
1538
+ model_config_json: str,
1539
+ model: Optional[str] = None,
1540
+ ) -> None:
1541
+ """Update model_config and optionally model for an existing session.
1542
+
1543
+ Uses COALESCE so that passing model=None leaves the stored model
1544
+ column unchanged. Routes through _execute_write for the standard
1545
+ BEGIN IMMEDIATE + jitter-retry + lock guarantee.
1546
+ """
1547
+ def _do(conn):
1548
+ conn.execute(
1549
+ "UPDATE sessions SET model_config = ?, model = COALESCE(?, model) WHERE id = ?",
1550
+ (model_config_json, model, session_id),
1551
+ )
1552
+ self._execute_write(_do)
1110
1553
 
1111
1554
  def update_system_prompt(self, session_id: str, system_prompt: str) -> None:
1112
1555
  """Store the full assembled system prompt snapshot."""
@@ -1393,6 +1836,43 @@ class SessionDB:
1393
1836
 
1394
1837
  return cleaned
1395
1838
 
1839
+ def _is_compression_ancestor(
1840
+ self, conn, *, ancestor_id: str, descendant_id: str
1841
+ ) -> bool:
1842
+ """Return True if *ancestor_id* is a compression predecessor of
1843
+ *descendant_id* (walking parent links up the continuation chain).
1844
+
1845
+ The continuation edge is the canonical one shared with
1846
+ :func:`_ephemeral_child_sql` / :meth:`set_session_archived`
1847
+ (``_COMPRESSION_CHILD_SQL``): a parent → child edge counts only when the
1848
+ parent ended with ``end_reason = 'compression'`` and the child started
1849
+ at or after the parent's ``ended_at``, which distinguishes continuations
1850
+ from delegate subagents / branch children that also carry a
1851
+ ``parent_session_id``. Expressed as a single recursive CTE rather than a
1852
+ per-hop Python walk so the edge definition lives in exactly one place.
1853
+ """
1854
+ if not ancestor_id or not descendant_id or ancestor_id == descendant_id:
1855
+ return False
1856
+ # Walk parent links up from the descendant, following only compression
1857
+ # continuation edges, and check whether ancestor_id is reached.
1858
+ edge = _COMPRESSION_CHILD_SQL.format(a="child")
1859
+ row = conn.execute(
1860
+ f"""
1861
+ WITH RECURSIVE ancestors(id) AS (
1862
+ SELECT ?
1863
+ UNION
1864
+ SELECT parent.id
1865
+ FROM ancestors a
1866
+ JOIN sessions child ON child.id = a.id
1867
+ JOIN sessions parent ON parent.id = child.parent_session_id
1868
+ WHERE {edge}
1869
+ )
1870
+ SELECT 1 FROM ancestors WHERE id = ? AND id != ? LIMIT 1
1871
+ """,
1872
+ (descendant_id, ancestor_id, descendant_id),
1873
+ ).fetchone()
1874
+ return row is not None
1875
+
1396
1876
  def set_session_title(self, session_id: str, title: str) -> bool:
1397
1877
  """Set or update a session's title.
1398
1878
 
@@ -1411,9 +1891,29 @@ class SessionDB:
1411
1891
  )
1412
1892
  conflict = cursor.fetchone()
1413
1893
  if conflict:
1414
- raise ValueError(
1415
- f"Title '{title}' is already in use by session {conflict['id']}"
1416
- )
1894
+ conflict_id = conflict["id"]
1895
+ # A compression continuation is the live, projected-forward
1896
+ # head of its conversation; its compressed predecessors are
1897
+ # ended and hidden from the session list (list_sessions_rich
1898
+ # projects roots → tip). When the title that "conflicts" is
1899
+ # held by such a hidden ancestor, the user has no way to free
1900
+ # it — renaming the visible tip back to the base name would
1901
+ # dead-end with "already in use by <session they can't see>".
1902
+ # Treat this as a transfer: move the title off the ancestor
1903
+ # onto the continuation. Uniqueness is preserved (still only
1904
+ # one session carries the exact title) and the parent-link
1905
+ # lineage is untouched.
1906
+ if self._is_compression_ancestor(
1907
+ conn, ancestor_id=conflict_id, descendant_id=session_id
1908
+ ):
1909
+ conn.execute(
1910
+ "UPDATE sessions SET title = NULL WHERE id = ?",
1911
+ (conflict_id,),
1912
+ )
1913
+ else:
1914
+ raise ValueError(
1915
+ f"Title '{title}' is already in use by session {conflict_id}"
1916
+ )
1417
1917
  cursor = conn.execute(
1418
1918
  "UPDATE sessions SET title = ? WHERE id = ?",
1419
1919
  (title, session_id),
@@ -1435,15 +1935,51 @@ class SessionDB:
1435
1935
  """Archive or unarchive a session.
1436
1936
 
1437
1937
  Archived sessions are hidden from the default session list but keep all
1438
- their messages — this is a soft hide, not a delete. Returns True when a
1439
- row was updated.
1938
+ their messages — this is a soft hide, not a delete. For compression
1939
+ chains, archive the whole logical conversation. Desktop lists compression
1940
+ roots projected forward to their latest continuation; updating only the
1941
+ displayed tip lets the still-unarchived root resurrect it on refresh.
1942
+ Returns True when at least one row was updated.
1440
1943
  """
1441
1944
  def _do(conn):
1442
1945
  cursor = conn.execute(
1443
- "UPDATE sessions SET archived = ? WHERE id = ?",
1444
- (1 if archived else 0, session_id),
1946
+ """
1947
+ WITH RECURSIVE
1948
+ ancestors(id) AS (
1949
+ SELECT ?
1950
+ UNION
1951
+ SELECT parent.id
1952
+ FROM ancestors a
1953
+ JOIN sessions child ON child.id = a.id
1954
+ JOIN sessions parent ON parent.id = child.parent_session_id
1955
+ WHERE parent.end_reason = 'compression'
1956
+ AND child.started_at >= parent.ended_at
1957
+ ),
1958
+ descendants(id) AS (
1959
+ SELECT ?
1960
+ UNION
1961
+ SELECT child.id
1962
+ FROM descendants d
1963
+ JOIN sessions parent ON parent.id = d.id
1964
+ JOIN sessions child ON child.parent_session_id = parent.id
1965
+ WHERE parent.end_reason = 'compression'
1966
+ AND child.started_at >= parent.ended_at
1967
+ ),
1968
+ lineage(id) AS (
1969
+ SELECT id FROM ancestors
1970
+ UNION
1971
+ SELECT id FROM descendants
1972
+ )
1973
+ UPDATE sessions
1974
+ SET archived = ?
1975
+ WHERE id IN (SELECT id FROM lineage)
1976
+ """,
1977
+ (session_id, session_id, 1 if archived else 0),
1445
1978
  )
1446
- return cursor.rowcount
1979
+ rowcount = cursor.rowcount
1980
+ if rowcount is None or rowcount < 0:
1981
+ rowcount = conn.execute("SELECT changes()").fetchone()[0]
1982
+ return rowcount
1447
1983
  rowcount = self._execute_write(_do)
1448
1984
  return rowcount > 0
1449
1985
 
@@ -1568,6 +2104,7 @@ class SessionDB:
1568
2104
  order_by_last_active: bool = False,
1569
2105
  include_archived: bool = False,
1570
2106
  archived_only: bool = False,
2107
+ id_query: str = None,
1571
2108
  ) -> List[Dict[str, Any]]:
1572
2109
  """List sessions with preview (first user message) and last active timestamp.
1573
2110
 
@@ -1600,18 +2137,22 @@ class SessionDB:
1600
2137
  params = []
1601
2138
 
1602
2139
  if not include_children:
1603
- # Show root sessions and branch sessions (whose parent ended with
1604
- # end_reason='branched' before the child was created), while still
1605
- # hiding sub-agent runs and compression continuations (which also
1606
- # carry a parent_session_id but were spawned while the parent was
1607
- # still live — i.e., started_at < parent.ended_at).
1608
- where_clauses.append(
1609
- "(s.parent_session_id IS NULL"
1610
- " OR EXISTS (SELECT 1 FROM sessions p"
1611
- " WHERE p.id = s.parent_session_id"
1612
- " AND p.end_reason = 'branched'"
1613
- " AND s.started_at >= p.ended_at))"
1614
- )
2140
+ # Show root sessions and branch sessions, while still hiding
2141
+ # sub-agent runs and compression continuations (which also carry a
2142
+ # parent_session_id but were spawned while the parent was still
2143
+ # live i.e., started_at < parent.ended_at).
2144
+ #
2145
+ # Branch sessions are identified two ways, OR'd for robustness:
2146
+ # 1. A stable ``_branched_from`` marker in model_config, written
2147
+ # by /branch at creation time. This survives the parent being
2148
+ # reopened and re-ended with a different end_reason (e.g.
2149
+ # tui_shutdown overwriting 'branched'), which otherwise hides
2150
+ # the branch see issue #20856.
2151
+ # 2. The legacy heuristic (parent ended with 'branched' before the
2152
+ # child started), covering branch sessions created before the
2153
+ # marker existed.
2154
+ where_clauses.append(_LISTABLE_CHILD_SQL)
2155
+ where_clauses.append(f"{_delegate_from_json('s.model_config')} IS NULL")
1615
2156
 
1616
2157
  if source:
1617
2158
  where_clauses.append("s.source = ?")
@@ -1629,6 +2170,16 @@ class SessionDB:
1629
2170
  where_clauses.append("s.archived = 0")
1630
2171
 
1631
2172
  where_sql = f"WHERE {' AND '.join(where_clauses)}" if where_clauses else ""
2173
+
2174
+ # Optional session-id filter, pushed into SQL so callers (Desktop
2175
+ # session-id search) don't have to fetch every row and filter in
2176
+ # Python. ``id_query`` is matched as a case-insensitive substring
2177
+ # against each surfaced row's id AND every id in its forward
2178
+ # compression chain — so searching a compression *root* id or a *tip*
2179
+ # id both resolve to the same projected conversation. Only used in the
2180
+ # order_by_last_active path (which builds the chain CTE); other callers
2181
+ # pass id_query=None.
2182
+ id_needle = (id_query or "").strip().lower()
1632
2183
  if order_by_last_active:
1633
2184
  # Compute effective_last_active by walking each surfaced session's
1634
2185
  # compression-continuation chain forward in SQL and taking the MAX
@@ -1641,6 +2192,28 @@ class SessionDB:
1641
2192
  # compression-continuation edges using the same criteria as
1642
2193
  # get_compression_tip (parent.end_reason='compression' AND
1643
2194
  # child.started_at >= parent.ended_at).
2195
+ outer_where = where_sql
2196
+ id_params: List[Any] = []
2197
+ if id_needle:
2198
+ # Admit a surfaced row if its own id or any id in its forward
2199
+ # compression chain matches the needle. LIKE with a leading
2200
+ # wildcard can't use an index, but the chain membership and
2201
+ # the small result set keep this bounded — far cheaper than
2202
+ # fetching every session and scanning in Python.
2203
+ id_clause = (
2204
+ "EXISTS (SELECT 1 FROM chain cq"
2205
+ " WHERE cq.root_id = s.id"
2206
+ " AND LOWER(cq.cur_id) LIKE ? ESCAPE '\\')"
2207
+ )
2208
+ like_pattern = (
2209
+ "%"
2210
+ + id_needle.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
2211
+ + "%"
2212
+ )
2213
+ id_params = [like_pattern]
2214
+ outer_where = (
2215
+ f"{where_sql} AND {id_clause}" if where_sql else f"WHERE {id_clause}"
2216
+ )
1644
2217
  query = f"""
1645
2218
  WITH RECURSIVE chain(root_id, cur_id) AS (
1646
2219
  SELECT s.id, s.id FROM sessions s {where_sql}
@@ -1677,12 +2250,13 @@ class SessionDB:
1677
2250
  COALESCE(cm.effective_last_active, s.started_at) AS _effective_last_active
1678
2251
  FROM sessions s
1679
2252
  LEFT JOIN chain_max cm ON cm.root_id = s.id
1680
- {where_sql}
2253
+ {outer_where}
1681
2254
  ORDER BY _effective_last_active DESC, s.started_at DESC, s.id DESC
1682
2255
  LIMIT ? OFFSET ?
1683
2256
  """
1684
- # WHERE params apply twice (CTE seed + outer select).
1685
- params = params + params + [limit, offset]
2257
+ # WHERE params apply twice (CTE seed + outer select); the id filter
2258
+ # only applies to the outer select.
2259
+ params = params + params + id_params + [limit, offset]
1686
2260
  else:
1687
2261
  query = f"""
1688
2262
  SELECT s.*,
@@ -1756,6 +2330,72 @@ class SessionDB:
1756
2330
 
1757
2331
  return sessions
1758
2332
 
2333
+ def list_cron_job_runs(
2334
+ self,
2335
+ job_id: str,
2336
+ limit: int = 20,
2337
+ offset: int = 0,
2338
+ ) -> List[Dict[str, Any]]:
2339
+ """List the run sessions produced by a single cron job, newest first.
2340
+
2341
+ Cron runs are flat, independent sessions whose id is
2342
+ ``cron_{job_id}_{timestamp}`` (see ``cron/scheduler.run_job``). They are
2343
+ never compression roots and never branch, so this deliberately skips the
2344
+ ``list_sessions_rich`` recursive compression-chain CTE / leading-wildcard
2345
+ ``id_query`` path — that path seeds from *every* ``source='cron'`` row in
2346
+ the DB and only filters to one job's runs after the scan, so it scales
2347
+ with the whole cron pile (a heavy history makes the desktop run-history
2348
+ endpoint time out before it eventually populates).
2349
+
2350
+ Instead this binds to one job with a ``[prefix, prefix_hi)`` range over
2351
+ the id (an index range scan, not a ``%...%`` substring), filters
2352
+ ``source='cron'``, and orders by ``started_at DESC``. Work scales with
2353
+ the requested window, not the total cron history.
2354
+
2355
+ Returns the same enriched row shape as ``list_sessions_rich`` (adds
2356
+ ``preview`` + ``last_active``) so callers can reuse it.
2357
+ """
2358
+ prefix = f"cron_{job_id}_"
2359
+ # Half-open upper bound for an index range scan: increment the final
2360
+ # byte of the prefix so the range covers exactly the ids that start
2361
+ # with ``prefix`` and nothing else. ``prefix`` always ends in '_', but
2362
+ # compute it generically rather than hardcoding the successor char.
2363
+ prefix_hi = prefix[:-1] + chr(ord(prefix[-1]) + 1)
2364
+
2365
+ query = """
2366
+ SELECT s.*,
2367
+ COALESCE(
2368
+ (SELECT SUBSTR(REPLACE(REPLACE(m.content, X'0A', ' '), X'0D', ' '), 1, 63)
2369
+ FROM messages m
2370
+ WHERE m.session_id = s.id AND m.role = 'user' AND m.content IS NOT NULL
2371
+ ORDER BY m.timestamp, m.id LIMIT 1),
2372
+ ''
2373
+ ) AS _preview_raw,
2374
+ COALESCE(
2375
+ (SELECT MAX(m2.timestamp) FROM messages m2 WHERE m2.session_id = s.id),
2376
+ s.started_at
2377
+ ) AS last_active
2378
+ FROM sessions s
2379
+ WHERE s.source = 'cron' AND s.id >= ? AND s.id < ?
2380
+ ORDER BY s.started_at DESC, s.id DESC
2381
+ LIMIT ? OFFSET ?
2382
+ """
2383
+ with self._lock:
2384
+ cursor = self._conn.execute(query, (prefix, prefix_hi, limit, offset))
2385
+ rows = cursor.fetchall()
2386
+
2387
+ runs: List[Dict[str, Any]] = []
2388
+ for row in rows:
2389
+ s = dict(row)
2390
+ raw = s.pop("_preview_raw", "").strip()
2391
+ if raw:
2392
+ text = raw[:60]
2393
+ s["preview"] = text + ("..." if len(raw) > 60 else "")
2394
+ else:
2395
+ s["preview"] = ""
2396
+ runs.append(s)
2397
+ return runs
2398
+
1759
2399
  def _get_session_rich_row(self, session_id: str) -> Optional[Dict[str, Any]]:
1760
2400
  """Fetch a single session with the same enriched columns as
1761
2401
  ``list_sessions_rich`` (preview + last_active). Returns None if the
@@ -1854,6 +2494,7 @@ class SessionDB:
1854
2494
  codex_message_items: Any = None,
1855
2495
  platform_message_id: str = None,
1856
2496
  observed: bool = False,
2497
+ timestamp: Any = None,
1857
2498
  ) -> int:
1858
2499
  """
1859
2500
  Append a message to a session. Returns the message row ID.
@@ -1885,6 +2526,16 @@ class SessionDB:
1885
2526
  # cannot bind list/dict parameters directly.
1886
2527
  stored_content = self._encode_content(content)
1887
2528
 
2529
+ message_timestamp = time.time()
2530
+ if timestamp is not None:
2531
+ try:
2532
+ if hasattr(timestamp, "timestamp"):
2533
+ message_timestamp = float(timestamp.timestamp())
2534
+ else:
2535
+ message_timestamp = float(timestamp)
2536
+ except (TypeError, ValueError):
2537
+ logger.debug("Ignoring invalid explicit message timestamp: %r", timestamp)
2538
+
1888
2539
  # Pre-compute tool call count
1889
2540
  num_tool_calls = 0
1890
2541
  if tool_calls is not None:
@@ -1904,7 +2555,7 @@ class SessionDB:
1904
2555
  tool_call_id,
1905
2556
  tool_calls_json,
1906
2557
  tool_name,
1907
- time.time(),
2558
+ message_timestamp,
1908
2559
  token_count,
1909
2560
  finish_reason,
1910
2561
  reasoning,
@@ -1957,6 +2608,16 @@ class SessionDB:
1957
2608
  for msg in messages:
1958
2609
  role = msg.get("role", "unknown")
1959
2610
  tool_calls = msg.get("tool_calls")
2611
+ message_timestamp = now_ts
2612
+ if msg.get("timestamp") is not None:
2613
+ try:
2614
+ ts_value = msg.get("timestamp")
2615
+ if hasattr(ts_value, "timestamp"):
2616
+ message_timestamp = float(ts_value.timestamp())
2617
+ else:
2618
+ message_timestamp = float(ts_value)
2619
+ except (TypeError, ValueError):
2620
+ logger.debug("Ignoring invalid explicit message timestamp: %r", msg.get("timestamp"))
1960
2621
  reasoning_details = msg.get("reasoning_details") if role == "assistant" else None
1961
2622
  codex_reasoning_items = (
1962
2623
  msg.get("codex_reasoning_items") if role == "assistant" else None
@@ -1994,7 +2655,7 @@ class SessionDB:
1994
2655
  msg.get("tool_call_id"),
1995
2656
  tool_calls_json,
1996
2657
  msg.get("tool_name"),
1997
- now_ts,
2658
+ message_timestamp,
1998
2659
  msg.get("token_count"),
1999
2660
  msg.get("finish_reason"),
2000
2661
  msg.get("reasoning") if role == "assistant" else None,
@@ -2011,7 +2672,7 @@ class SessionDB:
2011
2672
  total_tool_calls += (
2012
2673
  len(tool_calls) if isinstance(tool_calls, list) else 1
2013
2674
  )
2014
- now_ts += 1e-6
2675
+ now_ts = max(now_ts + 1e-6, message_timestamp + 1e-6)
2015
2676
 
2016
2677
  conn.execute(
2017
2678
  "UPDATE sessions SET message_count = ?, tool_call_count = ? WHERE id = ?",
@@ -2274,6 +2935,24 @@ class SessionDB:
2274
2935
  if not session_id:
2275
2936
  return session_id
2276
2937
 
2938
+ # Follow the compression-continuation chain forward to the live tip
2939
+ # FIRST. Auto-compression ends the current session and forks a
2940
+ # continuation child, but a long-lived parent keeps its own flushed
2941
+ # message rows — so the empty-head walk below never redirects it, and
2942
+ # resuming the parent id reloads the pre-compression transcript while
2943
+ # the turns generated *after* compression (and their responses) sit in
2944
+ # the continuation. ``get_compression_tip`` is lineage-aware: it only
2945
+ # follows children whose parent ended with ``end_reason='compression'``
2946
+ # (created after the parent was ended), so delegation / branch children
2947
+ # never hijack the resume. This is the fix for the desktop "I came back
2948
+ # and the reply isn't there" report on large sessions.
2949
+ try:
2950
+ tip = self.get_compression_tip(session_id)
2951
+ except Exception:
2952
+ tip = session_id
2953
+ if tip and tip != session_id:
2954
+ session_id = tip
2955
+
2277
2956
  with self._lock:
2278
2957
  # If this session already has messages, nothing to redirect.
2279
2958
  try:
@@ -2342,9 +3021,9 @@ class SessionDB:
2342
3021
  rows = self._conn.execute(
2343
3022
  "SELECT role, content, tool_call_id, tool_calls, tool_name, "
2344
3023
  "finish_reason, reasoning, reasoning_content, reasoning_details, "
2345
- "codex_reasoning_items, codex_message_items, platform_message_id, observed "
3024
+ "codex_reasoning_items, codex_message_items, platform_message_id, observed, timestamp "
2346
3025
  f"FROM messages WHERE session_id IN ({placeholders})"
2347
- f"{active_clause} ORDER BY id",
3026
+ f"{active_clause} ORDER BY timestamp, id",
2348
3027
  tuple(session_ids),
2349
3028
  ).fetchall()
2350
3029
 
@@ -2354,6 +3033,8 @@ class SessionDB:
2354
3033
  if row["role"] in {"user", "assistant"} and isinstance(content, str):
2355
3034
  content = sanitize_context(content).strip()
2356
3035
  msg = {"role": row["role"], "content": content}
3036
+ if row["timestamp"]:
3037
+ msg["timestamp"] = row["timestamp"]
2357
3038
  if row["tool_call_id"]:
2358
3039
  msg["tool_call_id"] = row["tool_call_id"]
2359
3040
  if row["tool_name"]:
@@ -2620,9 +3301,10 @@ class SessionDB:
2620
3301
  """Sanitize user input for safe use in FTS5 MATCH queries.
2621
3302
 
2622
3303
  FTS5 has its own query syntax where characters like ``"``, ``(``, ``)``,
2623
- ``+``, ``*``, ``{``, ``}`` and bare boolean operators (``AND``, ``OR``,
2624
- ``NOT``) have special meaning. Passing raw user input directly to
2625
- MATCH can cause ``sqlite3.OperationalError``.
3304
+ ``+``, ``*``, ``{``, ``}``, the column-filter operator ``:`` and bare
3305
+ boolean operators (``AND``, ``OR``, ``NOT``) have special meaning.
3306
+ Passing raw user input directly to MATCH can cause
3307
+ ``sqlite3.OperationalError``.
2626
3308
 
2627
3309
  Strategy:
2628
3310
  - Preserve properly paired quoted phrases (``"exact phrase"``)
@@ -2641,8 +3323,12 @@ class SessionDB:
2641
3323
 
2642
3324
  sanitized = re.sub(r'"[^"]*"', _preserve_quoted, query)
2643
3325
 
2644
- # Step 2: Strip remaining (unmatched) FTS5-special characters
2645
- sanitized = re.sub(r'[+{}()\"^]', " ", sanitized)
3326
+ # Step 2: Strip remaining (unmatched) FTS5-special characters. ``:`` is
3327
+ # FTS5's column-filter operator (``col:term``); since the FTS table has a
3328
+ # single ``content`` column, an unquoted colon query like ``TODO: fix``
3329
+ # parses as ``column:term`` and raises "no such column" — swallowed at
3330
+ # the execute site into zero results. Strip it like the others.
3331
+ sanitized = re.sub(r'[+{}():\"^]', " ", sanitized)
2646
3332
 
2647
3333
  # Step 3: Collapse repeated * (e.g. "***") into a single one,
2648
3334
  # and remove leading * (prefix-only needs at least one char before *)
@@ -2833,7 +3519,8 @@ class SessionDB:
2833
3519
  self._count_cjk(t) < 3 for t in _tokens_for_check
2834
3520
  )
2835
3521
 
2836
- if cjk_count >= 3 and not _any_short_cjk:
3522
+ _trigram_succeeded = False
3523
+ if cjk_count >= 3 and not _any_short_cjk and self._trigram_available:
2837
3524
  # Trigram FTS5 path — quote each non-operator token to handle
2838
3525
  # FTS5 special chars (%, *, etc.) while preserving boolean
2839
3526
  # operators (AND, OR, NOT) for multi-term queries.
@@ -2882,11 +3569,13 @@ class SessionDB:
2882
3569
  try:
2883
3570
  tri_cursor = self._conn.execute(tri_sql, tri_params)
2884
3571
  except sqlite3.OperationalError:
2885
- matches = []
3572
+ # Trigram query failed at runtime — fall through to LIKE.
3573
+ pass
2886
3574
  else:
2887
3575
  matches = [dict(row) for row in tri_cursor.fetchall()]
2888
- else:
2889
- # Short / mixed CJK query: trigram cannot match tokens with
3576
+ _trigram_succeeded = True
3577
+ if not _trigram_succeeded:
3578
+ # Short / mixed CJK query, trigram unavailable, or trigram
2890
3579
  # <3 CJK chars. Fall back to LIKE substring search.
2891
3580
  # For multi-token OR queries (e.g. "广西 OR 桂林 OR 漓江"),
2892
3581
  # build one LIKE condition per non-operator token so each term
@@ -3010,6 +3699,53 @@ class SessionDB:
3010
3699
 
3011
3700
  return matches
3012
3701
 
3702
+ def search_sessions_by_id(
3703
+ self,
3704
+ query: str,
3705
+ limit: int = 20,
3706
+ include_archived: bool = True,
3707
+ ) -> List[Dict[str, Any]]:
3708
+ """Search surfaced sessions by exact/prefix/substring session id.
3709
+
3710
+ Desktop search uses this alongside FTS message search so users can paste
3711
+ a session id from logs, CLI output, or another Hermes surface and jump
3712
+ straight to that conversation. Matching also checks ``_lineage_root_id``
3713
+ for projected compression-chain tips, so an old root id still resolves to
3714
+ the live continuation row.
3715
+ """
3716
+ needle = (query or "").strip().lower()
3717
+ if not needle or limit <= 0:
3718
+ return []
3719
+
3720
+ # SQL-bounded: list_sessions_rich pushes the id LIKE filter into the
3721
+ # query (matching the row's own id AND any id in its forward
3722
+ # compression chain), so we only materialize matching rows instead of
3723
+ # scanning every session. Fetch a small multiple of `limit` so the
3724
+ # in-Python exact/prefix/substring ranking below has enough candidates
3725
+ # to order, then truncate.
3726
+ candidates = self.list_sessions_rich(
3727
+ limit=max(limit * 4, limit),
3728
+ offset=0,
3729
+ include_archived=include_archived,
3730
+ order_by_last_active=True,
3731
+ id_query=needle,
3732
+ )
3733
+
3734
+ def score(row: Dict[str, Any]) -> int:
3735
+ ids = [str(row.get("id") or ""), str(row.get("_lineage_root_id") or "")]
3736
+ normalized = [value.lower() for value in ids if value]
3737
+ if any(value == needle for value in normalized):
3738
+ return 0
3739
+ if any(value.startswith(needle) for value in normalized):
3740
+ return 1
3741
+ return 2
3742
+
3743
+ ranked = sorted(
3744
+ enumerate(candidates),
3745
+ key=lambda item: (score(item[1]), item[0]),
3746
+ )
3747
+ return [row for _, row in ranked[:limit]]
3748
+
3013
3749
  def search_sessions(
3014
3750
  self,
3015
3751
  source: str = None,
@@ -3056,26 +3792,51 @@ class SessionDB:
3056
3792
  min_message_count: int = 0,
3057
3793
  include_archived: bool = False,
3058
3794
  archived_only: bool = False,
3795
+ exclude_children: bool = False,
3796
+ exclude_sources: List[str] = None,
3059
3797
  ) -> int:
3060
- """Count sessions, optionally filtered by source."""
3798
+ """Count sessions, optionally filtered by source.
3799
+
3800
+ Pass ``exclude_children=True`` to count only the conversations that
3801
+ ``list_sessions_rich`` surfaces (root + branch sessions), hiding
3802
+ sub-agent runs and compression continuations. Use it whenever the count
3803
+ is paired with a ``list_sessions_rich`` page (e.g. sidebar "load more"
3804
+ totals) so the total matches the number of listable rows — otherwise the
3805
+ raw row count is inflated by children and "load more" never settles.
3806
+
3807
+ Pass ``exclude_sources`` to drop whole source classes from the count
3808
+ (e.g. ``["cron"]`` so the recents "load more" total matches a
3809
+ cron-excluded ``list_sessions_rich`` page and doesn't keep "load more"
3810
+ stuck on for buried scheduler sessions).
3811
+ """
3061
3812
  where_clauses = []
3062
3813
  params = []
3063
3814
 
3815
+ if exclude_children:
3816
+ # Mirror list_sessions_rich's child-exclusion clause exactly so the
3817
+ # count lines up with the rows: roots (no parent) plus branch
3818
+ # children (parent ended with end_reason='branched').
3819
+ where_clauses.append(_LISTABLE_CHILD_SQL)
3820
+ where_clauses.append(f"{_delegate_from_json('s.model_config')} IS NULL")
3064
3821
  if source:
3065
- where_clauses.append("source = ?")
3822
+ where_clauses.append("s.source = ?")
3066
3823
  params.append(source)
3824
+ if exclude_sources:
3825
+ placeholders = ",".join("?" for _ in exclude_sources)
3826
+ where_clauses.append(f"s.source NOT IN ({placeholders})")
3827
+ params.extend(exclude_sources)
3067
3828
  if min_message_count > 0:
3068
- where_clauses.append("message_count >= ?")
3829
+ where_clauses.append("s.message_count >= ?")
3069
3830
  params.append(min_message_count)
3070
3831
  if archived_only:
3071
- where_clauses.append("archived = 1")
3832
+ where_clauses.append("s.archived = 1")
3072
3833
  elif not include_archived:
3073
- where_clauses.append("archived = 0")
3834
+ where_clauses.append("s.archived = 0")
3074
3835
 
3075
3836
  where_sql = f" WHERE {' AND '.join(where_clauses)}" if where_clauses else ""
3076
3837
 
3077
3838
  with self._lock:
3078
- cursor = self._conn.execute(f"SELECT COUNT(*) FROM sessions{where_sql}", params)
3839
+ cursor = self._conn.execute(f"SELECT COUNT(*) FROM sessions s{where_sql}", params)
3079
3840
  return cursor.fetchone()[0]
3080
3841
 
3081
3842
  def message_count(self, session_id: str = None) -> int:
@@ -3159,19 +3920,24 @@ class SessionDB:
3159
3920
  ) -> bool:
3160
3921
  """Delete a session and all its messages.
3161
3922
 
3162
- Child sessions are orphaned (parent_session_id set to NULL) rather
3163
- than cascade-deleted, so they remain accessible independently.
3923
+ Delegate subagent children (``model_config._delegate_from``) are
3924
+ cascade-deleted with the parent so they never resurface in session
3925
+ pickers as orphaned rows. Branch / compression children are orphaned
3926
+ (``parent_session_id → NULL``) so they remain accessible independently.
3164
3927
  When *sessions_dir* is provided, also removes on-disk transcript
3165
- files (``.json`` / ``.jsonl`` / ``request_dump_*``) for the deleted
3928
+ files (``.json`` / ``.jsonl`` / ``request_dump_*``) for every deleted
3166
3929
  session. Returns True if the session was found and deleted.
3167
3930
  """
3931
+ removed_delegate_ids: List[str] = []
3932
+
3168
3933
  def _do(conn):
3169
3934
  cursor = conn.execute(
3170
3935
  "SELECT COUNT(*) FROM sessions WHERE id = ?", (session_id,)
3171
3936
  )
3172
3937
  if cursor.fetchone()[0] == 0:
3173
3938
  return False
3174
- # Orphan child sessions so FK constraint is satisfied
3939
+ removed_delegate_ids.extend(_delete_delegate_children(conn, [session_id]))
3940
+ # Orphan remaining child sessions (branches, etc.) so FK is satisfied.
3175
3941
  conn.execute(
3176
3942
  "UPDATE sessions SET parent_session_id = NULL "
3177
3943
  "WHERE parent_session_id = ?",
@@ -3181,10 +3947,54 @@ class SessionDB:
3181
3947
  conn.execute("DELETE FROM sessions WHERE id = ?", (session_id,))
3182
3948
  return True
3183
3949
 
3950
+ deleted = self._execute_write(_do)
3951
+ if deleted:
3952
+ for delegate_id in removed_delegate_ids:
3953
+ self._remove_session_files(sessions_dir, delegate_id)
3954
+ self._remove_session_files(sessions_dir, session_id)
3955
+ return bool(deleted)
3956
+
3957
+ def delete_session_if_empty(
3958
+ self,
3959
+ session_id: str,
3960
+ sessions_dir: Optional[Path] = None,
3961
+ ) -> bool:
3962
+ """Delete *session_id* only when it never gained resumable content.
3963
+
3964
+ A session is considered empty when it has no messages and no
3965
+ user-assigned title. Used by CLI exit / session-rotation paths so
3966
+ immediately-started-and-quit sessions don't pile up in ``/resume``
3967
+ and ``hermes sessions list`` output. (Pattern ported from
3968
+ google-gemini/gemini-cli#27770.)
3969
+
3970
+ The emptiness check and delete run in one transaction, so a message
3971
+ flushed concurrently by another writer can't be lost. Sessions with
3972
+ children (delegate subagent runs) are preserved — a parent that
3973
+ spawned work is not "empty" even if its own transcript never
3974
+ flushed. Returns True if the session was deleted.
3975
+ """
3976
+ def _do(conn):
3977
+ cursor = conn.execute(
3978
+ """
3979
+ DELETE FROM sessions
3980
+ WHERE id = ?
3981
+ AND title IS NULL
3982
+ AND NOT EXISTS (
3983
+ SELECT 1 FROM messages WHERE messages.session_id = sessions.id
3984
+ )
3985
+ AND NOT EXISTS (
3986
+ SELECT 1 FROM sessions child
3987
+ WHERE child.parent_session_id = sessions.id
3988
+ )
3989
+ """,
3990
+ (session_id,),
3991
+ )
3992
+ return cursor.rowcount > 0
3993
+
3184
3994
  deleted = self._execute_write(_do)
3185
3995
  if deleted:
3186
3996
  self._remove_session_files(sessions_dir, session_id)
3187
- return deleted
3997
+ return bool(deleted)
3188
3998
 
3189
3999
  def delete_sessions(
3190
4000
  self,
@@ -3200,10 +4010,9 @@ class SessionDB:
3200
4010
  * Unknown IDs are silently skipped (no 404) — selection state
3201
4011
  in the UI can race against another tab's delete, and we'd
3202
4012
  rather succeed-on-the-rest than fail-the-whole-batch.
3203
- * Children of every deleted ID are orphaned
3204
- (``parent_session_id NULL``), never cascade-deleted, so a
3205
- branch / subagent transcript survives an inadvertent parent
3206
- delete.
4013
+ * Delegate subagent children (``model_config._delegate_from``) are
4014
+ cascade-deleted with their parent; branch children are orphaned
4015
+ (``parent_session_id NULL``) so they stay accessible.
3207
4016
  * Messages and the session row both go in one
3208
4017
  ``_execute_write`` call so a partial failure can't leave the
3209
4018
  DB in a "messages gone but session row still there" state.
@@ -3226,6 +4035,7 @@ class SessionDB:
3226
4035
  return 0
3227
4036
 
3228
4037
  removed_ids: list[str] = []
4038
+ removed_delegate_ids: list[str] = []
3229
4039
 
3230
4040
  def _do(conn):
3231
4041
  placeholders = ",".join("?" * len(unique_ids))
@@ -3240,7 +4050,8 @@ class SessionDB:
3240
4050
  return 0
3241
4051
 
3242
4052
  existing_placeholders = ",".join("?" * len(existing))
3243
- # Orphan children whose parent is in the kill list so the
4053
+ removed_delegate_ids.extend(_delete_delegate_children(conn, existing))
4054
+ # Orphan remaining children whose parent is in the kill list so the
3244
4055
  # FK constraint stays satisfied. Pin children whose parent
3245
4056
  # is itself in the kill list rather than NULL-ing parents
3246
4057
  # of survivors — the IN list on ``parent_session_id`` does
@@ -3262,6 +4073,8 @@ class SessionDB:
3262
4073
  return len(existing)
3263
4074
 
3264
4075
  count = self._execute_write(_do)
4076
+ for sid in removed_delegate_ids:
4077
+ self._remove_session_files(sessions_dir, sid)
3265
4078
  for sid in removed_ids:
3266
4079
  self._remove_session_files(sessions_dir, sid)
3267
4080
  return count