@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
@@ -31,6 +31,8 @@ from agent.codex_responses_adapter import _summarize_user_message_for_log
31
31
  from agent.display import KawaiiSpinner
32
32
  from agent.error_classifier import FailoverReason, classify_api_error
33
33
  from agent.iteration_budget import IterationBudget
34
+ from agent.turn_context import build_turn_context
35
+ from agent.turn_retry_state import TurnRetryState
34
36
  from agent.memory_manager import build_memory_context_block
35
37
  from agent.message_sanitization import (
36
38
  _repair_tool_call_arguments,
@@ -63,6 +65,40 @@ from utils import base_url_host_matches, env_var_enabled
63
65
 
64
66
  logger = logging.getLogger(__name__)
65
67
 
68
+ # Stable prefix of the local interrupt status string emitted when a turn is
69
+ # cancelled while waiting on the provider. Surfaces (ACP, TUI) match on this
70
+ # to treat it as cancellation metadata rather than assistant prose.
71
+ INTERRUPT_WAITING_FOR_MODEL_PREFIX = "Operation interrupted: waiting for model response ("
72
+
73
+
74
+ def _image_error_max_dimension(error: Exception) -> Optional[int]:
75
+ """Extract a provider-reported image dimension ceiling, if present."""
76
+ parts = []
77
+ for value in (
78
+ error,
79
+ getattr(error, "message", None),
80
+ getattr(error, "body", None),
81
+ ):
82
+ if value:
83
+ try:
84
+ parts.append(str(value))
85
+ except Exception:
86
+ pass
87
+ text = " ".join(parts).lower()
88
+ if "image" not in text or "dimension" not in text or "max allowed size" not in text:
89
+ return None
90
+
91
+ match = re.search(r"max allowed size(?:\s+for [^:]+)?:\s*(\d{3,5})\s*pixels?", text)
92
+ if not match:
93
+ return None
94
+ try:
95
+ max_dimension = int(match.group(1))
96
+ except ValueError:
97
+ return None
98
+ if 512 <= max_dimension <= 8000:
99
+ return max_dimension
100
+ return None
101
+
66
102
 
67
103
  def _ollama_context_limit_error(agent: Any, request_tokens: int) -> Optional[str]:
68
104
  """Return a user-facing error when Ollama is loaded with too little context."""
@@ -264,11 +300,20 @@ def _restore_or_build_system_prompt(agent, system_message, conversation_history)
264
300
  agent.session_id, exc,
265
301
  )
266
302
 
267
- if stored_prompt:
303
+ if stored_prompt and _stored_prompt_matches_runtime(agent, stored_prompt):
268
304
  # Continuing session — reuse the exact system prompt from the
269
305
  # previous turn so the Anthropic cache prefix matches.
270
306
  agent._cached_system_prompt = stored_prompt
271
307
  return
308
+ if stored_prompt:
309
+ stored_state = "stale_runtime"
310
+ logger.info(
311
+ "Stored system prompt for session %s has stale runtime identity; "
312
+ "rebuilding for model=%s provider=%s.",
313
+ agent.session_id,
314
+ getattr(agent, "model", "") or "",
315
+ getattr(agent, "provider", "") or "",
316
+ )
272
317
 
273
318
  if conversation_history and stored_state in ("null", "empty"):
274
319
  # Continuing session whose stored prompt is unusable. The
@@ -301,6 +346,19 @@ def _restore_or_build_system_prompt(agent, system_message, conversation_history)
301
346
  except Exception as exc:
302
347
  logger.warning("on_session_start hook failed: %s", exc)
303
348
 
349
+ # Cold-start credits seed (L3) — fallback for the first-turn path. The TUI/
350
+ # desktop build seeds at session OPEN (see seed_credits_at_session_start in
351
+ # tui_gateway), so this call is usually a no-op there (idempotent: skips when
352
+ # _credits_state already exists). For the plain CLI / any path that didn't seed
353
+ # at build, it primes credits state from /api/oauth/account (or a fixture) on the
354
+ # first turn so depletion / usage-band warnings fire. Fail-open inside the helper.
355
+ try:
356
+ from agent.credits_tracker import seed_credits_at_session_start
357
+
358
+ seed_credits_at_session_start(agent)
359
+ except Exception:
360
+ logger.debug("cold-start credits seed failed (fail-open)", exc_info=True)
361
+
304
362
  # Persist the system prompt snapshot in SQLite. Failure here used
305
363
  # to log at DEBUG, which silently broke prefix-cache reuse on the
306
364
  # gateway path (fresh AIAgent per turn → reads from this row every
@@ -317,6 +375,30 @@ def _restore_or_build_system_prompt(agent, system_message, conversation_history)
317
375
  )
318
376
 
319
377
 
378
+ def _stored_prompt_matches_runtime(agent, prompt: str) -> bool:
379
+ """Return False when the persisted Model/Provider lines are stale."""
380
+
381
+ def line_value(label: str) -> str:
382
+ prefix = f"{label}:"
383
+ value = ""
384
+ for line in prompt.splitlines():
385
+ if line.startswith(prefix):
386
+ value = line[len(prefix):].strip()
387
+ return value
388
+
389
+ stored_model = line_value("Model")
390
+ current_model = str(getattr(agent, "model", "") or "").strip()
391
+ if stored_model and current_model and stored_model != current_model:
392
+ return False
393
+
394
+ stored_provider = line_value("Provider")
395
+ current_provider = str(getattr(agent, "provider", "") or "").strip()
396
+ if stored_provider and current_provider and stored_provider != current_provider:
397
+ return False
398
+
399
+ return True
400
+
401
+
320
402
  def _get_continuation_prompt(is_partial_stub: bool, dropped_tools: Optional[List[str]] = None) -> str:
321
403
  if is_partial_stub and dropped_tools:
322
404
  tool_list = ", ".join(dropped_tools[:3])
@@ -348,6 +430,42 @@ def _get_continuation_prompt(is_partial_stub: bool, dropped_tools: Optional[List
348
430
  )
349
431
 
350
432
 
433
+ # Shared recovery hint appended to every content-policy refusal message. Both
434
+ # the HTTP-200 refusal path (``finish_reason=content_filter``) and the
435
+ # exception path (a provider moderation error classified as
436
+ # ``content_policy_blocked``) end with the same actionable next steps, so they
437
+ # share one trailer to keep the guidance from drifting between the two sites.
438
+ _CONTENT_POLICY_RECOVERY_HINT = (
439
+ "Try rephrasing the request, narrowing the context, or "
440
+ "adding a fallback provider with `hermes fallback add`."
441
+ )
442
+
443
+
444
+ def _content_policy_blocked_result(
445
+ messages: List[Dict],
446
+ api_call_count: int,
447
+ *,
448
+ final_response: str,
449
+ error_detail: str,
450
+ ) -> Dict[str, Any]:
451
+ """Build the terminal turn result for a content-policy block.
452
+
453
+ A content-policy refusal is deterministic for the unchanged prompt, so the
454
+ turn ends here (no retry). Both the HTTP-200 refusal handler and the
455
+ exception-path handler return the identical shape — a failed, non-completed
456
+ turn carrying the user-facing message and a ``content_policy_blocked:``
457
+ prefixed error — so they funnel through this one builder.
458
+ """
459
+ return {
460
+ "final_response": final_response,
461
+ "messages": messages,
462
+ "api_calls": api_call_count,
463
+ "completed": False,
464
+ "failed": True,
465
+ "error": f"content_policy_blocked: {error_detail}",
466
+ }
467
+
468
+
351
469
  def run_conversation(
352
470
  agent,
353
471
  user_message: str,
@@ -356,6 +474,7 @@ def run_conversation(
356
474
  task_id: str = None,
357
475
  stream_callback: Optional[callable] = None,
358
476
  persist_user_message: Optional[str] = None,
477
+ persist_user_timestamp: Optional[float] = None,
359
478
  ) -> Dict[str, Any]:
360
479
  """
361
480
  Run a complete conversation with tool calling until completion.
@@ -371,356 +490,51 @@ def run_conversation(
371
490
  persist_user_message: Optional clean user message to store in
372
491
  transcripts/history when user_message contains API-only
373
492
  synthetic prefixes.
493
+ persist_user_timestamp: Optional platform event timestamp to store
494
+ as metadata on that persisted user message.
374
495
  or queuing follow-up prefetch work.
375
496
 
376
497
  Returns:
377
498
  Dict: Complete conversation result with final response and message history
378
499
  """
379
- # Guard stdio against OSError from broken pipes (systemd/headless/daemon).
380
- # Installed once, transparent when streams are healthy, prevents crash on write.
381
- _install_safe_stdio()
382
-
383
- agent._ensure_db_session()
384
-
385
- # Tell auxiliary_client what the live main provider/model are for
386
- # this turn. Used by tools whose behaviour depends on the active
387
- # main model (e.g. vision_analyze's native fast path) so they see
388
- # the CLI/gateway override instead of the stale config.yaml
389
- # default. Idempotent — fine to call every turn.
390
- try:
391
- from agent.auxiliary_client import set_runtime_main
392
- set_runtime_main(
393
- getattr(agent, "provider", "") or "",
394
- getattr(agent, "model", "") or "",
395
- base_url=getattr(agent, "base_url", "") or "",
396
- api_key=getattr(agent, "api_key", "") or "",
397
- api_mode=getattr(agent, "api_mode", "") or "",
398
- )
399
- except Exception:
400
- pass
401
-
402
- # Tag all log records on this thread with the session ID so
403
- # ``hermes logs --session <id>`` can filter a single conversation.
404
- set_session_context(agent.session_id)
405
-
406
- # Bind the skill write-origin ContextVar for this thread so tool
407
- # handlers (e.g. skill_manage create) can tell whether they are
408
- # running inside the background agent-improvement review fork vs.
409
- # a foreground user-directed turn. Set at the top of each call;
410
- # the review fork runs on its own thread with a fresh context,
411
- # so the foreground value here does not leak into it.
412
- set_current_write_origin(getattr(agent, "_memory_write_origin", "assistant_tool"))
413
-
414
- # If the previous turn activated fallback, restore the primary
415
- # runtime so this turn gets a fresh attempt with the preferred model.
416
- # No-op when _fallback_activated is False (gateway, first turn, etc.).
417
- agent._restore_primary_runtime()
418
-
419
- # Sanitize surrogate characters from user input. Clipboard paste from
420
- # rich-text editors (Google Docs, Word, etc.) can inject lone surrogates
421
- # that are invalid UTF-8 and crash JSON serialization in the OpenAI SDK.
422
- if isinstance(user_message, str):
423
- user_message = _sanitize_surrogates(user_message)
424
- if isinstance(persist_user_message, str):
425
- persist_user_message = _sanitize_surrogates(persist_user_message)
426
-
427
- # Store stream callback for _interruptible_api_call to pick up
428
- agent._stream_callback = stream_callback
429
- agent._persist_user_message_idx = None
430
- agent._persist_user_message_override = persist_user_message
431
- # Generate unique task_id if not provided to isolate VMs between concurrent tasks
432
- effective_task_id = task_id or str(uuid.uuid4())
433
- # Expose the active task_id so tools running mid-turn (e.g. delegate_task
434
- # in delegate_tool.py) can identify this agent for the cross-agent file
435
- # state registry. Set BEFORE any tool dispatch so snapshots taken at
436
- # child-launch time see the parent's real id, not None.
437
- agent._current_task_id = effective_task_id
438
-
439
- # Reset retry counters and iteration budget at the start of each turn
440
- # so subagent usage from a previous turn doesn't eat into the next one.
441
- agent._invalid_tool_retries = 0
442
- agent._invalid_json_retries = 0
443
- agent._empty_content_retries = 0
444
- agent._incomplete_scratchpad_retries = 0
445
- agent._codex_incomplete_retries = 0
446
- agent._thinking_prefill_retries = 0
447
- agent._post_tool_empty_retried = False
448
- agent._last_content_with_tools = None
449
- agent._last_content_tools_all_housekeeping = False
450
- agent._mute_post_response = False
451
- agent._unicode_sanitization_passes = 0
452
- agent._tool_guardrails.reset_for_turn()
453
- agent._tool_guardrail_halt_decision = None
454
- # True until the server rejects an image_url content part with an error
455
- # like "Only 'text' content type is supported." Set to False on first
456
- # rejection and kept False for the rest of the session so we never re-send
457
- # images to a text-only endpoint. Scoped per `_run()` call, not per instance.
458
- agent._vision_supported = True
459
-
460
- # Pre-turn connection health check: detect and clean up dead TCP
461
- # connections left over from provider outages or dropped streams.
462
- # This prevents the next API call from hanging on a zombie socket.
463
- if agent.api_mode != "anthropic_messages":
464
- try:
465
- if agent._cleanup_dead_connections():
466
- agent._emit_status(
467
- "🔌 Detected stale connections from a previous provider "
468
- "issue — cleaned up automatically. Proceeding with fresh "
469
- "connection."
470
- )
471
- except Exception:
472
- pass
473
- # Replay compression warning through status_callback for gateway
474
- # platforms (the callback was not wired during __init__).
475
- if agent._compression_warning:
476
- agent._replay_compression_warning()
477
- agent._compression_warning = None # send once
478
-
479
- # NOTE: _turns_since_memory and _iters_since_skill are NOT reset here.
480
- # They are initialized in __init__ and must persist across run_conversation
481
- # calls so that nudge logic accumulates correctly in CLI mode.
482
- agent.iteration_budget = IterationBudget(agent.max_iterations)
483
-
484
- # Log conversation turn start for debugging/observability
485
- _preview_text = _summarize_user_message_for_log(user_message)
486
- _msg_preview = (_preview_text[:80] + "...") if len(_preview_text) > 80 else _preview_text
487
- _msg_preview = _msg_preview.replace("\n", " ")
488
- logger.info(
489
- "conversation turn: session=%s model=%s provider=%s platform=%s history=%d msg=%r",
490
- agent.session_id or "none", agent.model, agent.provider or "unknown",
491
- agent.platform or "unknown", len(conversation_history or []),
492
- _msg_preview,
500
+ # ── Per-turn setup (the prologue) ──
501
+ # All once-per-turn setup stdio guarding, retry-counter resets, user
502
+ # message sanitization, todo/nudge hydration, system-prompt restore-or-
503
+ # build, crash-resilience persistence, preflight compression, the
504
+ # ``pre_llm_call`` plugin hook, and external-memory prefetch — lives in
505
+ # ``build_turn_context``. It mutates ``agent`` exactly as the inline code
506
+ # did and returns the locals the loop below reads back. See
507
+ # ``agent/turn_context.py``.
508
+ _ctx = build_turn_context(
509
+ agent,
510
+ user_message,
511
+ system_message,
512
+ conversation_history,
513
+ task_id,
514
+ stream_callback,
515
+ persist_user_message,
516
+ persist_user_timestamp,
517
+ restore_or_build_system_prompt=_restore_or_build_system_prompt,
518
+ install_safe_stdio=_install_safe_stdio,
519
+ sanitize_surrogates=_sanitize_surrogates,
520
+ summarize_user_message_for_log=_summarize_user_message_for_log,
521
+ set_session_context=set_session_context,
522
+ set_current_write_origin=set_current_write_origin,
523
+ ra=_ra,
493
524
  )
494
-
495
- # Initialize conversation (copy to avoid mutating the caller's list)
496
- messages = list(conversation_history) if conversation_history else []
497
-
498
- # Hydrate todo store from conversation history (gateway creates a fresh
499
- # AIAgent per message, so the in-memory store is empty -- we need to
500
- # recover the todo state from the most recent todo tool response in history)
501
- if conversation_history and not agent._todo_store.has_items():
502
- agent._hydrate_todo_store(conversation_history)
503
-
504
- # Hydrate per-session nudge counters from persisted history.
505
- # Gateway creates a fresh AIAgent per inbound message (cache miss /
506
- # 1h idle eviction / config-signature mismatch / process restart), so
507
- # _turns_since_memory and _user_turn_count start at 0 every turn and
508
- # the memory.nudge_interval trigger may never be reached. Reconstruct
509
- # an effective count from prior user turns in conversation_history.
510
- # Idempotent: a cached agent that already accumulated counters keeps
511
- # them; only a freshly-built agent with empty in-memory state hydrates.
512
- # See issue #22357.
513
- if conversation_history and agent._user_turn_count == 0:
514
- prior_user_turns = sum(
515
- 1 for m in conversation_history if m.get("role") == "user"
516
- )
517
- if prior_user_turns > 0:
518
- agent._user_turn_count = prior_user_turns
519
- if agent._memory_nudge_interval > 0 and agent._turns_since_memory == 0:
520
- # % preserves original 1-in-N cadence rather than firing a
521
- # review immediately on resume (which would surprise users
522
- # whose session happened to land just past a multiple of N).
523
- agent._turns_since_memory = prior_user_turns % agent._memory_nudge_interval
524
-
525
-
526
- # Prefill messages (few-shot priming) are injected at API-call time only,
527
- # never stored in the messages list. This keeps them ephemeral: they won't
528
- # be saved to session DB, session logs, or batch trajectories, but they're
529
- # automatically re-applied on every API call (including session continuations).
530
-
531
- # Track user turns for memory flush and periodic nudge logic
532
- agent._user_turn_count += 1
533
-
534
- # Reset the streaming context scrubber at the top of each turn so a
535
- # hung span from a prior interrupted stream can't taint this turn's
536
- # output.
537
- scrubber = getattr(agent, "_stream_context_scrubber", None)
538
- if scrubber is not None:
539
- scrubber.reset()
540
- # Reset the think scrubber for the same reason — an interrupted
541
- # prior stream may have left us inside an unterminated block.
542
- think_scrubber = getattr(agent, "_stream_think_scrubber", None)
543
- if think_scrubber is not None:
544
- think_scrubber.reset()
545
-
546
- # Preserve the original user message (no nudge injection).
547
- original_user_message = persist_user_message if persist_user_message is not None else user_message
548
-
549
- # Track memory nudge trigger (turn-based, checked here).
550
- # Skill trigger is checked AFTER the agent loop completes, based on
551
- # how many tool iterations THIS turn used.
552
- _should_review_memory = False
553
- if (agent._memory_nudge_interval > 0
554
- and "memory" in agent.valid_tool_names
555
- and agent._memory_store):
556
- agent._turns_since_memory += 1
557
- if agent._turns_since_memory >= agent._memory_nudge_interval:
558
- _should_review_memory = True
559
- agent._turns_since_memory = 0
560
-
561
- # Add user message
562
- user_msg = {"role": "user", "content": user_message}
563
- messages.append(user_msg)
564
- current_turn_user_idx = len(messages) - 1
565
- agent._persist_user_message_idx = current_turn_user_idx
566
-
567
- if not agent.quiet_mode:
568
- _print_preview = _summarize_user_message_for_log(user_message)
569
- agent._safe_print(f"💬 Starting conversation: '{_print_preview[:60]}{'...' if len(_print_preview) > 60 else ''}'")
570
-
571
- # ── System prompt (cached per session for prefix caching) ──
572
- # Built once on first call, reused for all subsequent calls.
573
- # Only rebuilt after context compression events (which invalidate
574
- # the cache and reload memory from disk).
575
- #
576
- # For continuing sessions (gateway creates a fresh AIAgent per
577
- # message), we load the stored system prompt from the session DB
578
- # instead of rebuilding. Rebuilding would pick up memory changes
579
- # from disk that the model already knows about (it wrote them!),
580
- # producing a different system prompt and breaking the Anthropic
581
- # prefix cache.
582
- if agent._cached_system_prompt is None:
583
- _restore_or_build_system_prompt(agent, system_message, conversation_history)
584
-
585
- active_system_prompt = agent._cached_system_prompt
586
-
587
- # ── Preflight context compression ──
588
- # Before entering the main loop, check if the loaded conversation
589
- # history already exceeds the model's context threshold. This handles
590
- # cases where a user switches to a model with a smaller context window
591
- # while having a large existing session — compress proactively rather
592
- # than waiting for an API error (which might be caught as a non-retryable
593
- # 4xx and abort the request entirely).
594
- if (
595
- agent.compression_enabled
596
- and len(messages) > agent.context_compressor.protect_first_n
597
- + agent.context_compressor.protect_last_n + 1
598
- ):
599
- # Include tool schema tokens — with many tools these can add
600
- # 20-30K+ tokens that the old sys+msg estimate missed entirely.
601
- _preflight_tokens = estimate_request_tokens_rough(
602
- messages,
603
- system_prompt=active_system_prompt or "",
604
- tools=agent.tools or None,
605
- )
606
- _compressor = agent.context_compressor
607
- _defer_preflight = getattr(
608
- _compressor,
609
- "should_defer_preflight_to_real_usage",
610
- lambda _tokens: False,
611
- )
612
- _preflight_deferred = _defer_preflight(_preflight_tokens)
613
-
614
- if not _preflight_deferred:
615
- # Keep the CLI/ACP context display in sync with what preflight
616
- # actually measured. The status bar reads
617
- # ``compressor.last_prompt_tokens``, which otherwise only updates
618
- # from a *successful* API response. When the conversation has grown
619
- # since the last successful call — or when compression then fails
620
- # (e.g. the auxiliary summary model times out) and no fresh usage
621
- # arrives — the bar stays stuck at the old, smaller value while
622
- # preflight reports a much larger number, looking out of sync.
623
- # Seed it with the fresh estimate (only ever revising upward; a real
624
- # ``update_from_response`` will correct it after the next API call).
625
- # Skipped when deferring — a deferred estimate is known to over-count
626
- # vs the last real provider prompt, so trusting it for the display
627
- # would re-introduce the very desync we're avoiding.
628
- if _preflight_tokens > (_compressor.last_prompt_tokens or 0):
629
- _compressor.last_prompt_tokens = _preflight_tokens
630
-
631
- if _preflight_deferred:
632
- logger.info(
633
- "Skipping preflight compression: rough estimate ~%s >= %s, "
634
- "but last real provider prompt was %s after compression",
635
- f"{_preflight_tokens:,}",
636
- f"{_compressor.threshold_tokens:,}",
637
- f"{_compressor.last_real_prompt_tokens:,}",
638
- )
639
- elif _compressor.should_compress(_preflight_tokens):
640
- logger.info(
641
- "Preflight compression: ~%s tokens >= %s threshold (model %s, ctx %s)",
642
- f"{_preflight_tokens:,}",
643
- f"{_compressor.threshold_tokens:,}",
644
- agent.model,
645
- f"{_compressor.context_length:,}",
646
- )
647
- agent._emit_status(
648
- f"📦 Preflight compression: ~{_preflight_tokens:,} tokens "
649
- f">= {_compressor.threshold_tokens:,} threshold. "
650
- "This may take a moment."
651
- )
652
- # May need multiple passes for very large sessions with small
653
- # context windows (each pass summarises the middle N turns).
654
- for _pass in range(3):
655
- _orig_len = len(messages)
656
- messages, active_system_prompt = agent._compress_context(
657
- messages, system_message, approx_tokens=_preflight_tokens,
658
- task_id=effective_task_id,
659
- )
660
- if len(messages) >= _orig_len:
661
- break # Cannot compress further
662
- # Compression created a new session — clear the history
663
- # reference so _flush_messages_to_session_db writes ALL
664
- # compressed messages to the new session's SQLite, not
665
- # skipping them because conversation_history is still the
666
- # pre-compression length.
667
- conversation_history = None
668
- # Fix: reset retry counters after compression so the model
669
- # gets a fresh budget on the compressed context. Without
670
- # this, pre-compression retries carry over and the model
671
- # hits "(empty)" immediately after compression-induced
672
- # context loss.
673
- agent._empty_content_retries = 0
674
- agent._thinking_prefill_retries = 0
675
- agent._last_content_with_tools = None
676
- agent._last_content_tools_all_housekeeping = False
677
- agent._mute_post_response = False
678
- # Re-estimate after compression
679
- _preflight_tokens = estimate_request_tokens_rough(
680
- messages,
681
- system_prompt=active_system_prompt or "",
682
- tools=agent.tools or None,
683
- )
684
- if not _compressor.should_compress(_preflight_tokens):
685
- break # Under threshold or anti-thrash guard stopped it
686
-
687
- # Plugin hook: pre_llm_call
688
- # Fired once per turn before the tool-calling loop. Plugins can
689
- # return a dict with a ``context`` key (or a plain string) whose
690
- # value is appended to the current turn's user message.
691
- #
692
- # Context is ALWAYS injected into the user message, never the
693
- # system prompt. This preserves the prompt cache prefix — the
694
- # system prompt stays identical across turns so cached tokens
695
- # are reused. The system prompt is Hermes's territory; plugins
696
- # contribute context alongside the user's input.
697
- #
698
- # All injected context is ephemeral (not persisted to session DB).
699
- _plugin_user_context = ""
700
- try:
701
- from hermes_cli.plugins import invoke_hook as _invoke_hook
702
- _pre_results = _invoke_hook(
703
- "pre_llm_call",
704
- session_id=agent.session_id,
705
- user_message=original_user_message,
706
- conversation_history=list(messages),
707
- is_first_turn=(not bool(conversation_history)),
708
- model=agent.model,
709
- platform=getattr(agent, "platform", None) or "",
710
- sender_id=getattr(agent, "_user_id", None) or "",
711
- )
712
- _ctx_parts: list[str] = []
713
- for r in _pre_results:
714
- if isinstance(r, dict) and r.get("context"):
715
- _ctx_parts.append(str(r["context"]))
716
- elif isinstance(r, str) and r.strip():
717
- _ctx_parts.append(r)
718
- if _ctx_parts:
719
- _plugin_user_context = "\n\n".join(_ctx_parts)
720
- except Exception as exc:
721
- logger.warning("pre_llm_call hook failed: %s", exc)
722
-
723
- # Main conversation loop
525
+ user_message = _ctx.user_message
526
+ original_user_message = _ctx.original_user_message
527
+ messages = _ctx.messages
528
+ conversation_history = _ctx.conversation_history
529
+ active_system_prompt = _ctx.active_system_prompt
530
+ effective_task_id = _ctx.effective_task_id
531
+ turn_id = _ctx.turn_id
532
+ current_turn_user_idx = _ctx.current_turn_user_idx
533
+ _should_review_memory = _ctx.should_review_memory
534
+ _plugin_user_context = _ctx.plugin_user_context
535
+ _ext_prefetch_cache = _ctx.ext_prefetch_cache
536
+
537
+ # Main conversation loop counters (pure locals consumed by the loop below).
724
538
  api_call_count = 0
725
539
  final_response = None
726
540
  interrupted = False
@@ -732,53 +546,6 @@ def run_conversation(
732
546
  compression_attempts = 0
733
547
  _turn_exit_reason = "unknown" # Diagnostic: why the loop ended
734
548
 
735
- # Per-turn file-mutation verifier state. Keyed by resolved path;
736
- # each failed ``write_file`` / ``patch`` call records the error
737
- # preview. Later successful writes to the same path remove the
738
- # entry (the model recovered). At end-of-turn, any entries still
739
- # present are surfaced in an advisory footer so the model cannot
740
- # over-claim success while the file is actually unchanged on disk.
741
- agent._turn_failed_file_mutations: Dict[str, Dict[str, Any]] = {}
742
-
743
- # Record the execution thread so interrupt()/clear_interrupt() can
744
- # scope the tool-level interrupt signal to THIS agent's thread only.
745
- # Must be set before any thread-scoped interrupt syncing.
746
- agent._execution_thread_id = threading.current_thread().ident
747
-
748
- # Always clear stale per-thread state from a previous turn. If an
749
- # interrupt arrived before startup finished, preserve it and bind it
750
- # to this execution thread now instead of dropping it on the floor.
751
- _ra()._set_interrupt(False, agent._execution_thread_id)
752
- if agent._interrupt_requested:
753
- _ra()._set_interrupt(True, agent._execution_thread_id)
754
- agent._interrupt_thread_signal_pending = False
755
- else:
756
- agent._interrupt_message = None
757
- agent._interrupt_thread_signal_pending = False
758
-
759
- # Notify memory providers of the new turn so cadence tracking works.
760
- # Must happen BEFORE prefetch_all() so providers know which turn it is
761
- # and can gate context/dialectic refresh via contextCadence/dialecticCadence.
762
- if agent._memory_manager:
763
- try:
764
- _turn_msg = original_user_message if isinstance(original_user_message, str) else ""
765
- agent._memory_manager.on_turn_start(agent._user_turn_count, _turn_msg)
766
- except Exception:
767
- pass
768
-
769
- # External memory provider: prefetch once before the tool loop.
770
- # Reuse the cached result on every iteration to avoid re-calling
771
- # prefetch_all() on each tool call (10 tool calls = 10x latency + cost).
772
- # Use original_user_message (clean input) — user_message may contain
773
- # injected skill content that bloats / breaks provider queries.
774
- _ext_prefetch_cache = ""
775
- if agent._memory_manager:
776
- try:
777
- _query = original_user_message if isinstance(original_user_message, str) else ""
778
- _ext_prefetch_cache = agent._memory_manager.prefetch_all(_query) or ""
779
- except Exception:
780
- pass
781
-
782
549
  # Optional opt-in runtime: if api_mode == codex_app_server, hand the
783
550
  # turn to the codex app-server subprocess (terminal/file ops/patching
784
551
  # all run inside Codex). Default Hermes path is bypassed entirely.
@@ -872,7 +639,8 @@ def run_conversation(
872
639
  for _si in range(len(messages) - 1, -1, -1):
873
640
  _sm = messages[_si]
874
641
  if isinstance(_sm, dict) and _sm.get("role") == "tool":
875
- marker = f"\n\nUser guidance: {_pre_api_steer}"
642
+ from agent.prompt_builder import format_steer_marker
643
+ marker = format_steer_marker(_pre_api_steer)
876
644
  existing = _sm.get("content", "")
877
645
  if isinstance(existing, str):
878
646
  _sm["content"] = existing + marker
@@ -929,7 +697,11 @@ def run_conversation(
929
697
  # landed after an orphan tool result). Most providers return
930
698
  # empty content on malformed sequences, which would otherwise
931
699
  # retrigger the empty-retry loop indefinitely.
932
- repaired_seq = agent._repair_message_sequence(messages)
700
+ # repair_message_sequence_with_cursor also recomputes the SessionDB
701
+ # flush cursor (_last_flushed_db_idx) when repair compacts the list,
702
+ # so the turn-end flush doesn't skip the assistant/tool chain (#44837).
703
+ from agent.agent_runtime_helpers import repair_message_sequence_with_cursor
704
+ repaired_seq = repair_message_sequence_with_cursor(agent, messages)
933
705
  if repaired_seq > 0:
934
706
  request_logger.info(
935
707
  "Repaired %s message-alternation violations before request (session=%s)",
@@ -977,7 +749,7 @@ def run_conversation(
977
749
  # Uses new dicts so the internal messages list retains the fields
978
750
  # for Codex Responses compatibility.
979
751
  if agent._should_sanitize_tool_calls():
980
- agent._sanitize_tool_calls_for_strict_api(api_msg)
752
+ agent._sanitize_tool_calls_for_strict_api(api_msg, model=agent.model)
981
753
  # Keep 'reasoning_details' - OpenRouter uses this for multi-turn reasoning context
982
754
  # The signature field helps maintain reasoning continuity
983
755
  api_messages.append(api_msg)
@@ -1037,7 +809,10 @@ def run_conversation(
1037
809
  # a thinking-only turn. Runs on the per-call copy only — the
1038
810
  # stored conversation history keeps the reasoning block for the
1039
811
  # UI transcript and session persistence.
1040
- api_messages = agent._drop_thinking_only_and_merge_users(api_messages)
812
+ api_messages = agent._drop_thinking_only_and_merge_users(
813
+ api_messages,
814
+ drop_codex_reasoning_items=agent.api_mode != "codex_responses",
815
+ )
1041
816
 
1042
817
  # Normalize message whitespace and tool-call JSON for consistent
1043
818
  # prefix matching. Ensures bit-perfect prefixes across turns,
@@ -1133,26 +908,14 @@ def run_conversation(
1133
908
  api_start_time = time.time()
1134
909
  retry_count = 0
1135
910
  max_retries = agent._api_max_retries
1136
- primary_recovery_attempted = False
911
+ _retry = TurnRetryState()
1137
912
  max_compression_attempts = 3
1138
- codex_auth_retry_attempted=False
1139
- anthropic_auth_retry_attempted=False
1140
- nous_auth_retry_attempted=False
1141
- nous_paid_entitlement_refresh_attempted=False
1142
- copilot_auth_retry_attempted=False
1143
- thinking_sig_retry_attempted = False
1144
- invalid_encrypted_content_retry_attempted = False
1145
- image_shrink_retry_attempted = False
1146
- multimodal_tool_content_retry_attempted = False
1147
- oauth_1m_beta_retry_attempted = False
1148
- llama_cpp_grammar_retry_attempted = False
1149
- has_retried_429 = False
1150
- restart_with_compressed_messages = False
1151
- restart_with_length_continuation = False
1152
913
 
1153
914
  finish_reason = "stop"
1154
915
  response = None # Guard against UnboundLocalError if all retries fail
1155
916
  api_kwargs = None # Guard against UnboundLocalError in except handler
917
+ api_request_id = f"{turn_id}:api:{api_call_count}"
918
+ agent._current_api_request_id = api_request_id
1156
919
 
1157
920
  while retry_count < max_retries:
1158
921
  # ── Nous Portal rate limit guard ──────────────────────
@@ -1179,7 +942,7 @@ def run_conversation(
1179
942
  if agent._try_activate_fallback():
1180
943
  retry_count = 0
1181
944
  compression_attempts = 0
1182
- primary_recovery_attempted = False
945
+ _retry.primary_recovery_attempted = False
1183
946
  continue
1184
947
  # No fallback available — surface buffered context
1185
948
  # so user sees the rate-limit message that led here.
@@ -1218,39 +981,83 @@ def run_conversation(
1218
981
  _sanitize_structure_non_ascii(api_kwargs)
1219
982
  if agent.api_mode == "codex_responses":
1220
983
  api_kwargs = agent._get_transport().preflight_kwargs(api_kwargs, allow_stream=False)
1221
-
1222
984
  try:
1223
- from hermes_cli.plugins import invoke_hook as _invoke_hook
1224
- request_messages = api_kwargs.get("messages")
1225
- if not isinstance(request_messages, list):
1226
- request_messages = api_kwargs.get("input")
1227
- if not isinstance(request_messages, list):
1228
- request_messages = api_messages
1229
- # Shallow-copy the outer list so plugins that retain the
1230
- # reference for async snapshotting don't observe later
1231
- # mutations of api_messages. The inner dicts are not
1232
- # mutated by the agent loop, so a shallow copy is
1233
- # sufficient; a deepcopy would walk every tool result
1234
- # and base64 image on every API call.
1235
- _invoke_hook(
1236
- "pre_api_request",
985
+ from hermes_cli.middleware import apply_llm_request_middleware
986
+
987
+ _llm_request_mw = apply_llm_request_middleware(
988
+ api_kwargs,
1237
989
  task_id=effective_task_id,
990
+ turn_id=turn_id,
991
+ api_request_id=api_request_id,
1238
992
  session_id=agent.session_id or "",
1239
- user_message=original_user_message,
1240
- conversation_history=list(messages),
1241
993
  platform=agent.platform or "",
1242
994
  model=agent.model,
1243
995
  provider=agent.provider,
1244
996
  base_url=agent.base_url,
1245
997
  api_mode=agent.api_mode,
1246
998
  api_call_count=api_call_count,
1247
- request_messages=list(request_messages) if isinstance(request_messages, list) else [],
1248
- message_count=len(api_messages),
1249
- tool_count=len(agent.tools or []),
1250
- approx_input_tokens=approx_tokens,
1251
- request_char_count=total_chars,
1252
- max_tokens=agent.max_tokens,
1253
999
  )
1000
+ api_kwargs = _llm_request_mw.payload
1001
+ _original_api_kwargs = _llm_request_mw.original_payload
1002
+ _llm_middleware_trace = _llm_request_mw.trace
1003
+ except Exception:
1004
+ _original_api_kwargs = dict(api_kwargs)
1005
+ _llm_middleware_trace = []
1006
+
1007
+ try:
1008
+ from hermes_cli.plugins import (
1009
+ has_hook,
1010
+ invoke_hook as _invoke_hook,
1011
+ )
1012
+ if has_hook("pre_api_request"):
1013
+ request_messages = api_kwargs.get("messages")
1014
+ if not isinstance(request_messages, list):
1015
+ request_messages = api_kwargs.get("input")
1016
+ if not isinstance(request_messages, list):
1017
+ request_messages = api_messages
1018
+ # Shallow-copy the outer list so plugins that retain the
1019
+ # reference for async snapshotting don't observe later
1020
+ # mutations of api_messages. The inner dicts are not
1021
+ # mutated by the agent loop, so a shallow copy is
1022
+ # sufficient; a deepcopy would walk every tool result
1023
+ # and base64 image on every API call.
1024
+ #
1025
+ # The ``request_messages`` and ``conversation_history``
1026
+ # kwargs below are pre-existing raw passthroughs
1027
+ # consumed by the bundled langfuse plugin
1028
+ # (``plugins/observability/langfuse/__init__.py:_coerce_request_messages``).
1029
+ # They predate ``request`` and are intentionally NOT
1030
+ # sanitised — secrets are not expected here because
1031
+ # ``api_kwargs`` is the same object passed to the
1032
+ # provider client. New consumers should read the
1033
+ # sanitised view from ``request["body"]["messages"]``.
1034
+ _request_payload = agent._api_request_payload_for_hook(api_kwargs)
1035
+ _invoke_hook(
1036
+ "pre_api_request",
1037
+ task_id=effective_task_id,
1038
+ turn_id=turn_id,
1039
+ api_request_id=api_request_id,
1040
+ session_id=agent.session_id or "",
1041
+ user_message=original_user_message,
1042
+ conversation_history=list(messages),
1043
+ platform=agent.platform or "",
1044
+ model=agent.model,
1045
+ provider=agent.provider,
1046
+ base_url=agent.base_url,
1047
+ api_mode=agent.api_mode,
1048
+ api_call_count=api_call_count,
1049
+ request_messages=list(request_messages)
1050
+ if isinstance(request_messages, list)
1051
+ else [],
1052
+ message_count=len(api_messages),
1053
+ tool_count=len(agent.tools or []),
1054
+ approx_input_tokens=approx_tokens,
1055
+ request_char_count=total_chars,
1056
+ max_tokens=agent.max_tokens,
1057
+ started_at=api_start_time,
1058
+ middleware_trace=list(_llm_middleware_trace),
1059
+ request=_request_payload,
1060
+ )
1254
1061
  except Exception:
1255
1062
  pass
1256
1063
 
@@ -1300,12 +1107,31 @@ def run_conversation(
1300
1107
  if isinstance(getattr(agent, "client", None), Mock):
1301
1108
  _use_streaming = False
1302
1109
 
1303
- if _use_streaming:
1304
- response = agent._interruptible_streaming_api_call(
1305
- api_kwargs, on_first_delta=_stop_spinner
1306
- )
1307
- else:
1308
- response = agent._interruptible_api_call(api_kwargs)
1110
+ def _perform_api_call(next_api_kwargs):
1111
+ if _use_streaming:
1112
+ return agent._interruptible_streaming_api_call(
1113
+ next_api_kwargs, on_first_delta=_stop_spinner
1114
+ )
1115
+ return agent._interruptible_api_call(next_api_kwargs)
1116
+
1117
+ from hermes_cli.middleware import run_llm_execution_middleware
1118
+
1119
+ response = run_llm_execution_middleware(
1120
+ api_kwargs,
1121
+ _perform_api_call,
1122
+ original_request=_original_api_kwargs,
1123
+ task_id=effective_task_id,
1124
+ turn_id=turn_id,
1125
+ api_request_id=api_request_id,
1126
+ session_id=agent.session_id or "",
1127
+ platform=agent.platform or "",
1128
+ model=agent.model,
1129
+ provider=agent.provider,
1130
+ base_url=agent.base_url,
1131
+ api_mode=agent.api_mode,
1132
+ api_call_count=api_call_count,
1133
+ middleware_trace=list(_llm_middleware_trace),
1134
+ )
1309
1135
 
1310
1136
  api_duration = time.time() - api_start_time
1311
1137
 
@@ -1406,6 +1232,21 @@ def run_conversation(
1406
1232
  error_details.append("response.choices is empty")
1407
1233
 
1408
1234
  if response_invalid:
1235
+ agent._invoke_api_request_error_hook(
1236
+ task_id=effective_task_id,
1237
+ turn_id=turn_id,
1238
+ api_request_id=api_request_id,
1239
+ api_call_count=api_call_count,
1240
+ api_start_time=api_start_time,
1241
+ api_kwargs=api_kwargs,
1242
+ error_type="InvalidAPIResponse",
1243
+ error_message=", ".join(error_details) or "Invalid API response",
1244
+ status_code=getattr(getattr(response, "error", None), "code", None),
1245
+ retry_count=retry_count,
1246
+ max_retries=max_retries,
1247
+ retryable=True,
1248
+ reason="invalid_response",
1249
+ )
1409
1250
  # Stop spinner silently — retry status is now buffered
1410
1251
  # and only surfaced if every retry+fallback exhausts.
1411
1252
  if thinking_spinner:
@@ -1426,7 +1267,7 @@ def run_conversation(
1426
1267
  if agent._try_activate_fallback():
1427
1268
  retry_count = 0
1428
1269
  compression_attempts = 0
1429
- primary_recovery_attempted = False
1270
+ _retry.primary_recovery_attempted = False
1430
1271
  continue
1431
1272
 
1432
1273
  # Check for error field in response (some providers include this)
@@ -1497,7 +1338,7 @@ def run_conversation(
1497
1338
  if agent._try_activate_fallback():
1498
1339
  retry_count = 0
1499
1340
  compression_attempts = 0
1500
- primary_recovery_attempted = False
1341
+ _retry.primary_recovery_attempted = False
1501
1342
  continue
1502
1343
  # Terminal — flush buffered retry trace so user sees what happened.
1503
1344
  agent._flush_status_buffer()
@@ -1580,6 +1421,106 @@ def run_conversation(
1580
1421
  )
1581
1422
  finish_reason = "length"
1582
1423
 
1424
+ # ── Content-policy refusal (HTTP 200) ──────────────────
1425
+ # The model — or the provider's safety system — returned a
1426
+ # *successful* response whose stop/finish reason is a refusal:
1427
+ # Anthropic ``stop_reason="refusal"`` → ``content_filter``;
1428
+ # OpenAI / portal ``finish_reason="content_filter"`` or a
1429
+ # populated ``message.refusal`` (mapped in the chat_completions
1430
+ # transport); Bedrock ``guardrail_intervened``. The content is
1431
+ # typically empty, so without this branch the response falls
1432
+ # through to the empty-response / invalid-response retry loops
1433
+ # and is mis-surfaced as "rate limited" / "no content after
1434
+ # retries" — burning paid attempts reproducing a deterministic
1435
+ # refusal. Surface it clearly and stop. Mirrors the
1436
+ # exception-based ``content_policy_blocked`` recovery: try a
1437
+ # configured fallback once, otherwise return the refusal.
1438
+ if finish_reason == "content_filter":
1439
+ _refusal_transport = agent._get_transport()
1440
+ if agent.api_mode == "anthropic_messages":
1441
+ _refusal_result = _refusal_transport.normalize_response(
1442
+ response, strip_tool_prefix=agent._is_anthropic_oauth
1443
+ )
1444
+ else:
1445
+ _refusal_result = _refusal_transport.normalize_response(response)
1446
+ _refusal_text = (getattr(_refusal_result, "content", None) or "").strip()
1447
+ # Some refusals carry the explanation only in the reasoning
1448
+ # channel; fall back to it so the user sees *something*.
1449
+ if not _refusal_text:
1450
+ _refusal_text = (agent._extract_reasoning(_refusal_result) or "").strip()
1451
+
1452
+ agent._invoke_api_request_error_hook(
1453
+ task_id=effective_task_id,
1454
+ turn_id=turn_id,
1455
+ api_request_id=api_request_id,
1456
+ api_call_count=api_call_count,
1457
+ api_start_time=api_start_time,
1458
+ api_kwargs=api_kwargs,
1459
+ error_type="ContentPolicyBlocked",
1460
+ error_message=_refusal_text or "model declined to respond (content_filter)",
1461
+ status_code=None,
1462
+ retry_count=retry_count,
1463
+ max_retries=max_retries,
1464
+ retryable=False,
1465
+ reason=FailoverReason.content_policy_blocked.value,
1466
+ )
1467
+
1468
+ if thinking_spinner:
1469
+ thinking_spinner.stop("")
1470
+ thinking_spinner = None
1471
+ if agent.thinking_callback:
1472
+ agent.thinking_callback("")
1473
+
1474
+ # Deterministic for the unchanged prompt — never retry.
1475
+ # Try a configured fallback once (a different model may not
1476
+ # refuse); otherwise surface the refusal terminally.
1477
+ if agent._has_pending_fallback():
1478
+ agent._buffer_status(
1479
+ "⚠️ Model declined to respond (safety refusal) — trying fallback..."
1480
+ )
1481
+ if agent._try_activate_fallback():
1482
+ retry_count = 0
1483
+ compression_attempts = 0
1484
+ _retry.primary_recovery_attempted = False
1485
+ continue
1486
+
1487
+ agent._flush_status_buffer()
1488
+ _refusal_log = (
1489
+ _refusal_text[:500] + "..."
1490
+ if len(_refusal_text) > 500
1491
+ else _refusal_text
1492
+ )
1493
+ logger.warning(
1494
+ "%sModel declined to respond (finish_reason=content_filter). "
1495
+ "model=%s provider=%s refusal=%s",
1496
+ agent.log_prefix, agent.model, agent.provider,
1497
+ _refusal_log or "(no text)",
1498
+ )
1499
+ agent._emit_status(
1500
+ "⚠️ The model declined to respond to this request (safety refusal)."
1501
+ )
1502
+
1503
+ _refusal_detail = (
1504
+ f"Model's explanation: {_refusal_text}"
1505
+ if _refusal_text
1506
+ else "The model returned no explanation."
1507
+ )
1508
+ _refusal_response = (
1509
+ "⚠️ The model declined to respond to this request "
1510
+ "(safety refusal — not a Hermes/gateway failure).\n\n"
1511
+ f"{_refusal_detail}\n\n"
1512
+ f"{_CONTENT_POLICY_RECOVERY_HINT}"
1513
+ )
1514
+
1515
+ agent._cleanup_task_resources(effective_task_id)
1516
+ agent._persist_session(messages, conversation_history)
1517
+ return _content_policy_blocked_result(
1518
+ messages,
1519
+ api_call_count,
1520
+ final_response=_refusal_response,
1521
+ error_detail=_refusal_text or "model declined (content_filter)",
1522
+ )
1523
+
1583
1524
  if finish_reason == "length":
1584
1525
  if getattr(response, "id", "") == PARTIAL_STREAM_STUB_ID:
1585
1526
  agent._vprint(
@@ -1721,7 +1662,7 @@ def run_conversation(
1721
1662
  }
1722
1663
  messages.append(continue_msg)
1723
1664
  agent._session_messages = messages
1724
- restart_with_length_continuation = True
1665
+ _retry.restart_with_length_continuation = True
1725
1666
  break
1726
1667
 
1727
1668
  partial_response = agent._strip_think_blocks("".join(truncated_response_parts)).strip()
@@ -1970,7 +1911,7 @@ def run_conversation(
1970
1911
  f"({hit_pct:.0f}% hit, {written:,} written)"
1971
1912
  )
1972
1913
 
1973
- has_retried_429 = False # Reset on success
1914
+ _retry.has_retried_429 = False # Reset on success
1974
1915
  # Note: don't clear the retry buffer here — an "API call
1975
1916
  # success" only means we got bytes back, not that we got
1976
1917
  # usable content. Empty responses still loop through the
@@ -1998,7 +1939,7 @@ def run_conversation(
1998
1939
  agent._vprint(f"{agent.log_prefix}⚡ Interrupted during API call.", force=True)
1999
1940
  agent._persist_session(messages, conversation_history)
2000
1941
  interrupted = True
2001
- final_response = f"Operation interrupted: waiting for model response ({api_elapsed:.1f}s elapsed)."
1942
+ final_response = f"{INTERRUPT_WAITING_FOR_MODEL_PREFIX}{api_elapsed:.1f}s elapsed)."
2002
1943
  break
2003
1944
 
2004
1945
  except Exception as api_error:
@@ -2278,6 +2219,21 @@ def run_conversation(
2278
2219
  classified.retryable, classified.should_compress,
2279
2220
  classified.should_rotate_credential, classified.should_fallback,
2280
2221
  )
2222
+ agent._invoke_api_request_error_hook(
2223
+ task_id=effective_task_id,
2224
+ turn_id=turn_id,
2225
+ api_request_id=api_request_id,
2226
+ api_call_count=api_call_count,
2227
+ api_start_time=api_start_time,
2228
+ api_kwargs=api_kwargs,
2229
+ error_type=type(api_error).__name__,
2230
+ error_message=str(api_error),
2231
+ status_code=status_code,
2232
+ retry_count=retry_count,
2233
+ max_retries=max_retries,
2234
+ retryable=classified.retryable,
2235
+ reason=classified.reason.value,
2236
+ )
2281
2237
 
2282
2238
  if (
2283
2239
  classified.reason == FailoverReason.billing
@@ -2285,9 +2241,9 @@ def run_conversation(
2285
2241
  getattr(agent, "provider", "") or "",
2286
2242
  getattr(agent, "base_url", "") or "",
2287
2243
  )
2288
- and not nous_paid_entitlement_refresh_attempted
2244
+ and not _retry.nous_paid_entitlement_refresh_attempted
2289
2245
  ):
2290
- nous_paid_entitlement_refresh_attempted = True
2246
+ _retry.nous_paid_entitlement_refresh_attempted = True
2291
2247
  if _try_refresh_nous_paid_entitlement_credentials(agent):
2292
2248
  agent._vprint(
2293
2249
  f"{agent.log_prefix}🔐 Nous paid access verified — "
@@ -2296,9 +2252,9 @@ def run_conversation(
2296
2252
  )
2297
2253
  continue
2298
2254
 
2299
- recovered_with_pool, has_retried_429 = agent._recover_with_credential_pool(
2255
+ recovered_with_pool, _retry.has_retried_429 = agent._recover_with_credential_pool(
2300
2256
  status_code=status_code,
2301
- has_retried_429=has_retried_429,
2257
+ has_retried_429=_retry.has_retried_429,
2302
2258
  classified_reason=classified.reason,
2303
2259
  error_context=error_context,
2304
2260
  )
@@ -2313,10 +2269,14 @@ def run_conversation(
2313
2269
  # fails, fall through to normal error handling.
2314
2270
  if (
2315
2271
  classified.reason == FailoverReason.image_too_large
2316
- and not image_shrink_retry_attempted
2272
+ and not _retry.image_shrink_retry_attempted
2317
2273
  ):
2318
- image_shrink_retry_attempted = True
2319
- if agent._try_shrink_image_parts_in_messages(api_messages):
2274
+ _retry.image_shrink_retry_attempted = True
2275
+ image_max_dimension = _image_error_max_dimension(api_error) or 8000
2276
+ if agent._try_shrink_image_parts_in_messages(
2277
+ api_messages,
2278
+ max_dimension=image_max_dimension,
2279
+ ):
2320
2280
  agent._vprint(
2321
2281
  f"{agent.log_prefix}📐 Image(s) exceeded provider size limit — "
2322
2282
  f"shrank and retrying...",
@@ -2338,9 +2298,9 @@ def run_conversation(
2338
2298
  # downgrade, and retry once. See issue #27344.
2339
2299
  if (
2340
2300
  classified.reason == FailoverReason.multimodal_tool_content_unsupported
2341
- and not multimodal_tool_content_retry_attempted
2301
+ and not _retry.multimodal_tool_content_retry_attempted
2342
2302
  ):
2343
- multimodal_tool_content_retry_attempted = True
2303
+ _retry.multimodal_tool_content_retry_attempted = True
2344
2304
  if agent._try_strip_image_parts_from_tool_messages(api_messages):
2345
2305
  agent._vprint(
2346
2306
  f"{agent.log_prefix}📐 Provider rejected list-type tool content — "
@@ -2367,9 +2327,9 @@ def run_conversation(
2367
2327
  classified.reason == FailoverReason.oauth_long_context_beta_forbidden
2368
2328
  and agent.api_mode == "anthropic_messages"
2369
2329
  and agent._is_anthropic_oauth
2370
- and not oauth_1m_beta_retry_attempted
2330
+ and not _retry.oauth_1m_beta_retry_attempted
2371
2331
  ):
2372
- oauth_1m_beta_retry_attempted = True
2332
+ _retry.oauth_1m_beta_retry_attempted = True
2373
2333
  if not getattr(agent, "_oauth_1m_beta_disabled", False):
2374
2334
  agent._oauth_1m_beta_disabled = True
2375
2335
  try:
@@ -2388,9 +2348,9 @@ def run_conversation(
2388
2348
  agent.api_mode == "codex_responses"
2389
2349
  and agent.provider in {"openai-codex", "xai-oauth"}
2390
2350
  and status_code == 401
2391
- and not codex_auth_retry_attempted
2351
+ and not _retry.codex_auth_retry_attempted
2392
2352
  ):
2393
- codex_auth_retry_attempted = True
2353
+ _retry.codex_auth_retry_attempted = True
2394
2354
  if agent._try_refresh_codex_client_credentials(force=True):
2395
2355
  _label = "xAI OAuth" if agent.provider == "xai-oauth" else "Codex"
2396
2356
  agent._buffer_vprint(f"🔐 {_label} auth refreshed after 401. Retrying request...")
@@ -2399,9 +2359,9 @@ def run_conversation(
2399
2359
  agent.api_mode == "chat_completions"
2400
2360
  and agent.provider == "nous"
2401
2361
  and status_code == 401
2402
- and not nous_auth_retry_attempted
2362
+ and not _retry.nous_auth_retry_attempted
2403
2363
  ):
2404
- nous_auth_retry_attempted = True
2364
+ _retry.nous_auth_retry_attempted = True
2405
2365
  if agent._try_refresh_nous_client_credentials(force=True):
2406
2366
  print(f"{agent.log_prefix}🔐 Nous agent key refreshed after 401. Retrying request...")
2407
2367
  continue
@@ -2430,9 +2390,9 @@ def run_conversation(
2430
2390
  if (
2431
2391
  agent.provider == "copilot"
2432
2392
  and status_code == 401
2433
- and not copilot_auth_retry_attempted
2393
+ and not _retry.copilot_auth_retry_attempted
2434
2394
  ):
2435
- copilot_auth_retry_attempted = True
2395
+ _retry.copilot_auth_retry_attempted = True
2436
2396
  if agent._try_refresh_copilot_client_credentials():
2437
2397
  agent._buffer_vprint(f"🔐 Copilot credentials refreshed after 401. Retrying request...")
2438
2398
  continue
@@ -2440,9 +2400,9 @@ def run_conversation(
2440
2400
  agent.api_mode == "anthropic_messages"
2441
2401
  and status_code == 401
2442
2402
  and hasattr(agent, '_anthropic_api_key')
2443
- and not anthropic_auth_retry_attempted
2403
+ and not _retry.anthropic_auth_retry_attempted
2444
2404
  ):
2445
- anthropic_auth_retry_attempted = True
2405
+ _retry.anthropic_auth_retry_attempted = True
2446
2406
  from agent.anthropic_adapter import _is_oauth_token
2447
2407
  from agent.azure_identity_adapter import is_token_provider
2448
2408
  if agent._try_refresh_anthropic_client_credentials():
@@ -2474,30 +2434,54 @@ def run_conversation(
2474
2434
  print(f"{agent.log_prefix} • Legacy cleanup: hermes config set ANTHROPIC_TOKEN \"\"")
2475
2435
  print(f"{agent.log_prefix} • Clear stale keys: hermes config set ANTHROPIC_API_KEY \"\"")
2476
2436
 
2477
- # ── Thinking block signature recovery ─────────────────
2437
+ # Thinking block signature recovery.
2438
+ #
2478
2439
  # Anthropic signs thinking blocks against the full turn
2479
- # content. Any upstream mutation (context compression,
2440
+ # content. Any upstream mutation (context compression,
2480
2441
  # session truncation, message merging) invalidates the
2481
- # signature HTTP 400. Recovery: strip reasoning_details
2482
- # from all messages so the next retry sends no thinking
2483
- # blocks at all. One-shot don't retry infinitely.
2442
+ # signature and the API replies HTTP 400 ("invalid
2443
+ # signature" or "cannot be modified"). Recovery strips
2444
+ # ``reasoning_details`` so the retry sends no thinking
2445
+ # blocks at all. One-shot per outer loop.
2446
+ #
2447
+ # The strip targets ``api_messages``, which is the
2448
+ # API-call-time list that ``_build_api_kwargs`` consumes
2449
+ # on every retry. ``api_messages`` was populated once at
2450
+ # the start of the turn from shallow copies of
2451
+ # ``messages``, so mutating it does not touch the
2452
+ # canonical store. The previous implementation popped
2453
+ # ``reasoning_details`` from ``messages`` instead, which
2454
+ # had two problems: ``api_messages`` carried its own
2455
+ # reference to the field through the shallow copy, so the
2456
+ # retry's wire payload still included thinking blocks and
2457
+ # the recovery never reached the API; and the mutation
2458
+ # persisted into ``state.db`` through any subsequent
2459
+ # ``_persist_session`` call, permanently corrupting the
2460
+ # conversation. Future turns would replay the stripped
2461
+ # state, hit the same 400, and the agent would terminate
2462
+ # with ``max_retries_exhausted``, often spawning
2463
+ # cascading compaction-ended sessions chained off the
2464
+ # corrupted parent.
2484
2465
  if (
2485
2466
  classified.reason == FailoverReason.thinking_signature
2486
- and not thinking_sig_retry_attempted
2467
+ and not _retry.thinking_sig_retry_attempted
2487
2468
  ):
2488
- thinking_sig_retry_attempted = True
2489
- for _m in messages:
2490
- if isinstance(_m, dict):
2469
+ _retry.thinking_sig_retry_attempted = True
2470
+ _api_stripped = 0
2471
+ for _m in api_messages:
2472
+ if isinstance(_m, dict) and "reasoning_details" in _m:
2491
2473
  _m.pop("reasoning_details", None)
2474
+ _api_stripped += 1
2492
2475
  agent._vprint(
2493
- f"{agent.log_prefix}⚠️ Thinking block signature invalid "
2494
- f"stripped all thinking blocks, retrying...",
2476
+ f"{agent.log_prefix}⚠️ Thinking block signature invalid, "
2477
+ f"stripped reasoning_details from api_messages for retry...",
2495
2478
  force=True,
2496
2479
  )
2497
2480
  logger.warning(
2498
2481
  "%sThinking block signature recovery: stripped "
2499
- "reasoning_details from %d messages",
2500
- agent.log_prefix, len(messages),
2482
+ "reasoning_details from %d api_messages "
2483
+ "(canonical messages unchanged)",
2484
+ agent.log_prefix, _api_stripped,
2501
2485
  )
2502
2486
  continue
2503
2487
 
@@ -2517,7 +2501,7 @@ def run_conversation(
2517
2501
  # handles it (the provider is rejecting something else).
2518
2502
  if (
2519
2503
  classified.reason == FailoverReason.invalid_encrypted_content
2520
- and not invalid_encrypted_content_retry_attempted
2504
+ and not _retry.invalid_encrypted_content_retry_attempted
2521
2505
  and agent.api_mode == "codex_responses"
2522
2506
  and bool(getattr(agent, "_codex_reasoning_replay_enabled", True))
2523
2507
  and any(
@@ -2528,7 +2512,7 @@ def run_conversation(
2528
2512
  for _m in messages
2529
2513
  )
2530
2514
  ):
2531
- invalid_encrypted_content_retry_attempted = True
2515
+ _retry.invalid_encrypted_content_retry_attempted = True
2532
2516
  replay_stats = agent._disable_codex_reasoning_replay(messages)
2533
2517
  agent._vprint(
2534
2518
  f"{agent.log_prefix}⚠️ Encrypted reasoning replay was rejected by the provider — "
@@ -2555,9 +2539,9 @@ def run_conversation(
2555
2539
  # fires only for users on llama.cpp's OAI server.
2556
2540
  if (
2557
2541
  classified.reason == FailoverReason.llama_cpp_grammar_pattern
2558
- and not llama_cpp_grammar_retry_attempted
2542
+ and not _retry.llama_cpp_grammar_retry_attempted
2559
2543
  ):
2560
- llama_cpp_grammar_retry_attempted = True
2544
+ _retry.llama_cpp_grammar_retry_attempted = True
2561
2545
  try:
2562
2546
  from tools.schema_sanitizer import strip_pattern_and_format
2563
2547
  _, _stripped = strip_pattern_and_format(agent.tools)
@@ -2660,6 +2644,61 @@ def run_conversation(
2660
2644
  # compress history and retry, not abort immediately.
2661
2645
  status_code = getattr(api_error, "status_code", None)
2662
2646
 
2647
+ # ── Respect disabled auto-compaction on overflow ──────
2648
+ # Ported from anomalyco/opencode#30749. When the user has
2649
+ # turned auto-compaction off (``compression.enabled: false``),
2650
+ # NO automatic compaction trigger may fire — including the
2651
+ # provider/request-size overflow recovery paths below
2652
+ # (long-context-tier 429, 413 payload-too-large, and
2653
+ # context-overflow). Without this guard the proactive
2654
+ # threshold path correctly honours the setting (see the
2655
+ # preflight check and the post-response ``should_compress``
2656
+ # gate) but a provider overflow error would still silently
2657
+ # compress + rotate the session, bypassing the user's
2658
+ # explicit choice. Surface a terminal error instead so the
2659
+ # user can compact manually (``/compress``), start fresh
2660
+ # (``/new``), switch to a larger-context model, or reduce
2661
+ # attachments. Forced compaction via ``/compress``
2662
+ # (``force=True``) is unaffected — it never reaches this loop.
2663
+ _overflow_reasons = {
2664
+ FailoverReason.long_context_tier,
2665
+ FailoverReason.payload_too_large,
2666
+ FailoverReason.context_overflow,
2667
+ }
2668
+ if (
2669
+ classified.reason in _overflow_reasons
2670
+ and not getattr(agent, "compression_enabled", True)
2671
+ ):
2672
+ agent._flush_status_buffer()
2673
+ agent._vprint(
2674
+ f"{agent.log_prefix}❌ Context overflow, but auto-compaction is disabled "
2675
+ f"(compression.enabled: false).",
2676
+ force=True,
2677
+ )
2678
+ agent._vprint(
2679
+ f"{agent.log_prefix} 💡 Run /compress to compact manually, /new to start fresh, "
2680
+ f"switch to a larger-context model, or reduce attachments.",
2681
+ force=True,
2682
+ )
2683
+ logger.error(
2684
+ f"{agent.log_prefix}Context overflow ({classified.reason.value}) with "
2685
+ f"auto-compaction disabled — not compressing."
2686
+ )
2687
+ agent._persist_session(messages, conversation_history)
2688
+ return {
2689
+ "messages": messages,
2690
+ "completed": False,
2691
+ "api_calls": api_call_count,
2692
+ "error": (
2693
+ "Context overflow and auto-compaction is disabled "
2694
+ "(compression.enabled: false). Run /compress to compact manually, "
2695
+ "/new to start fresh, or switch to a larger-context model."
2696
+ ),
2697
+ "partial": True,
2698
+ "failed": True,
2699
+ "compaction_disabled": True,
2700
+ }
2701
+
2663
2702
  # ── Anthropic Sonnet long-context tier gate ───────────
2664
2703
  # Anthropic returns HTTP 429 "Extra usage is required for
2665
2704
  # long context requests" when a Claude Max (or similar)
@@ -2713,7 +2752,7 @@ def run_conversation(
2713
2752
  f"(was {old_ctx:,}), retrying..."
2714
2753
  )
2715
2754
  time.sleep(2)
2716
- restart_with_compressed_messages = True
2755
+ _retry.restart_with_compressed_messages = True
2717
2756
  break
2718
2757
  # Fall through to normal error handling if compression
2719
2758
  # is exhausted or didn't help.
@@ -2746,7 +2785,7 @@ def run_conversation(
2746
2785
  if agent._try_activate_fallback(reason=classified.reason):
2747
2786
  retry_count = 0
2748
2787
  compression_attempts = 0
2749
- primary_recovery_attempted = False
2788
+ _retry.primary_recovery_attempted = False
2750
2789
  continue
2751
2790
 
2752
2791
  # ── Nous Portal: record rate limit & skip retries ─────
@@ -2805,10 +2844,13 @@ def run_conversation(
2805
2844
  except Exception:
2806
2845
  pass
2807
2846
  if _genuine_nous_rate_limit:
2808
- # Skip straight to max_retries -- the
2809
- # top-of-loop guard will handle fallback or
2810
- # bail cleanly.
2811
- retry_count = max_retries
2847
+ # Re-enter the loop exactly once so the
2848
+ # top-of-loop Nous guard handles fallback or
2849
+ # bails cleanly. (Setting retry_count to
2850
+ # max_retries would make the while condition
2851
+ # false immediately and the guard would never
2852
+ # run -- no fallback, generic exhaustion error.)
2853
+ retry_count = max(0, max_retries - 1)
2812
2854
  continue
2813
2855
  # Upstream capacity 429: fall through to normal
2814
2856
  # retry logic. A different model (or the same
@@ -2884,7 +2926,7 @@ def run_conversation(
2884
2926
  if len(messages) < original_len:
2885
2927
  agent._buffer_status(f"🗜️ Compressed {original_len} → {len(messages)} messages, retrying...")
2886
2928
  time.sleep(2) # Brief pause between compression retries
2887
- restart_with_compressed_messages = True
2929
+ _retry.restart_with_compressed_messages = True
2888
2930
  break
2889
2931
  else:
2890
2932
  # Terminal — surface buffered context so the user
@@ -2956,7 +2998,7 @@ def run_conversation(
2956
2998
  "failed": True,
2957
2999
  "compression_exhausted": True,
2958
3000
  }
2959
- restart_with_compressed_messages = True
3001
+ _retry.restart_with_compressed_messages = True
2960
3002
  break
2961
3003
 
2962
3004
  # Error is about the INPUT being too large. Only reduce
@@ -3041,7 +3083,7 @@ def run_conversation(
3041
3083
  if len(messages) < original_len:
3042
3084
  agent._buffer_status(f"🗜️ Compressed {original_len} → {len(messages)} messages, retrying...")
3043
3085
  time.sleep(2) # Brief pause between compression retries
3044
- restart_with_compressed_messages = True
3086
+ _retry.restart_with_compressed_messages = True
3045
3087
  break
3046
3088
  else:
3047
3089
  # Can't compress further and already at minimum tier
@@ -3146,7 +3188,7 @@ def run_conversation(
3146
3188
  if agent._try_activate_fallback():
3147
3189
  retry_count = 0
3148
3190
  compression_attempts = 0
3149
- primary_recovery_attempted = False
3191
+ _retry.primary_recovery_attempted = False
3150
3192
  continue
3151
3193
  if api_kwargs is not None:
3152
3194
  agent._dump_api_request_debug(
@@ -3155,15 +3197,22 @@ def run_conversation(
3155
3197
  # Terminal — flush buffered context so the user sees
3156
3198
  # what was tried before the abort.
3157
3199
  agent._flush_status_buffer()
3200
+ # Summarize once: Cloudflare/proxy HTML challenge pages and
3201
+ # other raw provider bodies must be collapsed to a short
3202
+ # one-liner here, otherwise the full page leaks into the
3203
+ # returned ``error`` field and downstream consumers deliver
3204
+ # it verbatim (e.g. a cron failure notification dumped a
3205
+ # ~60KB Cloudflare challenge page as 31 Discord messages).
3206
+ _nonretryable_summary = agent._summarize_api_error(api_error)
3158
3207
  if classified.reason == FailoverReason.content_policy_blocked:
3159
3208
  agent._emit_status(
3160
3209
  f"❌ Provider safety filter blocked this request: "
3161
- f"{agent._summarize_api_error(api_error)}"
3210
+ f"{_nonretryable_summary}"
3162
3211
  )
3163
3212
  else:
3164
3213
  agent._emit_status(
3165
3214
  f"❌ Non-retryable error (HTTP {status_code}): "
3166
- f"{agent._summarize_api_error(api_error)}"
3215
+ f"{_nonretryable_summary}"
3167
3216
  )
3168
3217
  agent._vprint(f"{agent.log_prefix}❌ Non-retryable client error (HTTP {status_code}). Aborting.", force=True)
3169
3218
  agent._vprint(f"{agent.log_prefix} 🔌 Provider: {_provider} Model: {_model}", force=True)
@@ -3195,7 +3244,7 @@ def run_conversation(
3195
3244
  else: # nous
3196
3245
  agent._vprint(f"{agent.log_prefix} 💡 Nous Portal OAuth token was rejected (HTTP 401). Your token may be", force=True)
3197
3246
  agent._vprint(f"{agent.log_prefix} expired, revoked, or your account may be out of credits. To fix:", force=True)
3198
- agent._vprint(f"{agent.log_prefix} 1. Re-authenticate: hermes auth add nous --type oauth", force=True)
3247
+ agent._vprint(f"{agent.log_prefix} 1. Re-authenticate: hermes portal", force=True)
3199
3248
  agent._vprint(f"{agent.log_prefix} 2. Check your portal account: https://portal.nousresearch.com", force=True)
3200
3249
  # ``:free`` is OpenRouter slug syntax; Nous Portal will reject
3201
3250
  # the model name even after a successful re-auth.
@@ -3248,29 +3297,25 @@ def run_conversation(
3248
3297
  else:
3249
3298
  agent._persist_session(messages, conversation_history)
3250
3299
  if classified.reason == FailoverReason.content_policy_blocked:
3251
- _summary = agent._summarize_api_error(api_error)
3252
3300
  _policy_response = (
3253
- f"⚠️ The model provider's safety filter blocked this request "
3254
- f"(not a Hermes/gateway failure).\n\n"
3255
- f"Provider message: {_summary}\n\n"
3256
- f"Try rephrasing the request, narrowing the context, or "
3257
- f"adding a fallback provider with `hermes fallback add`."
3301
+ "⚠️ The model provider's safety filter blocked this request "
3302
+ "(not a Hermes/gateway failure).\n\n"
3303
+ f"Provider message: {_nonretryable_summary}\n\n"
3304
+ f"{_CONTENT_POLICY_RECOVERY_HINT}"
3305
+ )
3306
+ return _content_policy_blocked_result(
3307
+ messages,
3308
+ api_call_count,
3309
+ final_response=_policy_response,
3310
+ error_detail=_nonretryable_summary,
3258
3311
  )
3259
- return {
3260
- "final_response": _policy_response,
3261
- "messages": messages,
3262
- "api_calls": api_call_count,
3263
- "completed": False,
3264
- "failed": True,
3265
- "error": f"content_policy_blocked: {_summary}",
3266
- }
3267
3312
  return {
3268
3313
  "final_response": None,
3269
3314
  "messages": messages,
3270
3315
  "api_calls": api_call_count,
3271
3316
  "completed": False,
3272
3317
  "failed": True,
3273
- "error": str(api_error),
3318
+ "error": _nonretryable_summary,
3274
3319
  }
3275
3320
 
3276
3321
  if retry_count >= max_retries:
@@ -3278,10 +3323,10 @@ def run_conversation(
3278
3323
  # client once for transient transport errors (stale
3279
3324
  # connection pool, TCP reset). Only attempted once
3280
3325
  # per API call block.
3281
- if not primary_recovery_attempted and agent._try_recover_primary_transport(
3326
+ if not _retry.primary_recovery_attempted and agent._try_recover_primary_transport(
3282
3327
  api_error, retry_count=retry_count, max_retries=max_retries,
3283
3328
  ):
3284
- primary_recovery_attempted = True
3329
+ _retry.primary_recovery_attempted = True
3285
3330
  retry_count = 0
3286
3331
  continue
3287
3332
  # Try fallback before giving up entirely
@@ -3290,7 +3335,7 @@ def run_conversation(
3290
3335
  if agent._try_activate_fallback():
3291
3336
  retry_count = 0
3292
3337
  compression_attempts = 0
3293
- primary_recovery_attempted = False
3338
+ _retry.primary_recovery_attempted = False
3294
3339
  continue
3295
3340
  # Terminal — flush buffered retry/fallback trace.
3296
3341
  agent._flush_status_buffer()
@@ -3378,6 +3423,12 @@ def run_conversation(
3378
3423
  "completed": False,
3379
3424
  "failed": True,
3380
3425
  "error": _final_summary,
3426
+ # Surface the classified reason so callers (notably the
3427
+ # kanban worker path in cli.py) can distinguish a
3428
+ # transient throttle from a real failure and choose a
3429
+ # different exit code. ``rate_limit`` / ``billing`` here
3430
+ # mean "quota wall, not a task error".
3431
+ "failure_reason": classified.reason.value,
3381
3432
  }
3382
3433
 
3383
3434
  # For rate limits, respect the Retry-After header if present
@@ -3435,17 +3486,17 @@ def run_conversation(
3435
3486
  _turn_exit_reason = "interrupted_during_api_call"
3436
3487
  break
3437
3488
 
3438
- if restart_with_compressed_messages:
3489
+ if _retry.restart_with_compressed_messages:
3439
3490
  api_call_count -= 1
3440
3491
  agent.iteration_budget.refund()
3441
3492
  # Count compression restarts toward the retry limit to prevent
3442
3493
  # infinite loops when compression reduces messages but not enough
3443
3494
  # to fit the context window.
3444
3495
  retry_count += 1
3445
- restart_with_compressed_messages = False
3496
+ _retry.restart_with_compressed_messages = False
3446
3497
  continue
3447
3498
 
3448
- if restart_with_length_continuation:
3499
+ if _retry.restart_with_length_continuation:
3449
3500
  # Progressively boost the output token budget on each retry.
3450
3501
  # Retry 1 → 2× base, retry 2 → 3× base, capped at 32 768.
3451
3502
  # Applies to all providers via _ephemeral_max_output_tokens.
@@ -3501,29 +3552,44 @@ def run_conversation(
3501
3552
  assistant_message.content = str(raw)
3502
3553
 
3503
3554
  try:
3504
- from hermes_cli.plugins import invoke_hook as _invoke_hook
3505
- _assistant_tool_calls = getattr(assistant_message, "tool_calls", None) or []
3506
- _assistant_text = assistant_message.content or ""
3507
- _invoke_hook(
3508
- "post_api_request",
3509
- task_id=effective_task_id,
3510
- session_id=agent.session_id or "",
3511
- platform=agent.platform or "",
3512
- model=agent.model,
3513
- provider=agent.provider,
3514
- base_url=agent.base_url,
3515
- api_mode=agent.api_mode,
3516
- api_call_count=api_call_count,
3517
- api_duration=api_duration,
3518
- finish_reason=finish_reason,
3519
- message_count=len(api_messages),
3520
- response_model=getattr(response, "model", None),
3521
- response=response,
3522
- usage=agent._usage_summary_for_api_request_hook(response),
3523
- assistant_message=assistant_message,
3524
- assistant_content_chars=len(_assistant_text),
3525
- assistant_tool_call_count=len(_assistant_tool_calls),
3555
+ from hermes_cli.plugins import (
3556
+ has_hook,
3557
+ invoke_hook as _invoke_hook,
3526
3558
  )
3559
+ if has_hook("post_api_request"):
3560
+ _assistant_tool_calls = (
3561
+ getattr(assistant_message, "tool_calls", None) or []
3562
+ )
3563
+ _assistant_text = assistant_message.content or ""
3564
+ _api_ended_at = api_start_time + api_duration
3565
+ _invoke_hook(
3566
+ "post_api_request",
3567
+ task_id=effective_task_id,
3568
+ turn_id=turn_id,
3569
+ api_request_id=api_request_id,
3570
+ session_id=agent.session_id or "",
3571
+ platform=agent.platform or "",
3572
+ model=agent.model,
3573
+ provider=agent.provider,
3574
+ base_url=agent.base_url,
3575
+ api_mode=agent.api_mode,
3576
+ api_call_count=api_call_count,
3577
+ api_duration=api_duration,
3578
+ started_at=api_start_time,
3579
+ ended_at=_api_ended_at,
3580
+ finish_reason=finish_reason,
3581
+ message_count=len(api_messages),
3582
+ response_model=getattr(response, "model", None),
3583
+ response=agent._api_response_payload_for_hook(
3584
+ response,
3585
+ assistant_message,
3586
+ finish_reason=finish_reason,
3587
+ ),
3588
+ usage=agent._usage_summary_for_api_request_hook(response),
3589
+ assistant_message=assistant_message,
3590
+ assistant_content_chars=len(_assistant_text),
3591
+ assistant_tool_call_count=len(_assistant_tool_calls),
3592
+ )
3527
3593
  except Exception:
3528
3594
  pass
3529
3595
 
@@ -3696,8 +3762,30 @@ def run_conversation(
3696
3762
  assistant_msg = agent._build_assistant_message(assistant_message, finish_reason)
3697
3763
  messages.append(assistant_msg)
3698
3764
  for tc in assistant_message.tool_calls:
3699
- if tc.function.name not in agent.valid_tool_names:
3700
- content = f"Tool '{tc.function.name}' does not exist. Available tools: {available}"
3765
+ _tc_name = tc.function.name
3766
+ if _tc_name not in agent.valid_tool_names:
3767
+ # A blank/whitespace-only name is not a typo the
3768
+ # model can fuzzy-correct toward a real tool — it is
3769
+ # almost always a weak open model echoing tool-call
3770
+ # XML/JSON it saw in file or tool output (#47967:
3771
+ # <tool_call>/<invoke name=...> payloads in a file
3772
+ # prime mimo/nemotron-class models to emit empty
3773
+ # structured calls). Dumping the full tool catalog
3774
+ # in that case feeds the priming loop more names to
3775
+ # mimic and inflates context 3-4x across retries, so
3776
+ # send a terse error that tells the model in-context
3777
+ # tool-call syntax is DATA, not a call to make.
3778
+ if not (_tc_name or "").strip():
3779
+ content = (
3780
+ "Tool call rejected: the tool name was empty. "
3781
+ "If tool-call XML or JSON appeared in file "
3782
+ "contents or tool output, that is data — do "
3783
+ "not re-emit it as a tool call. To call a "
3784
+ "tool, use a valid name from your tool list; "
3785
+ "otherwise reply in plain text."
3786
+ )
3787
+ else:
3788
+ content = f"Tool '{_tc_name}' does not exist. Available tools: {available}"
3701
3789
  else:
3702
3790
  content = "Skipped: another tool call in this turn used an invalid name. Please retry this tool call."
3703
3791
  messages.append({
@@ -4373,379 +4461,26 @@ def run_conversation(
4373
4461
  messages.append({"role": "assistant", "content": final_response})
4374
4462
  break
4375
4463
 
4376
- if final_response is None and (
4377
- api_call_count >= agent.max_iterations
4378
- or agent.iteration_budget.remaining <= 0
4379
- ):
4380
- # Budget exhausted — ask the model for a summary via one extra
4381
- # API call with tools stripped. _handle_max_iterations injects a
4382
- # user message and makes a single toolless request.
4383
- _turn_exit_reason = f"max_iterations_reached({api_call_count}/{agent.max_iterations})"
4384
- agent._emit_status(
4385
- f"⚠️ Iteration budget exhausted ({api_call_count}/{agent.max_iterations}) "
4386
- "— asking model to summarise"
4387
- )
4388
- if not agent.quiet_mode:
4389
- agent._safe_print(
4390
- f"\n⚠️ Iteration budget exhausted ({api_call_count}/{agent.max_iterations}) "
4391
- "— requesting summary..."
4392
- )
4393
- final_response = agent._handle_max_iterations(messages, api_call_count)
4394
-
4395
- # If running as a kanban worker, signal the dispatcher that the
4396
- # worker could not complete (rather than treating it as a
4397
- # protocol violation). The agent loop strips tools before calling
4398
- # _handle_max_iterations, so the model cannot call kanban_block
4399
- # itself — we must do it on its behalf.
4400
- #
4401
- # We route through ``_record_task_failure(outcome="timed_out")``
4402
- # rather than ``kanban_block`` so this counts toward the
4403
- # ``consecutive_failures`` counter and the dispatcher's
4404
- # ``failure_limit`` circuit breaker (#29747 gap 2). Without this,
4405
- # a task whose worker keeps exhausting its budget would block
4406
- # silently each run, get auto-promoted by the operator (or never
4407
- # surface), and re-block in an endless loop with no signal.
4408
- _kanban_task = os.environ.get("HERMES_KANBAN_TASK")
4409
- if _kanban_task:
4410
- try:
4411
- from hermes_cli import kanban_db as _kb
4412
- _conn = _kb.connect()
4413
- try:
4414
- _kb._record_task_failure(
4415
- _conn,
4416
- _kanban_task,
4417
- error=(
4418
- f"Iteration budget exhausted "
4419
- f"({api_call_count}/{agent.max_iterations}) — "
4420
- "task could not complete within the allowed "
4421
- "iterations"
4422
- ),
4423
- outcome="timed_out",
4424
- release_claim=True,
4425
- end_run=True,
4426
- event_payload_extra={
4427
- "budget_used": api_call_count,
4428
- "budget_max": agent.max_iterations,
4429
- },
4430
- )
4431
- logger.info(
4432
- "recorded budget-exhausted failure for task %s (%d/%d)",
4433
- _kanban_task, api_call_count, agent.max_iterations,
4434
- )
4435
- finally:
4436
- try:
4437
- _conn.close()
4438
- except Exception:
4439
- pass
4440
- except Exception:
4441
- logger.warning(
4442
- "Failed to record budget-exhausted failure for task %s",
4443
- _kanban_task,
4444
- exc_info=True,
4445
- )
4446
-
4447
- # Determine if conversation completed successfully
4448
- completed = (
4449
- final_response is not None
4450
- and api_call_count < agent.max_iterations
4451
- and not failed
4452
- )
4453
-
4454
- # Save trajectory if enabled. ``user_message`` may be a multimodal
4455
- # list of parts; the trajectory format wants a plain string.
4456
- agent._save_trajectory(messages, _summarize_user_message_for_log(user_message), completed)
4457
-
4458
- # Clean up VM and browser for this task after conversation completes
4459
- agent._cleanup_task_resources(effective_task_id)
4460
-
4461
- # Persist session to both JSON log and SQLite only after private retry
4462
- # scaffolding has been removed. Otherwise a later user "continue" turn
4463
- # can replay assistant("(empty)") / recovery nudges and fall into the
4464
- # same empty-response loop again.
4465
- agent._drop_trailing_empty_response_scaffolding(messages)
4466
- agent._persist_session(messages, conversation_history)
4467
-
4468
- # ── Turn-exit diagnostic log ─────────────────────────────────────
4469
- # Always logged at INFO so agent.log captures WHY every turn ended.
4470
- # When the last message is a tool result (agent was mid-work), log
4471
- # at WARNING — this is the "just stops" scenario users report.
4472
- _last_msg_role = messages[-1].get("role") if messages else None
4473
- _last_tool_name = None
4474
- if _last_msg_role == "tool":
4475
- # Walk back to find the assistant message with the tool call
4476
- for _m in reversed(messages):
4477
- if _m.get("role") == "assistant" and _m.get("tool_calls"):
4478
- _tcs = _m["tool_calls"]
4479
- if _tcs and isinstance(_tcs[0], dict):
4480
- _last_tool_name = _tcs[-1].get("function", {}).get("name")
4481
- break
4482
-
4483
- _turn_tool_count = sum(
4484
- 1 for m in messages
4485
- if isinstance(m, dict) and m.get("role") == "assistant" and m.get("tool_calls")
4486
- )
4487
- _resp_len = len(final_response) if final_response else 0
4488
- _budget_used = agent.iteration_budget.used if agent.iteration_budget else 0
4489
- _budget_max = agent.iteration_budget.max_total if agent.iteration_budget else 0
4490
-
4491
- _diag_msg = (
4492
- "Turn ended: reason=%s model=%s api_calls=%d/%d budget=%d/%d "
4493
- "tool_turns=%d last_msg_role=%s response_len=%d session=%s"
4494
- )
4495
- _diag_args = (
4496
- _turn_exit_reason, agent.model, api_call_count, agent.max_iterations,
4497
- _budget_used, _budget_max,
4498
- _turn_tool_count, _last_msg_role, _resp_len,
4499
- agent.session_id or "none",
4500
- )
4501
-
4502
- if _last_msg_role == "tool" and not interrupted:
4503
- # Agent was mid-work — this is the "just stops" case.
4504
- logger.warning(
4505
- "Turn ended with pending tool result (agent may appear stuck). "
4506
- + _diag_msg + " last_tool=%s",
4507
- *_diag_args, _last_tool_name,
4508
- )
4509
- else:
4510
- logger.info(_diag_msg, *_diag_args)
4511
-
4512
- # File-mutation verifier footer.
4513
- # If one or more ``write_file`` / ``patch`` calls failed during this
4514
- # turn and were never superseded by a successful write to the same
4515
- # path, append an advisory footer to the assistant response. This
4516
- # catches the specific case — reported by Ben Eng (#15524-adjacent)
4517
- # — where a model issues a batch of parallel patches, half of them
4518
- # fail with "Could not find old_string", and the model summarises
4519
- # the turn claiming every file was edited. The user then has to
4520
- # manually run ``git status`` to catch the lie. With this footer
4521
- # the truth is surfaced on every turn, so over-claiming is
4522
- # structurally impossible past the model.
4523
- #
4524
- # Gate: only applied when a real text response exists for this
4525
- # turn and the user didn't interrupt. Empty/interrupted turns
4526
- # already have other surface text that shouldn't be augmented.
4527
- if final_response and not interrupted:
4528
- try:
4529
- _failed = getattr(agent, "_turn_failed_file_mutations", None) or {}
4530
- if _failed and agent._file_mutation_verifier_enabled():
4531
- footer = agent._format_file_mutation_failure_footer(_failed)
4532
- if footer:
4533
- final_response = final_response.rstrip() + "\n\n" + footer
4534
- except Exception as _ver_err:
4535
- logger.debug("file-mutation verifier footer failed: %s", _ver_err)
4536
-
4537
- # Turn-completion explainer.
4538
- # When a turn ends abnormally after substantive work — empty content
4539
- # after retries, a partial/truncated stream, a still-pending tool
4540
- # result, or an iteration/budget limit — the user otherwise gets a
4541
- # blank or fragmentary response box with no consolidated reason why
4542
- # the agent stopped (#34452). Surface a single user-visible
4543
- # explanation derived from ``_turn_exit_reason``, mirroring the
4544
- # file-mutation verifier footer pattern above.
4545
- #
4546
- # Gate carefully so healthy turns stay quiet:
4547
- # - ``text_response(...)`` exits never produce an explanation
4548
- # (handled inside the formatter), so a terse ``Done.`` is silent.
4549
- # - We only ACT when there is no genuinely usable reply this turn:
4550
- # an empty response, the "(empty)" terminal sentinel, or a
4551
- # suspiciously short partial fragment with no terminating
4552
- # punctuation (e.g. "The"). A real short answer keeps its text.
4553
- if not interrupted:
4554
- try:
4555
- if agent._turn_completion_explainer_enabled():
4556
- _stripped = (final_response or "").strip()
4557
- _is_empty_terminal = _stripped == "" or _stripped == "(empty)"
4558
- # A short fragment that is not a normal text_response exit
4559
- # and lacks sentence-ending punctuation is treated as a
4560
- # truncated partial (the "The" case from #34452).
4561
- _is_partial_fragment = (
4562
- not _is_empty_terminal
4563
- and not str(_turn_exit_reason).startswith("text_response")
4564
- and len(_stripped) <= 24
4565
- and _stripped[-1:] not in {".", "!", "?", "。", "!", "?", "`", ")"}
4566
- )
4567
- if _is_empty_terminal or _is_partial_fragment:
4568
- _explanation = agent._format_turn_completion_explanation(
4569
- _turn_exit_reason
4570
- )
4571
- if _explanation:
4572
- if _is_empty_terminal:
4573
- # Replace the bare "(empty)"/blank sentinel with
4574
- # the actionable explanation.
4575
- final_response = _explanation
4576
- else:
4577
- # Keep the partial fragment, append the reason so
4578
- # the user sees both what arrived and why it
4579
- # stopped.
4580
- final_response = (
4581
- _stripped + "\n\n" + _explanation
4582
- )
4583
- except Exception as _exp_err:
4584
- logger.debug("turn-completion explainer failed: %s", _exp_err)
4585
-
4586
- _response_transformed = False
4587
-
4588
- # Plugin hook: transform_llm_output
4589
- # Fired once per turn after the tool-calling loop completes.
4590
- # Plugins can transform the LLM's output text before it's returned.
4591
- # First hook to return a string wins; None/empty return leaves text unchanged.
4592
- if final_response and not interrupted:
4593
- try:
4594
- from hermes_cli.plugins import invoke_hook as _invoke_hook
4595
- _transform_results = _invoke_hook(
4596
- "transform_llm_output",
4597
- response_text=final_response,
4598
- session_id=agent.session_id or "",
4599
- model=agent.model,
4600
- platform=getattr(agent, "platform", None) or "",
4601
- )
4602
- for _hook_result in _transform_results:
4603
- if isinstance(_hook_result, str) and _hook_result:
4604
- final_response = _hook_result
4605
- _response_transformed = True
4606
- break # First non-empty string wins
4607
- except Exception as exc:
4608
- logger.warning("transform_llm_output hook failed: %s", exc)
4609
-
4610
- # Plugin hook: post_llm_call
4611
- # Fired once per turn after the tool-calling loop completes.
4612
- # Plugins can use this to persist conversation data (e.g. sync
4613
- # to an external memory system).
4614
- if final_response and not interrupted:
4615
- try:
4616
- from hermes_cli.plugins import invoke_hook as _invoke_hook
4617
- _invoke_hook(
4618
- "post_llm_call",
4619
- session_id=agent.session_id,
4620
- user_message=original_user_message,
4621
- assistant_response=final_response,
4622
- conversation_history=list(messages),
4623
- model=agent.model,
4624
- platform=getattr(agent, "platform", None) or "",
4625
- )
4626
- except Exception as exc:
4627
- logger.warning("post_llm_call hook failed: %s", exc)
4628
-
4629
- # Extract reasoning from the CURRENT turn only. Walk backwards
4630
- # but stop at the user message that started this turn — anything
4631
- # earlier is from a prior turn and must not leak into the reasoning
4632
- # box (confusing stale display; #17055). Within the current turn
4633
- # we still want the *most recent* non-empty reasoning: many
4634
- # providers (Claude thinking, DeepSeek v4, Codex Responses) emit
4635
- # reasoning on the tool-call step and leave the final-answer step
4636
- # with reasoning=None, so picking only the last assistant would
4637
- # silently drop legitimate same-turn reasoning.
4638
- last_reasoning = None
4639
- for msg in reversed(messages):
4640
- if msg.get("role") == "user":
4641
- break # turn boundary — don't cross into prior turns
4642
- if msg.get("role") == "assistant" and msg.get("reasoning"):
4643
- last_reasoning = msg["reasoning"]
4644
- break
4645
-
4646
- # Build result with interrupt info if applicable
4647
- result = {
4648
- "final_response": final_response,
4649
- "last_reasoning": last_reasoning,
4650
- "messages": messages,
4651
- "api_calls": api_call_count,
4652
- "completed": completed,
4653
- "turn_exit_reason": _turn_exit_reason,
4654
- "failed": failed,
4655
- "partial": False, # True only when stopped due to invalid tool calls
4656
- "interrupted": interrupted,
4657
- "response_transformed": _response_transformed,
4658
- "response_previewed": getattr(agent, "_response_was_previewed", False),
4659
- "model": agent.model,
4660
- "provider": agent.provider,
4661
- "base_url": agent.base_url,
4662
- "input_tokens": agent.session_input_tokens,
4663
- "output_tokens": agent.session_output_tokens,
4664
- "cache_read_tokens": agent.session_cache_read_tokens,
4665
- "cache_write_tokens": agent.session_cache_write_tokens,
4666
- "reasoning_tokens": agent.session_reasoning_tokens,
4667
- "prompt_tokens": agent.session_prompt_tokens,
4668
- "completion_tokens": agent.session_completion_tokens,
4669
- "total_tokens": agent.session_total_tokens,
4670
- "last_prompt_tokens": getattr(agent.context_compressor, "last_prompt_tokens", 0) or 0,
4671
- "estimated_cost_usd": agent.session_estimated_cost_usd,
4672
- "cost_status": agent.session_cost_status,
4673
- "cost_source": agent.session_cost_source,
4674
- "session_id": agent.session_id,
4675
- }
4676
- if agent._tool_guardrail_halt_decision is not None:
4677
- result["guardrail"] = agent._tool_guardrail_halt_decision.to_metadata()
4678
- # If a /steer landed after the final assistant turn (no more tool
4679
- # batches to drain into), hand it back to the caller so it can be
4680
- # delivered as the next user turn instead of being silently lost.
4681
- _leftover_steer = agent._drain_pending_steer()
4682
- if _leftover_steer:
4683
- result["pending_steer"] = _leftover_steer
4684
- agent._response_was_previewed = False
4685
-
4686
- # Include interrupt message if one triggered the interrupt
4687
- if interrupted and agent._interrupt_message:
4688
- result["interrupt_message"] = agent._interrupt_message
4689
-
4690
- # Clear interrupt state after handling
4691
- agent.clear_interrupt()
4692
-
4693
- # Clear stream callback so it doesn't leak into future calls
4694
- agent._stream_callback = None
4695
-
4696
- # Check skill trigger NOW — based on how many tool iterations THIS turn used.
4697
- _should_review_skills = False
4698
- if (agent._skill_nudge_interval > 0
4699
- and agent._iters_since_skill >= agent._skill_nudge_interval
4700
- and "skill_manage" in agent.valid_tool_names):
4701
- _should_review_skills = True
4702
- agent._iters_since_skill = 0
4703
-
4704
- # External memory provider: sync the completed turn + queue next prefetch.
4705
- agent._sync_external_memory_for_turn(
4706
- original_user_message=original_user_message,
4464
+ # Post-loop turn finalization extracted to agent/turn_finalizer.finalize_turn
4465
+ # (god-file decomposition Phase 1 step 4). Behavior-neutral: the assembled
4466
+ # result dict is returned exactly as before.
4467
+ from agent.turn_finalizer import finalize_turn
4468
+ return finalize_turn(
4469
+ agent,
4707
4470
  final_response=final_response,
4471
+ api_call_count=api_call_count,
4708
4472
  interrupted=interrupted,
4473
+ failed=failed,
4709
4474
  messages=messages,
4475
+ conversation_history=conversation_history,
4476
+ effective_task_id=effective_task_id,
4477
+ turn_id=turn_id,
4478
+ user_message=user_message,
4479
+ original_user_message=original_user_message,
4480
+ _should_review_memory=_should_review_memory,
4481
+ _turn_exit_reason=_turn_exit_reason,
4710
4482
  )
4711
4483
 
4712
- # Background memory/skill review — runs AFTER the response is delivered
4713
- # so it never competes with the user's task for model attention.
4714
- if final_response and not interrupted and (_should_review_memory or _should_review_skills):
4715
- try:
4716
- agent._spawn_background_review(
4717
- messages_snapshot=list(messages),
4718
- review_memory=_should_review_memory,
4719
- review_skills=_should_review_skills,
4720
- )
4721
- except Exception:
4722
- pass # Background review is best-effort
4723
-
4724
- # Note: Memory provider on_session_end() + shutdown_all() are NOT
4725
- # called here — run_conversation() is called once per user message in
4726
- # multi-turn sessions. Shutting down after every turn would kill the
4727
- # provider before the second message. Actual session-end cleanup is
4728
- # handled by the CLI (atexit / /reset) and gateway (session expiry /
4729
- # _reset_session).
4730
-
4731
- # Plugin hook: on_session_end
4732
- # Fired at the very end of every run_conversation call.
4733
- # Plugins can use this for cleanup, flushing buffers, etc.
4734
- try:
4735
- from hermes_cli.plugins import invoke_hook as _invoke_hook
4736
- _invoke_hook(
4737
- "on_session_end",
4738
- session_id=agent.session_id,
4739
- completed=completed,
4740
- interrupted=interrupted,
4741
- model=agent.model,
4742
- platform=getattr(agent, "platform", None) or "",
4743
- )
4744
- except Exception as exc:
4745
- logger.warning("on_session_end hook failed: %s", exc)
4746
-
4747
- return result
4748
-
4749
4484
 
4750
4485
 
4751
4486
  __all__ = ["run_conversation"]