@clawpump/claw-agent 0.1.5 → 0.1.6

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 +6188 -975
  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 +3 -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
@@ -7,12 +7,13 @@ automatic memory extraction, and session management.
7
7
  Original PR #3369 by Mibayy, rewritten to use the full OpenViking session
8
8
  lifecycle instead of read-only search endpoints.
9
9
 
10
- Config via environment variables (profile-scoped via each profile's .env):
10
+ Config via environment variables (profile-scoped via each profile's .env)
11
+ or a linked OpenViking CLI config:
11
12
  OPENVIKING_ENDPOINT — Server URL (default: http://127.0.0.1:1933)
12
13
  OPENVIKING_API_KEY — API key (required for authenticated servers)
13
- OPENVIKING_ACCOUNT — Tenant account (default: default)
14
- OPENVIKING_USER — Tenant user (default: default)
15
- OPENVIKING_AGENT Tenant agent (default: hermes)
14
+ OPENVIKING_ACCOUNT — Tenant account for local/trusted mode (default: default)
15
+ OPENVIKING_USER — Tenant user for local/trusted mode (default: default)
16
+ OPENVIKING_AGENT Hermes peer ID in OpenViking (default: hermes)
16
17
 
17
18
  Capabilities:
18
19
  - Automatic memory extraction on session commit (6 categories)
@@ -29,23 +30,48 @@ import json
29
30
  import logging
30
31
  import mimetypes
31
32
  import os
33
+ import re
34
+ import shutil
35
+ import stat
36
+ import subprocess
32
37
  import tempfile
33
38
  import threading
39
+ import time
34
40
  import uuid
35
41
  import zipfile
42
+ from dataclasses import dataclass, replace
36
43
  from pathlib import Path
37
- from typing import Any, Dict, List, Optional
44
+ from typing import Any, Callable, Dict, List, Optional, Set
38
45
  from urllib.parse import urlparse
39
46
  from urllib.request import url2pathname
40
47
 
48
+ from agent.message_content import flatten_message_text
41
49
  from agent.memory_provider import MemoryProvider
50
+ from agent.skill_commands import extract_user_instruction_from_skill_message
42
51
  from tools.registry import tool_error
52
+ from utils import atomic_json_write, env_var_enabled
43
53
 
44
54
  logger = logging.getLogger(__name__)
45
55
 
46
56
  _DEFAULT_ENDPOINT = "http://127.0.0.1:1933"
57
+ _OPENVIKING_SERVICE_ENDPOINT = "https://api.vikingdb.cn-beijing.volces.com/openviking"
58
+ _DEFAULT_AGENT = "hermes"
59
+ _AGENT_PROMPT_LABEL = "Hermes peer ID in OpenViking"
60
+ _OVCLI_CONFIG_ENV = "OPENVIKING_CLI_CONFIG_FILE"
61
+ _OVCLI_DEFAULT_RELATIVE_PATH = ".openviking/ovcli.conf"
62
+ _OVCLI_SAVED_PREFIX = "ovcli.conf."
63
+ _OPENVIKING_ENV_KEYS = (
64
+ "OPENVIKING_ENDPOINT",
65
+ "OPENVIKING_API_KEY",
66
+ "OPENVIKING_ACCOUNT",
67
+ "OPENVIKING_USER",
68
+ "OPENVIKING_AGENT",
69
+ )
47
70
  _TIMEOUT = 30.0
71
+ _SESSION_DRAIN_TIMEOUT = 10.0
72
+ _DEFERRED_COMMIT_TIMEOUT = (_TIMEOUT * 2) + 5.0
48
73
  _REMOTE_RESOURCE_PREFIXES = ("http://", "https://", "git@", "ssh://", "git://")
74
+ _SYNC_TRACE_ENV = "HERMES_OPENVIKING_SYNC_TRACE"
49
75
 
50
76
  # Maps the viking_remember `category` enum to a viking:// subdirectory.
51
77
  # Keep in sync with REMEMBER_SCHEMA.parameters.properties.category.enum.
@@ -65,6 +91,83 @@ _MEMORY_WRITE_TARGET_SUBDIR_MAP = {
65
91
  "user": "preferences",
66
92
  "memory": "patterns",
67
93
  }
94
+ _LOCAL_OPENVIKING_HOSTS = {"localhost", "127.0.0.1", "::1"}
95
+ _LOCAL_OPENVIKING_AUTOSTART_TIMEOUT = 60.0
96
+ _OPENVIKING_SERVER_LOG_RELATIVE_PATH = Path("logs") / "openviking-server.log"
97
+ _OPENVIKING_RESPONDED_FAILURE_PREFIX = "OpenViking server responded"
98
+ _SETUP_CANCELLED = object()
99
+
100
+
101
+ @dataclass(frozen=True)
102
+ class _OvcliProfile:
103
+ source: str
104
+ name: str
105
+ path: Path
106
+ data: dict
107
+ values: dict
108
+ is_active: bool = False
109
+
110
+
111
+ class _OpenVikingHTTPError(RuntimeError):
112
+ def __init__(self, message: str, status_code: Optional[int] = None):
113
+ super().__init__(message)
114
+ self.status_code = status_code
115
+
116
+
117
+ def _sanitize_openviking_error_message(message: str, status_code: Optional[int] = None) -> str:
118
+ text = (message or "").strip()
119
+ status = f"HTTP {status_code}" if status_code else "HTTP error"
120
+ looks_like_html = bool(re.search(r"^\s*<(!doctype|html|head|body)\b", text, flags=re.IGNORECASE))
121
+ if looks_like_html:
122
+ title_match = re.search(r"<title[^>]*>(.*?)</title>", text, flags=re.IGNORECASE | re.DOTALL)
123
+ if title_match:
124
+ title = re.sub(r"\s+", " ", title_match.group(1)).strip()
125
+ if "|" in title:
126
+ title = title.split("|", 1)[1].strip()
127
+ if status_code and title.startswith(f"{status_code}:"):
128
+ title = title.split(":", 1)[1].strip()
129
+ if title:
130
+ return f"{status}: {title}"
131
+ return f"{status}: OpenViking endpoint returned an HTML error page."
132
+
133
+ if len(text) > 300:
134
+ return text[:297].rstrip() + "..."
135
+ return text or status
136
+
137
+
138
+ def _format_openviking_exception(error: Exception) -> str:
139
+ status_code = None
140
+ if isinstance(error, _OpenVikingHTTPError):
141
+ status_code = error.status_code
142
+ else:
143
+ response = getattr(error, "response", None)
144
+ status_code = getattr(response, "status_code", None)
145
+ return _sanitize_openviking_error_message(str(error), status_code)
146
+
147
+
148
+ def _derive_openviking_user_text(content: Any) -> str:
149
+ """Strip Hermes slash-skill scaffolding before sending content to OpenViking.
150
+
151
+ Defense-in-depth: MemoryManager already strips skill scaffolding for the
152
+ whole provider fan-out (see ``MemoryManager._strip_skill_scaffolding``), so
153
+ in normal operation this receives already-clean text and passes it through
154
+ unchanged. It stays here so OpenViking is correct if its hooks are ever
155
+ invoked outside the manager. Delegates to the canonical extractor in
156
+ ``agent.skill_commands`` — no duplicated marker literals, no drift risk.
157
+ """
158
+ return extract_user_instruction_from_skill_message(content) or ""
159
+
160
+
161
+ def _sync_trace_enabled() -> bool:
162
+ return env_var_enabled(_SYNC_TRACE_ENV)
163
+
164
+
165
+ def _preview(value: Any, limit: int = 160) -> str:
166
+ text = "" if value is None else str(value)
167
+ text = text.replace("\n", "\\n")
168
+ if len(text) > limit:
169
+ return text[:limit] + "..."
170
+ return text
68
171
 
69
172
 
70
173
  # ---------------------------------------------------------------------------
@@ -108,31 +211,32 @@ class _VikingClient:
108
211
  """Thin HTTP client for the OpenViking REST API."""
109
212
 
110
213
  def __init__(self, endpoint: str, api_key: str = "",
111
- account: str = "", user: str = "", agent: str = ""):
214
+ account: Optional[str] = None, user: Optional[str] = None,
215
+ agent: Optional[str] = None):
112
216
  self._endpoint = endpoint.rstrip("/")
113
217
  self._api_key = api_key
218
+ # Account/user are local/trusted-mode tenant identity. API-key requests
219
+ # omit these headers by default; trusted-mode retry may send them only
220
+ # after OpenViking explicitly asks for asserted tenant identity.
114
221
  self._account = account or os.environ.get("OPENVIKING_ACCOUNT", "default")
115
222
  self._user = user or os.environ.get("OPENVIKING_USER", "default")
116
- self._agent = agent or os.environ.get("OPENVIKING_AGENT", "hermes")
223
+ self._agent = agent if agent is not None else os.environ.get("OPENVIKING_AGENT", _DEFAULT_AGENT)
117
224
  self._httpx = _get_httpx()
118
225
  if self._httpx is None:
119
226
  raise ImportError("httpx is required for OpenViking: pip install httpx")
120
227
 
121
- def _headers(self) -> dict:
122
- # Always send tenant headers when account/user are configured.
123
- # OpenViking 0.3.x requires X-OpenViking-Account and X-OpenViking-User
124
- # for ROOT API key requests to tenant-scoped APIs — omitting them
125
- # causes INVALID_ARGUMENT errors even when account="default".
126
- # User-level keys can omit them (server derives tenancy from the key),
127
- # but ROOT keys must always include them explicitly.
128
- h = {
129
- "Content-Type": "application/json",
130
- "X-OpenViking-Agent": self._agent,
131
- }
132
- if self._account:
133
- h["X-OpenViking-Account"] = self._account
134
- if self._user:
135
- h["X-OpenViking-User"] = self._user
228
+ def _headers(self, *, include_tenant: bool | None = None) -> dict:
229
+ if include_tenant is None:
230
+ include_tenant = not bool(self._api_key)
231
+
232
+ h = {"Content-Type": "application/json"}
233
+ if self._agent:
234
+ h["X-OpenViking-Actor-Peer"] = self._agent
235
+ if include_tenant:
236
+ if self._account:
237
+ h["X-OpenViking-Account"] = self._account
238
+ if self._user:
239
+ h["X-OpenViking-User"] = self._user
136
240
  if self._api_key:
137
241
  h["X-API-Key"] = self._api_key
138
242
  h["Authorization"] = "Bearer " + self._api_key
@@ -141,11 +245,33 @@ class _VikingClient:
141
245
  def _url(self, path: str) -> str:
142
246
  return f"{self._endpoint}{path}"
143
247
 
144
- def _multipart_headers(self) -> dict:
145
- headers = self._headers()
248
+ def _multipart_headers(self, *, include_tenant: bool | None = None) -> dict:
249
+ headers = self._headers(include_tenant=include_tenant)
146
250
  headers.pop("Content-Type", None)
147
251
  return headers
148
252
 
253
+ @staticmethod
254
+ def _needs_trusted_identity_retry(exc: Exception) -> bool:
255
+ message = str(exc)
256
+ return (
257
+ "Trusted mode requests must include X-OpenViking-Account" in message
258
+ or "Trusted mode requests must include X-OpenViking-User" in message
259
+ or "Trusted mode requests must include X-OpenViking-Account or explicit account_id" in message
260
+ )
261
+
262
+ def _send_with_trusted_identity_retry(self, send, *, multipart: bool = False) -> dict:
263
+ try:
264
+ headers = self._multipart_headers() if multipart else self._headers()
265
+ return self._parse_response(send(headers))
266
+ except Exception as exc:
267
+ if not self._api_key or not self._needs_trusted_identity_retry(exc):
268
+ raise
269
+ headers = (
270
+ self._multipart_headers(include_tenant=True)
271
+ if multipart else self._headers(include_tenant=True)
272
+ )
273
+ return self._parse_response(send(headers))
274
+
149
275
  def _parse_response(self, resp) -> dict:
150
276
  try:
151
277
  data = resp.json()
@@ -153,15 +279,19 @@ class _VikingClient:
153
279
  data = None
154
280
 
155
281
  if resp.status_code >= 400:
282
+ message = _sanitize_openviking_error_message(
283
+ getattr(resp, "text", ""),
284
+ resp.status_code,
285
+ )
156
286
  if isinstance(data, dict):
157
287
  error = data.get("error")
158
288
  if isinstance(error, dict):
159
289
  code = error.get("code", "HTTP_ERROR")
160
- message = error.get("message", resp.text)
161
- raise RuntimeError(f"{code}: {message}")
290
+ message = f"{code}: {error.get('message', message)}"
291
+ raise _OpenVikingHTTPError(message, resp.status_code)
162
292
  if data.get("status") == "error":
163
- raise RuntimeError(str(data))
164
- resp.raise_for_status()
293
+ raise _OpenVikingHTTPError(str(data), resp.status_code)
294
+ raise _OpenVikingHTTPError(message or f"HTTP {resp.status_code}", resp.status_code)
165
295
 
166
296
  if isinstance(data, dict) and data.get("status") == "error":
167
297
  error = data.get("error")
@@ -176,28 +306,33 @@ class _VikingClient:
176
306
  return data
177
307
 
178
308
  def get(self, path: str, **kwargs) -> dict:
179
- resp = self._httpx.get(
180
- self._url(path), headers=self._headers(), timeout=_TIMEOUT, **kwargs
309
+ return self._send_with_trusted_identity_retry(
310
+ lambda headers: self._httpx.get(
311
+ self._url(path), headers=headers, timeout=_TIMEOUT, **kwargs
312
+ )
181
313
  )
182
- return self._parse_response(resp)
183
314
 
184
315
  def post(self, path: str, payload: dict = None, **kwargs) -> dict:
185
- resp = self._httpx.post(
186
- self._url(path), json=payload or {}, headers=self._headers(),
187
- timeout=_TIMEOUT, **kwargs
316
+ return self._send_with_trusted_identity_retry(
317
+ lambda headers: self._httpx.post(
318
+ self._url(path), json=payload or {}, headers=headers,
319
+ timeout=_TIMEOUT, **kwargs
320
+ )
188
321
  )
189
- return self._parse_response(resp)
190
322
 
191
323
  def upload_temp_file(self, file_path: Path) -> str:
192
324
  mime_type = mimetypes.guess_type(file_path.name)[0] or "application/octet-stream"
193
- with file_path.open("rb") as f:
194
- resp = self._httpx.post(
195
- self._url("/api/v1/resources/temp_upload"),
196
- files={"file": (file_path.name, f, mime_type)},
197
- headers=self._multipart_headers(),
198
- timeout=_TIMEOUT,
199
- )
200
- data = self._parse_response(resp)
325
+
326
+ def _send(headers):
327
+ with file_path.open("rb") as f:
328
+ return self._httpx.post(
329
+ self._url("/api/v1/resources/temp_upload"),
330
+ files={"file": (file_path.name, f, mime_type)},
331
+ headers=headers,
332
+ timeout=_TIMEOUT,
333
+ )
334
+
335
+ data = self._send_with_trusted_identity_retry(_send, multipart=True)
201
336
  result = data.get("result", {})
202
337
  temp_file_id = result.get("temp_file_id", "")
203
338
  if not temp_file_id:
@@ -213,6 +348,20 @@ class _VikingClient:
213
348
  except Exception:
214
349
  return False
215
350
 
351
+ def health_payload(self) -> dict:
352
+ resp = self._httpx.get(
353
+ self._url("/health"), headers=self._headers(), timeout=3.0
354
+ )
355
+ return self._parse_response(resp)
356
+
357
+ def validate_auth(self) -> dict:
358
+ """Validate authenticated OpenViking access without mutating state."""
359
+ return self.get("/api/v1/system/status")
360
+
361
+ def validate_root_access(self) -> dict:
362
+ """Validate ROOT access against a read-only admin endpoint."""
363
+ return self.get("/api/v1/admin/accounts")
364
+
216
365
 
217
366
  # ---------------------------------------------------------------------------
218
367
  # Tool schemas
@@ -353,6 +502,25 @@ ADD_RESOURCE_SCHEMA = {
353
502
  }
354
503
 
355
504
 
505
+ # Recall tools (read-only) whose results we never re-ingest into OpenViking —
506
+ # echoing recalled memory back into the session transcript would re-store it.
507
+ # Write tools (viking_remember / viking_add_resource) are intentionally NOT
508
+ # here. Derived from the canonical schema names so renames can't desync.
509
+ _OPENVIKING_RECALL_TOOL_NAMES = {
510
+ SEARCH_SCHEMA["name"],
511
+ READ_SCHEMA["name"],
512
+ BROWSE_SCHEMA["name"],
513
+ }
514
+
515
+ # Canonical tool_status values emitted in OpenViking batch tool parts.
516
+ _TOOL_STATUS_COMPLETED = "completed"
517
+ _TOOL_STATUS_ERROR = "error"
518
+ _TOOL_STATUS_PENDING = "pending"
519
+ # Inbound status aliases (from varied tool-result shapes) -> canonical above.
520
+ _TOOL_STATUS_ERROR_ALIASES = {"error", "failed", "failure"}
521
+ _TOOL_STATUS_COMPLETED_ALIASES = {"completed", "complete", "success", "succeeded"}
522
+
523
+
356
524
  def _zip_directory(dir_path: Path) -> Path:
357
525
  """Create a temporary zip file containing a directory tree."""
358
526
  root = dir_path.resolve()
@@ -405,6 +573,1104 @@ def _path_from_file_uri(uri: str) -> Path | str:
405
573
  return Path(url2pathname(parsed.path)).expanduser()
406
574
 
407
575
 
576
+ def _clean_config_value(value: Any) -> str:
577
+ return value.strip() if isinstance(value, str) else ""
578
+
579
+
580
+ def _default_ovcli_config_path() -> Path:
581
+ return Path.home() / _OVCLI_DEFAULT_RELATIVE_PATH
582
+
583
+
584
+ def _resolve_ovcli_config_path(config_path: str = "") -> Path:
585
+ env_path = os.environ.get(_OVCLI_CONFIG_ENV, "").strip()
586
+ if env_path:
587
+ return Path(env_path).expanduser()
588
+ if config_path:
589
+ return Path(config_path).expanduser()
590
+ return _default_ovcli_config_path()
591
+
592
+
593
+ def _ovcli_config_dir() -> Path:
594
+ return _default_ovcli_config_path().parent
595
+
596
+
597
+ def _load_ovcli_config(path: Optional[Path] = None) -> dict:
598
+ config_path = path or _resolve_ovcli_config_path()
599
+ if not config_path.exists():
600
+ return {}
601
+ with config_path.open(encoding="utf-8") as f:
602
+ data = json.load(f)
603
+ if not isinstance(data, dict):
604
+ raise ValueError(f"OpenViking CLI config must be a JSON object: {config_path}")
605
+ return data
606
+
607
+
608
+ def _connection_values_from_ovcli(data: dict) -> dict:
609
+ api_key = _clean_config_value(data.get("api_key")) or _clean_config_value(data.get("root_api_key"))
610
+ root_api_key = _clean_config_value(data.get("root_api_key"))
611
+ send_identity = not api_key or api_key == root_api_key
612
+ account = _clean_config_value(data.get("account") or data.get("account_id"))
613
+ user = _clean_config_value(data.get("user") or data.get("user_id"))
614
+ return {
615
+ "endpoint": _normalize_openviking_url(data.get("url")),
616
+ "api_key": api_key,
617
+ "root_api_key": root_api_key,
618
+ "account": account if send_identity else "",
619
+ "user": user if send_identity else "",
620
+ "agent": _clean_config_value(data.get("actor_peer_id") or data.get("agent_id")),
621
+ }
622
+
623
+
624
+ def _is_valid_ovcli_profile_name(name: str) -> bool:
625
+ if not name or name.strip() != name or name.startswith("."):
626
+ return False
627
+ if "/" in name or "\\" in name:
628
+ return False
629
+ return all(ch.isascii() and (ch.isalnum() or ch in {"-", "_"}) for ch in name)
630
+
631
+
632
+ def _validate_openviking_identity_value(value: str, *, field: str) -> tuple[bool, str, str]:
633
+ label = "Account ID" if field == "account" else "User ID"
634
+ identifier = "account_id" if field == "account" else "user_id"
635
+ trimmed = value.strip()
636
+ if not trimmed:
637
+ return False, f"{label} cannot be empty.", ""
638
+ if trimmed != value:
639
+ return False, f"{label} cannot start or end with whitespace.", ""
640
+ if field == "account" and trimmed.startswith("_"):
641
+ return False, "Account ID cannot start with '_'.", ""
642
+ if not all(ch.isascii() and (ch.isalnum() or ch in {"_", "-", ".", "@"}) for ch in trimmed):
643
+ return False, f"{label} can only contain letters, numbers, '_', '-', '.', and '@'.", ""
644
+ if trimmed.count("@") > 1:
645
+ return False, f"{identifier} must have at most one '@'.", ""
646
+ return True, "", trimmed
647
+
648
+
649
+ def _normalize_openviking_url(url: str) -> str:
650
+ trimmed = _clean_config_value(url).rstrip("/")
651
+ if not trimmed:
652
+ return _DEFAULT_ENDPOINT
653
+ lower = trimmed.lower()
654
+ if lower in {"::1", "[::1]"}:
655
+ return "http://[::1]:1933"
656
+ if lower.startswith("[::1]:"):
657
+ return f"http://[::1]:{trimmed.rsplit(':', 1)[1]}"
658
+ if lower.startswith("::1:"):
659
+ return f"http://[::1]:{trimmed.rsplit(':', 1)[1]}"
660
+ if "://" in trimmed:
661
+ return trimmed
662
+ host, _sep, port = trimmed.partition(":")
663
+ if host.lower() in {"localhost", "127.0.0.1"}:
664
+ return f"http://{host}:{port or '1933'}"
665
+ return trimmed
666
+
667
+
668
+ def _load_profile(path: Path, *, source: str, name: str) -> Optional[_OvcliProfile]:
669
+ try:
670
+ data = _load_ovcli_config(path)
671
+ except Exception as e:
672
+ logger.debug("Skipping invalid OpenViking CLI config %s: %s", path, e)
673
+ return None
674
+ return _OvcliProfile(
675
+ source=source,
676
+ name=name,
677
+ path=path,
678
+ data=data,
679
+ values=_connection_values_from_ovcli(data),
680
+ )
681
+
682
+
683
+ def _profile_identity(path: Path) -> str:
684
+ try:
685
+ return str(path.expanduser().resolve())
686
+ except OSError:
687
+ return str(path.expanduser())
688
+
689
+
690
+ def _profiles_equivalent(left: _OvcliProfile, right: _OvcliProfile) -> bool:
691
+ return left.values == right.values
692
+
693
+
694
+ def _discover_ovcli_profiles() -> list[_OvcliProfile]:
695
+ profiles: list[_OvcliProfile] = []
696
+ seen_paths: set[str] = set()
697
+
698
+ def add(path: Path, *, source: str, name: str) -> None:
699
+ if not path.exists() or not path.is_file():
700
+ return
701
+ identity = _profile_identity(path)
702
+ if identity in seen_paths:
703
+ return
704
+ profile = _load_profile(path, source=source, name=name)
705
+ if profile is None:
706
+ return
707
+ seen_paths.add(identity)
708
+ profiles.append(profile)
709
+
710
+ env_path = os.environ.get(_OVCLI_CONFIG_ENV, "").strip()
711
+ if env_path:
712
+ add(Path(env_path).expanduser(), source="env", name=_OVCLI_CONFIG_ENV)
713
+
714
+ active_path = _default_ovcli_config_path()
715
+ active_profile = _load_profile(active_path, source="active", name="active") if active_path.exists() else None
716
+
717
+ config_dir = _ovcli_config_dir()
718
+ saved_start = len(profiles)
719
+ if config_dir.exists():
720
+ for path in sorted(config_dir.iterdir(), key=lambda item: item.name):
721
+ if not path.is_file():
722
+ continue
723
+ name = path.name.removeprefix(_OVCLI_SAVED_PREFIX)
724
+ if name == path.name or name == "bak" or not _is_valid_ovcli_profile_name(name):
725
+ continue
726
+ add(path, source="saved", name=name)
727
+
728
+ if active_profile is not None:
729
+ marked_active = False
730
+ for idx in range(saved_start, len(profiles)):
731
+ if profiles[idx].source == "saved" and _profiles_equivalent(profiles[idx], active_profile):
732
+ profiles[idx] = replace(profiles[idx], is_active=True)
733
+ marked_active = True
734
+ break
735
+ has_env_profile = any(profile.source == "env" for profile in profiles)
736
+ has_saved_profile = any(profile.source == "saved" for profile in profiles)
737
+ active_identity = _profile_identity(active_profile.path)
738
+ if not marked_active and not has_env_profile and not has_saved_profile and active_identity not in seen_paths:
739
+ profiles.append(active_profile)
740
+
741
+ return profiles
742
+
743
+
744
+ def _is_local_openviking_url(value: str) -> bool:
745
+ candidate = _normalize_openviking_url(value)
746
+ if not candidate:
747
+ return False
748
+ if "://" not in candidate:
749
+ candidate = f"//{candidate}"
750
+ parsed = urlparse(candidate)
751
+ scheme = (parsed.scheme or "http").lower()
752
+ return scheme == "http" and (parsed.hostname or "").lower() in _LOCAL_OPENVIKING_HOSTS
753
+
754
+
755
+ def _load_hermes_openviking_config() -> dict:
756
+ try:
757
+ from hermes_cli.config import load_config
758
+
759
+ config = load_config()
760
+ memory_config = config.get("memory", {}) if isinstance(config, dict) else {}
761
+ provider_config = memory_config.get("openviking", {}) if isinstance(memory_config, dict) else {}
762
+ return dict(provider_config) if isinstance(provider_config, dict) else {}
763
+ except Exception:
764
+ return {}
765
+
766
+
767
+ def _env_value(name: str) -> Optional[str]:
768
+ return os.environ[name].strip() if name in os.environ else None
769
+
770
+
771
+ def _first_nonempty(*values: Optional[str], default: str = "") -> str:
772
+ for value in values:
773
+ if value:
774
+ return value
775
+ return default
776
+
777
+
778
+ def _resolve_connection_settings(provider_config: Optional[dict] = None) -> dict:
779
+ provider_config = dict(provider_config or {})
780
+ ovcli_values: dict = {}
781
+ if provider_config.get("use_ovcli_config"):
782
+ ovcli_path = _resolve_ovcli_config_path(str(provider_config.get("ovcli_config_path") or ""))
783
+ ovcli_values = _connection_values_from_ovcli(_load_ovcli_config(ovcli_path))
784
+
785
+ endpoint_env = _env_value("OPENVIKING_ENDPOINT")
786
+ api_key_env = _env_value("OPENVIKING_API_KEY")
787
+ account_env = _env_value("OPENVIKING_ACCOUNT")
788
+ user_env = _env_value("OPENVIKING_USER")
789
+ agent_env = _env_value("OPENVIKING_AGENT")
790
+
791
+ return {
792
+ "endpoint": _first_nonempty(endpoint_env, ovcli_values.get("endpoint"), default=_DEFAULT_ENDPOINT),
793
+ "api_key": api_key_env if api_key_env is not None else ovcli_values.get("api_key", ""),
794
+ "account": account_env if account_env is not None else ovcli_values.get("account", ""),
795
+ "user": user_env if user_env is not None else ovcli_values.get("user", ""),
796
+ "agent": _first_nonempty(agent_env, ovcli_values.get("agent"), default=_DEFAULT_AGENT),
797
+ }
798
+
799
+
800
+ def _env_writes_from_connection_values(values: dict) -> dict:
801
+ writes = {}
802
+ mapping = {
803
+ "OPENVIKING_ENDPOINT": "endpoint",
804
+ "OPENVIKING_API_KEY": "api_key",
805
+ "OPENVIKING_ACCOUNT": "account",
806
+ "OPENVIKING_USER": "user",
807
+ "OPENVIKING_AGENT": "agent",
808
+ }
809
+ for env_key, value_key in mapping.items():
810
+ value = _clean_config_value(values.get(value_key))
811
+ if value:
812
+ writes[env_key] = value
813
+ return writes
814
+
815
+
816
+ def _restrict_secret_file_permissions(path: Path) -> None:
817
+ try:
818
+ path.chmod(stat.S_IRUSR | stat.S_IWUSR)
819
+ except OSError as e:
820
+ logger.debug("Could not restrict permissions on %s: %s", path, e)
821
+
822
+
823
+ def _precreate_secret_file(path: Path) -> None:
824
+ """Create (or tighten) a secret-bearing file with 0600 BEFORE writing.
825
+
826
+ Writing the file first and chmod-ing afterwards leaves a window where a
827
+ freshly-created file is world-readable under the default umask (e.g. 0644),
828
+ briefly exposing the api_key/root_api_key. Pre-creating with 0600 closes
829
+ that window; an existing file is tightened to 0600 here too.
830
+ """
831
+ try:
832
+ if not path.exists():
833
+ os.close(os.open(str(path), os.O_CREAT | os.O_WRONLY, 0o600))
834
+ _restrict_secret_file_permissions(path)
835
+ except OSError as e:
836
+ logger.debug("Could not pre-create secret file %s: %s", path, e)
837
+
838
+
839
+ def _write_env_vars(env_path: Path, env_writes: dict, remove_keys: tuple[str, ...] = ()) -> None:
840
+ env_path.parent.mkdir(parents=True, exist_ok=True)
841
+ remove_set = set(remove_keys) - set(env_writes)
842
+ existing_lines = env_path.read_text(encoding="utf-8").splitlines() if env_path.exists() else []
843
+ updated_keys = set()
844
+ new_lines = []
845
+ for line in existing_lines:
846
+ key_match = line.split("=", 1)[0].strip() if "=" in line else ""
847
+ if key_match in remove_set:
848
+ continue
849
+ if key_match in env_writes:
850
+ new_lines.append(f"{key_match}={env_writes[key_match]}")
851
+ updated_keys.add(key_match)
852
+ else:
853
+ new_lines.append(line)
854
+ for key, val in env_writes.items():
855
+ if key not in updated_keys:
856
+ new_lines.append(f"{key}={val}")
857
+ # Pre-create with 0600 so secrets are never briefly world-readable.
858
+ _precreate_secret_file(env_path)
859
+ env_path.write_text("\n".join(new_lines) + ("\n" if new_lines else ""), encoding="utf-8")
860
+ _restrict_secret_file_permissions(env_path)
861
+
862
+
863
+ def _remember_ovcli_path(provider_config: dict, ovcli_path: Path) -> None:
864
+ default_path = _default_ovcli_config_path().expanduser()
865
+ if os.environ.get(_OVCLI_CONFIG_ENV, "").strip() or ovcli_path.expanduser() != default_path:
866
+ provider_config["ovcli_config_path"] = str(ovcli_path)
867
+ else:
868
+ provider_config.pop("ovcli_config_path", None)
869
+
870
+
871
+ def _ovcli_data_from_connection_values(values: dict) -> dict:
872
+ data = {"url": _normalize_openviking_url(_clean_config_value(values.get("endpoint")) or _DEFAULT_ENDPOINT)}
873
+ api_key = _clean_config_value(values.get("api_key"))
874
+ root_api_key = _clean_config_value(values.get("root_api_key"))
875
+ account = _clean_config_value(values.get("account"))
876
+ user = _clean_config_value(values.get("user"))
877
+ agent = _clean_config_value(values.get("agent")) or _DEFAULT_AGENT
878
+ if api_key:
879
+ data["api_key"] = api_key
880
+ if root_api_key:
881
+ data["root_api_key"] = root_api_key
882
+ if account:
883
+ data["account"] = account
884
+ if user:
885
+ data["user"] = user
886
+ if agent:
887
+ data["actor_peer_id"] = agent
888
+ return data
889
+
890
+
891
+ def _write_ovcli_config(path: Path, values: dict) -> None:
892
+ path.parent.mkdir(parents=True, exist_ok=True)
893
+ # atomic_json_write creates the temp file with mode 0o600 and os.replace()s
894
+ # it into place — no half-written config on crash and no chmod-after-write
895
+ # TOCTOU window for the api_key/root_api_key it carries.
896
+ atomic_json_write(path, _ovcli_data_from_connection_values(values), mode=0o600)
897
+
898
+
899
+ def _validate_openviking_reachability(endpoint: str) -> tuple[bool, str]:
900
+ endpoint = _normalize_openviking_url(endpoint)
901
+ try:
902
+ client = _VikingClient(endpoint)
903
+ if hasattr(client, "health_payload"):
904
+ payload = client.health_payload()
905
+ if payload.get("healthy") is False:
906
+ return False, "OpenViking server responded but reported unhealthy status."
907
+ if payload:
908
+ return True, ""
909
+ elif client.health():
910
+ return True, ""
911
+ except Exception as e:
912
+ if _status_code_from_error(e) is not None:
913
+ return False, f"OpenViking server responded with {_format_openviking_exception(e)}."
914
+ return False, f"OpenViking server is not reachable at {endpoint}: {_format_openviking_exception(e)}"
915
+ return False, f"OpenViking server is not reachable at {endpoint}."
916
+
917
+
918
+ def _validate_openviking_auth(values: dict) -> tuple[bool, str]:
919
+ endpoint = _normalize_openviking_url(values.get("endpoint"))
920
+ try:
921
+ client = _VikingClient(
922
+ endpoint,
923
+ _clean_config_value(values.get("api_key")),
924
+ account=_clean_config_value(values.get("account")),
925
+ user=_clean_config_value(values.get("user")),
926
+ agent=_clean_config_value(values.get("agent")) or _DEFAULT_AGENT,
927
+ )
928
+ client.validate_auth()
929
+ except Exception as e:
930
+ return False, f"OpenViking authentication validation failed: {_format_openviking_exception(e)}"
931
+ return True, ""
932
+
933
+
934
+ def _validate_openviking_root_access(values: dict) -> tuple[bool, str]:
935
+ endpoint = _normalize_openviking_url(values.get("endpoint"))
936
+ try:
937
+ client = _VikingClient(
938
+ endpoint,
939
+ _clean_config_value(values.get("api_key")),
940
+ agent=_clean_config_value(values.get("agent")) or _DEFAULT_AGENT,
941
+ )
942
+ client.validate_root_access()
943
+ except Exception as e:
944
+ return False, f"OpenViking root API key validation failed: {_format_openviking_exception(e)}"
945
+ return True, ""
946
+
947
+
948
+ def _validate_openviking_user_key_scope(values: dict) -> tuple[bool, str]:
949
+ root_ok, _message = _validate_openviking_root_access(values)
950
+ if not root_ok:
951
+ return True, ""
952
+ return (
953
+ False,
954
+ "That key has ROOT access. Choose Root API key and provide account/user, "
955
+ "or enter a user API key.",
956
+ )
957
+
958
+
959
+ def _status_code_from_error(error: Exception) -> Optional[int]:
960
+ if isinstance(error, _OpenVikingHTTPError):
961
+ return error.status_code
962
+ response = getattr(error, "response", None)
963
+ return getattr(response, "status_code", None)
964
+
965
+
966
+ def _admin_probe_means_regular_key(error: Exception) -> bool:
967
+ return _status_code_from_error(error) in {401, 403, 404}
968
+
969
+
970
+ def _should_probe_openviking_auth(health: dict, *, require_api_key: bool, has_api_key: bool) -> bool:
971
+ if require_api_key or has_api_key:
972
+ return True
973
+ auth_mode = health.get("auth_mode")
974
+ if auth_mode == "dev":
975
+ return False
976
+ if auth_mode in {"api_key", "trusted", None}:
977
+ return True
978
+ return False
979
+
980
+
981
+ def _validate_openviking_setup_values(
982
+ values: dict,
983
+ *,
984
+ require_api_key: bool = False,
985
+ ) -> tuple[bool, str, Optional[str]]:
986
+ endpoint = _normalize_openviking_url(values.get("endpoint"))
987
+ api_key = _clean_config_value(values.get("api_key"))
988
+ if require_api_key and not api_key:
989
+ return False, "Remote OpenViking configs require an API key.", None
990
+
991
+ try:
992
+ client = _VikingClient(
993
+ endpoint,
994
+ api_key,
995
+ account=_clean_config_value(values.get("account")),
996
+ user=_clean_config_value(values.get("user")),
997
+ agent=_clean_config_value(values.get("agent")) or _DEFAULT_AGENT,
998
+ )
999
+ health = client.health_payload()
1000
+ if health.get("healthy") is False:
1001
+ return False, "OpenViking server responded but reported unhealthy status.", None
1002
+ if _should_probe_openviking_auth(
1003
+ health,
1004
+ require_api_key=require_api_key,
1005
+ has_api_key=bool(api_key),
1006
+ ):
1007
+ client.validate_auth()
1008
+ if not api_key:
1009
+ return True, "", None
1010
+ try:
1011
+ client.validate_root_access()
1012
+ return True, "", "root"
1013
+ except Exception as e:
1014
+ if _admin_probe_means_regular_key(e):
1015
+ return True, "", "user"
1016
+ raise
1017
+ except Exception as e:
1018
+ return False, f"OpenViking validation failed: {_format_openviking_exception(e)}", None
1019
+
1020
+
1021
+ def _retry_or_cancel_manual_setup(select, title: str, message: str, cancelled):
1022
+ print(f" {message}")
1023
+ choice = select(
1024
+ title,
1025
+ [
1026
+ ("Retry", "try this step again"),
1027
+ ("Cancel setup", "no changes saved"),
1028
+ ],
1029
+ default=0,
1030
+ cancel_returns=cancelled,
1031
+ )
1032
+ if choice == 0:
1033
+ return True
1034
+ return _SETUP_CANCELLED
1035
+
1036
+
1037
+ def _print_validation_progress(message: str) -> None:
1038
+ print(f" {message}", flush=True)
1039
+
1040
+
1041
+ def _local_openviking_bind(endpoint: str) -> tuple[str, int]:
1042
+ normalized = _normalize_openviking_url(endpoint)
1043
+ parsed = urlparse(normalized)
1044
+ host = parsed.hostname or "127.0.0.1"
1045
+ port = parsed.port or 1933
1046
+ return host, port
1047
+
1048
+
1049
+ def _openviking_server_log_path() -> Path:
1050
+ try:
1051
+ from hermes_constants import get_hermes_home
1052
+ home = get_hermes_home()
1053
+ except Exception:
1054
+ home = Path(os.environ.get("HERMES_HOME", "")).expanduser() if os.environ.get("HERMES_HOME") else Path.home() / ".hermes"
1055
+ return home / _OPENVIKING_SERVER_LOG_RELATIVE_PATH
1056
+
1057
+
1058
+ def _start_local_openviking_server(endpoint: str) -> tuple[bool, str]:
1059
+ server_cmd = shutil.which("openviking-server")
1060
+ if not server_cmd:
1061
+ return False, "openviking-server was not found on PATH. Start it manually, then retry."
1062
+ try:
1063
+ host, port = _local_openviking_bind(endpoint)
1064
+ except ValueError as e:
1065
+ return False, f"Could not parse local OpenViking URL: {e}"
1066
+ log_path = _openviking_server_log_path()
1067
+ try:
1068
+ log_path.parent.mkdir(parents=True, exist_ok=True)
1069
+ with log_path.open("ab") as log_file:
1070
+ subprocess.Popen(
1071
+ [server_cmd, "--host", host, "--port", str(port)],
1072
+ stdout=log_file,
1073
+ stderr=log_file,
1074
+ stdin=subprocess.DEVNULL,
1075
+ start_new_session=True,
1076
+ )
1077
+ except Exception as e:
1078
+ return False, f"Could not start openviking-server: {e}"
1079
+ return True, f"Started openviking-server on {host}:{port} in the background. Logs: {log_path}"
1080
+
1081
+
1082
+ def _wait_for_openviking_health(endpoint: str, *, timeout_seconds: float = 15.0) -> bool:
1083
+ deadline = time.monotonic() + timeout_seconds
1084
+ while time.monotonic() < deadline:
1085
+ ok, _message = _validate_openviking_reachability(endpoint)
1086
+ if ok:
1087
+ return True
1088
+ time.sleep(0.5)
1089
+ return False
1090
+
1091
+
1092
+ def _reachability_failure_allows_local_autostart(message: str) -> bool:
1093
+ return not (message or "").startswith(_OPENVIKING_RESPONDED_FAILURE_PREFIX)
1094
+
1095
+
1096
+ def _handle_unreachable_endpoint(
1097
+ endpoint: str,
1098
+ message: str,
1099
+ select,
1100
+ cancelled,
1101
+ *,
1102
+ allow_local_autostart: bool = True,
1103
+ ):
1104
+ if _is_local_openviking_url(endpoint) and allow_local_autostart:
1105
+ print(f" {message}")
1106
+ choice = select(
1107
+ " Local OpenViking server is down",
1108
+ [
1109
+ ("Start local OpenViking", "run openviking-server and retry"),
1110
+ ("Retry URL", "enter the server URL again"),
1111
+ ("Cancel setup", "no changes saved"),
1112
+ ],
1113
+ default=0,
1114
+ cancel_returns=cancelled,
1115
+ )
1116
+ if choice == 0:
1117
+ started, start_message = _start_local_openviking_server(endpoint)
1118
+ print(f" {start_message}")
1119
+ if not started:
1120
+ return False
1121
+ print(" Waiting for OpenViking server to become reachable...", flush=True)
1122
+ if _wait_for_openviking_health(
1123
+ endpoint,
1124
+ timeout_seconds=_LOCAL_OPENVIKING_AUTOSTART_TIMEOUT,
1125
+ ):
1126
+ print(" OpenViking server is reachable.")
1127
+ return True
1128
+ print(" OpenViking server did not become reachable.")
1129
+ return False
1130
+ if choice == 1:
1131
+ return False
1132
+ return _SETUP_CANCELLED
1133
+
1134
+ return _retry_or_cancel_manual_setup(
1135
+ select,
1136
+ " OpenViking server unhealthy" if _is_local_openviking_url(endpoint) else " OpenViking server unreachable",
1137
+ message,
1138
+ cancelled,
1139
+ )
1140
+
1141
+
1142
+ def _emit_runtime_warning(message: str, warning_callback=None) -> None:
1143
+ logger.warning("%s", message)
1144
+ if warning_callback:
1145
+ try:
1146
+ warning_callback(message)
1147
+ except Exception:
1148
+ logger.debug("OpenViking runtime warning callback failed", exc_info=True)
1149
+
1150
+
1151
+ def _emit_runtime_status(message: str, status_callback=None) -> None:
1152
+ logger.info("%s", message)
1153
+ if status_callback:
1154
+ try:
1155
+ status_callback(message)
1156
+ except Exception:
1157
+ logger.debug("OpenViking runtime status callback failed", exc_info=True)
1158
+
1159
+
1160
+ def _runtime_openviking_timeout_message(endpoint: str) -> str:
1161
+ return (
1162
+ f"Local OpenViking server at {endpoint} is not reachable. "
1163
+ "Tried to start openviking-server, but it did not become reachable "
1164
+ f"within {_LOCAL_OPENVIKING_AUTOSTART_TIMEOUT:.0f} seconds. "
1165
+ "OpenViking memory disabled for this Hermes run."
1166
+ )
1167
+
1168
+
1169
+ def _classify_runtime_openviking_health(client: _VikingClient, endpoint: str) -> tuple[str, str]:
1170
+ """Classify runtime health without treating every false result as server absence."""
1171
+ try:
1172
+ if hasattr(client, "health_payload"):
1173
+ payload = client.health_payload()
1174
+ if payload.get("healthy") is False:
1175
+ return (
1176
+ "responded",
1177
+ f"OpenViking server at {endpoint} responded but reported unhealthy status.",
1178
+ )
1179
+ return "healthy", ""
1180
+ if client.health():
1181
+ return "healthy", ""
1182
+ except _OpenVikingHTTPError as e:
1183
+ return (
1184
+ "responded",
1185
+ f"OpenViking server at {endpoint} responded with {_format_openviking_exception(e)}.",
1186
+ )
1187
+ except Exception:
1188
+ return "unreachable", ""
1189
+ return "unreachable", ""
1190
+
1191
+
1192
+ def _prompt_profile_name(prompt, select, cancelled) -> str | object:
1193
+ while True:
1194
+ name = _clean_config_value(prompt("OpenViking profile name"))
1195
+ if _is_valid_ovcli_profile_name(name):
1196
+ return name
1197
+ retry = _retry_or_cancel_manual_setup(
1198
+ select,
1199
+ " Invalid OpenViking profile name",
1200
+ "Profile names can only contain letters, numbers, '-' and '_'.",
1201
+ cancelled,
1202
+ )
1203
+ if retry is _SETUP_CANCELLED:
1204
+ return _SETUP_CANCELLED
1205
+
1206
+
1207
+ def _confirm_replace_existing_profile(path: Path, values: dict, select, cancelled):
1208
+ if not path.exists():
1209
+ return True
1210
+ try:
1211
+ existing_data = _load_ovcli_config(path)
1212
+ except Exception:
1213
+ existing_data = {}
1214
+ if existing_data == _ovcli_data_from_connection_values(values):
1215
+ return True
1216
+ choice = select(
1217
+ " OpenViking profile already exists",
1218
+ [
1219
+ ("Choose another name", "leave the existing profile unchanged"),
1220
+ ("Replace profile", "overwrite this saved OpenViking profile"),
1221
+ ("Cancel setup", "no changes saved"),
1222
+ ],
1223
+ default=0,
1224
+ cancel_returns=cancelled,
1225
+ )
1226
+ if choice == 1:
1227
+ return True
1228
+ if choice == 0:
1229
+ return False
1230
+ return _SETUP_CANCELLED
1231
+
1232
+
1233
+ def _prompt_manual_connection_values(prompt, select, cancelled, *, service: bool = False):
1234
+ if service:
1235
+ endpoint = _OPENVIKING_SERVICE_ENDPOINT
1236
+ print(f" OpenViking Service endpoint: {endpoint}")
1237
+ else:
1238
+ while True:
1239
+ endpoint = _normalize_openviking_url(prompt("OpenViking server URL", default=_DEFAULT_ENDPOINT))
1240
+ _print_validation_progress("Checking OpenViking server...")
1241
+ reachable, message = _validate_openviking_reachability(endpoint)
1242
+ if reachable:
1243
+ print(" OpenViking server is reachable.")
1244
+ break
1245
+ retry = _handle_unreachable_endpoint(
1246
+ endpoint,
1247
+ message,
1248
+ select,
1249
+ cancelled,
1250
+ allow_local_autostart=_reachability_failure_allows_local_autostart(message),
1251
+ )
1252
+ if retry is True:
1253
+ break
1254
+ if retry is _SETUP_CANCELLED:
1255
+ return _SETUP_CANCELLED
1256
+
1257
+ is_local = _is_local_openviking_url(endpoint)
1258
+ api_key_type = "user" if service else ""
1259
+ prefilled_api_key = ""
1260
+ prefilled_agent = ""
1261
+ while True:
1262
+ values = {
1263
+ "endpoint": endpoint,
1264
+ "api_key": "",
1265
+ "root_api_key": "",
1266
+ "account": "",
1267
+ "user": "",
1268
+ "agent": "",
1269
+ }
1270
+ if not api_key_type and is_local:
1271
+ credential_choice = select(
1272
+ " OpenViking credential",
1273
+ [
1274
+ ("No API key", "local dev mode"),
1275
+ ("User API key", "server derives account/user automatically"),
1276
+ ("Root API key", "requires account and user IDs"),
1277
+ ],
1278
+ default=0,
1279
+ cancel_returns=cancelled,
1280
+ )
1281
+ if credential_choice == cancelled:
1282
+ return _SETUP_CANCELLED
1283
+ if credential_choice == 0:
1284
+ values["agent"] = _clean_config_value(
1285
+ prompt(_AGENT_PROMPT_LABEL, default=_DEFAULT_AGENT)
1286
+ ) or _DEFAULT_AGENT
1287
+ _print_validation_progress("Validating OpenViking local dev access...")
1288
+ valid, message, _role = _validate_openviking_setup_values(values)
1289
+ if valid:
1290
+ print(" OpenViking local dev access validated.")
1291
+ return values
1292
+ retry = _retry_or_cancel_manual_setup(
1293
+ select,
1294
+ " OpenViking credential failed",
1295
+ message,
1296
+ cancelled,
1297
+ )
1298
+ if retry is _SETUP_CANCELLED:
1299
+ return _SETUP_CANCELLED
1300
+ continue
1301
+ api_key_type = "root" if credential_choice == 2 else "user"
1302
+ elif not api_key_type:
1303
+ credential_choice = select(
1304
+ " OpenViking API key type",
1305
+ [
1306
+ ("User API key", "server derives account/user automatically"),
1307
+ ("Root API key", "requires account and user IDs"),
1308
+ ],
1309
+ default=0,
1310
+ cancel_returns=cancelled,
1311
+ )
1312
+ if credential_choice == cancelled:
1313
+ return _SETUP_CANCELLED
1314
+ api_key_type = "root" if credential_choice == 1 else "user"
1315
+
1316
+ values["api_key_type"] = api_key_type
1317
+ if service:
1318
+ api_key_label = "OpenViking API key"
1319
+ else:
1320
+ api_key_label = (
1321
+ "OpenViking root API key"
1322
+ if api_key_type == "root"
1323
+ else "OpenViking user API key"
1324
+ )
1325
+ if prefilled_api_key:
1326
+ values["api_key"] = prefilled_api_key
1327
+ prefilled_api_key = ""
1328
+ else:
1329
+ values["api_key"] = _clean_config_value(prompt(api_key_label, secret=True))
1330
+ if not values["api_key"]:
1331
+ retry = _retry_or_cancel_manual_setup(
1332
+ select,
1333
+ " OpenViking API key required",
1334
+ f"{api_key_label} is required.",
1335
+ cancelled,
1336
+ )
1337
+ if retry is _SETUP_CANCELLED:
1338
+ return _SETUP_CANCELLED
1339
+ continue
1340
+
1341
+ if api_key_type == "root":
1342
+ _print_validation_progress("Validating OpenViking root API key...")
1343
+ valid, message, role = _validate_openviking_setup_values(values, require_api_key=True)
1344
+ root_ok = valid and role == "root"
1345
+ if not root_ok:
1346
+ if valid and role == "user":
1347
+ print(" That key is valid, but it is a user API key.")
1348
+ route_choice = select(
1349
+ " OpenViking key is a user key",
1350
+ [
1351
+ ("Use as User API key", "server derives account/user automatically"),
1352
+ ("Re-enter Root API key", "try another root key"),
1353
+ ("Cancel setup", "no changes saved"),
1354
+ ],
1355
+ default=0,
1356
+ cancel_returns=cancelled,
1357
+ )
1358
+ if route_choice == 0:
1359
+ prefilled_api_key = values["api_key"]
1360
+ api_key_type = "user"
1361
+ continue
1362
+ if route_choice == 1:
1363
+ api_key_type = "root"
1364
+ continue
1365
+ return _SETUP_CANCELLED
1366
+ retry = _retry_or_cancel_manual_setup(
1367
+ select,
1368
+ " OpenViking root API key failed",
1369
+ message,
1370
+ cancelled,
1371
+ )
1372
+ if retry is _SETUP_CANCELLED:
1373
+ return _SETUP_CANCELLED
1374
+ continue
1375
+ print(" OpenViking root API key validated.")
1376
+ values["root_api_key"] = values["api_key"]
1377
+ account_ok, account_message, account = _validate_openviking_identity_value(
1378
+ prompt("OpenViking account"),
1379
+ field="account",
1380
+ )
1381
+ user_ok, user_message, user = _validate_openviking_identity_value(
1382
+ prompt("OpenViking user"),
1383
+ field="user",
1384
+ )
1385
+ values["account"] = account
1386
+ values["user"] = user
1387
+ if not account_ok or not user_ok:
1388
+ message = account_message if not account_ok else user_message
1389
+ retry = _retry_or_cancel_manual_setup(
1390
+ select,
1391
+ " OpenViking tenant identity required",
1392
+ message,
1393
+ cancelled,
1394
+ )
1395
+ if retry is _SETUP_CANCELLED:
1396
+ return _SETUP_CANCELLED
1397
+ prefilled_api_key = values["api_key"]
1398
+ continue
1399
+
1400
+ if prefilled_agent:
1401
+ values["agent"] = prefilled_agent
1402
+ prefilled_agent = ""
1403
+ else:
1404
+ values["agent"] = _clean_config_value(
1405
+ prompt(_AGENT_PROMPT_LABEL, default=_DEFAULT_AGENT)
1406
+ ) or _DEFAULT_AGENT
1407
+ _print_validation_progress("Validating OpenViking API access...")
1408
+ valid, message, role = _validate_openviking_setup_values(
1409
+ values,
1410
+ require_api_key=service or not is_local,
1411
+ )
1412
+ if valid:
1413
+ if api_key_type == "user":
1414
+ if role == "root":
1415
+ print(" That key is valid, but it has root access.")
1416
+ route_choice = select(
1417
+ " OpenViking user API key is root key",
1418
+ [
1419
+ ("Configure as Root API key", "provide account and user IDs"),
1420
+ ("Re-enter User API key", "try another user key"),
1421
+ ("Cancel setup", "no changes saved"),
1422
+ ],
1423
+ default=0,
1424
+ cancel_returns=cancelled,
1425
+ )
1426
+ if route_choice == 0:
1427
+ prefilled_api_key = values["api_key"]
1428
+ prefilled_agent = values["agent"]
1429
+ api_key_type = "root"
1430
+ continue
1431
+ if route_choice == 1:
1432
+ api_key_type = "user"
1433
+ continue
1434
+ return _SETUP_CANCELLED
1435
+ if api_key_type == "root" and role != "root":
1436
+ retry = _retry_or_cancel_manual_setup(
1437
+ select,
1438
+ " OpenViking root API key failed",
1439
+ "The supplied key was not accepted as a root API key.",
1440
+ cancelled,
1441
+ )
1442
+ if retry is _SETUP_CANCELLED:
1443
+ return _SETUP_CANCELLED
1444
+ continue
1445
+ print(" OpenViking API access validated.")
1446
+ return values
1447
+ retry = _retry_or_cancel_manual_setup(
1448
+ select,
1449
+ " OpenViking API access failed",
1450
+ message,
1451
+ cancelled,
1452
+ )
1453
+ if retry is _SETUP_CANCELLED:
1454
+ return _SETUP_CANCELLED
1455
+
1456
+
1457
+ def _set_openviking_provider(config: dict, provider_config: dict) -> None:
1458
+ config["memory"]["provider"] = "openviking"
1459
+ config["memory"]["openviking"] = provider_config
1460
+
1461
+
1462
+ def _link_ovcli_profile(
1463
+ *,
1464
+ config: dict,
1465
+ provider_config: dict,
1466
+ env_path: Path,
1467
+ ovcli_path: Path,
1468
+ ) -> None:
1469
+ for key in ("endpoint", "api_key", "root_api_key", "account", "user", "agent", "api_key_type"):
1470
+ provider_config.pop(key, None)
1471
+ provider_config["use_ovcli_config"] = True
1472
+ _remember_ovcli_path(provider_config, ovcli_path)
1473
+ _set_openviking_provider(config, provider_config)
1474
+ _write_env_vars(env_path, {}, remove_keys=_OPENVIKING_ENV_KEYS)
1475
+ for key in _OPENVIKING_ENV_KEYS:
1476
+ os.environ.pop(key, None)
1477
+
1478
+
1479
+ def _save_hermes_only_config(
1480
+ *,
1481
+ config: dict,
1482
+ provider_config: dict,
1483
+ env_path: Path,
1484
+ values: dict,
1485
+ ) -> None:
1486
+ provider_config["use_ovcli_config"] = False
1487
+ provider_config.pop("ovcli_config_path", None)
1488
+ _set_openviking_provider(config, provider_config)
1489
+ _write_env_vars(
1490
+ env_path,
1491
+ _env_writes_from_connection_values(values),
1492
+ remove_keys=_OPENVIKING_ENV_KEYS,
1493
+ )
1494
+
1495
+
1496
+ def _profile_display_name(profile: _OvcliProfile) -> str:
1497
+ if profile.source == "env":
1498
+ return _OVCLI_CONFIG_ENV
1499
+ if profile.source == "active":
1500
+ return "ovcli.conf"
1501
+ return profile.name
1502
+
1503
+
1504
+ def _profile_description(profile: _OvcliProfile) -> str:
1505
+ endpoint = _clean_config_value(profile.values.get("endpoint")) or _DEFAULT_ENDPOINT
1506
+ return f"{endpoint} ({profile.path})"
1507
+
1508
+
1509
+ def _validate_profile_for_setup(profile: _OvcliProfile) -> tuple[bool, str, Optional[str]]:
1510
+ require_api_key = not _is_local_openviking_url(profile.values.get("endpoint", ""))
1511
+ return _validate_openviking_setup_values(profile.values, require_api_key=require_api_key)
1512
+
1513
+
1514
+ def _print_openviking_ready(message: str, path: Optional[Path] = None) -> None:
1515
+ print("\n OpenViking memory is ready")
1516
+ print(f" {message}")
1517
+ if path is not None:
1518
+ print(f" Config file: {path}")
1519
+ print(" Start a new Hermes session to activate.\n")
1520
+
1521
+
1522
+ def _run_existing_profile_setup(
1523
+ *,
1524
+ profiles: list[_OvcliProfile],
1525
+ select,
1526
+ cancelled,
1527
+ config: dict,
1528
+ provider_config: dict,
1529
+ env_path: Path,
1530
+ ) -> bool | object:
1531
+ while True:
1532
+ choice = select(
1533
+ " OpenViking profile",
1534
+ [(_profile_display_name(profile), _profile_description(profile)) for profile in profiles],
1535
+ default=0,
1536
+ cancel_returns=cancelled,
1537
+ )
1538
+ if choice == cancelled:
1539
+ return _SETUP_CANCELLED
1540
+ if choice < 0 or choice >= len(profiles):
1541
+ return _SETUP_CANCELLED
1542
+
1543
+ profile = profiles[choice]
1544
+ _print_validation_progress("Validating OpenViking profile...")
1545
+ ok, message, _role = _validate_profile_for_setup(profile)
1546
+ if ok:
1547
+ _link_ovcli_profile(
1548
+ config=config,
1549
+ provider_config=provider_config,
1550
+ env_path=env_path,
1551
+ ovcli_path=profile.path,
1552
+ )
1553
+ _print_openviking_ready(f"Linked profile: {_profile_display_name(profile)}", profile.path)
1554
+ return True
1555
+
1556
+ print(f" {message}")
1557
+ retry = select(
1558
+ " OpenViking profile validation failed",
1559
+ [
1560
+ ("Choose another profile", "select a different OpenViking profile"),
1561
+ ("Retry validation", "try this profile again"),
1562
+ ("Cancel setup", "no changes saved"),
1563
+ ],
1564
+ default=0,
1565
+ cancel_returns=cancelled,
1566
+ )
1567
+ if retry == 0:
1568
+ continue
1569
+ if retry == 1:
1570
+ _print_validation_progress("Validating OpenViking profile...")
1571
+ ok, message, _role = _validate_profile_for_setup(profile)
1572
+ if ok:
1573
+ _link_ovcli_profile(
1574
+ config=config,
1575
+ provider_config=provider_config,
1576
+ env_path=env_path,
1577
+ ovcli_path=profile.path,
1578
+ )
1579
+ _print_openviking_ready(f"Linked profile: {_profile_display_name(profile)}", profile.path)
1580
+ return True
1581
+ print(f" {message}")
1582
+ continue
1583
+ return _SETUP_CANCELLED
1584
+
1585
+
1586
+ def _mirror_manual_config_to_openviking_store(
1587
+ *,
1588
+ prompt,
1589
+ select,
1590
+ cancelled,
1591
+ values: dict,
1592
+ ) -> Path | object:
1593
+ while True:
1594
+ name = _prompt_profile_name(prompt, select, cancelled)
1595
+ if name is _SETUP_CANCELLED:
1596
+ return _SETUP_CANCELLED
1597
+ path = _ovcli_config_dir() / f"{_OVCLI_SAVED_PREFIX}{name}"
1598
+ replace = _confirm_replace_existing_profile(path, values, select, cancelled)
1599
+ if replace is _SETUP_CANCELLED:
1600
+ return _SETUP_CANCELLED
1601
+ if replace is False:
1602
+ continue
1603
+ _write_ovcli_config(path, values)
1604
+ return path
1605
+
1606
+
1607
+ def _run_create_profile_setup(
1608
+ *,
1609
+ prompt,
1610
+ select,
1611
+ cancelled,
1612
+ config: dict,
1613
+ provider_config: dict,
1614
+ env_path: Path,
1615
+ ) -> bool | object:
1616
+ source_choice = select(
1617
+ " OpenViking connection",
1618
+ [
1619
+ ("OpenViking Service (VolcEngine Cloud)", "use the managed OpenViking endpoint"),
1620
+ ("Custom", "use a local, VPS, or self-hosted OpenViking server"),
1621
+ ],
1622
+ default=0,
1623
+ cancel_returns=cancelled,
1624
+ )
1625
+ if source_choice == cancelled:
1626
+ return _SETUP_CANCELLED
1627
+
1628
+ values = _prompt_manual_connection_values(prompt, select, cancelled, service=(source_choice == 0))
1629
+ if values is _SETUP_CANCELLED:
1630
+ return _SETUP_CANCELLED
1631
+ if values is None:
1632
+ return False
1633
+
1634
+ save_choice = select(
1635
+ " Save OpenViking config",
1636
+ [
1637
+ ("Keep in Hermes only", "write values only to Hermes .env"),
1638
+ ("Mirror to OpenViking store", "write ~/.openviking/ovcli.conf.<name> and link it"),
1639
+ ],
1640
+ default=1,
1641
+ cancel_returns=cancelled,
1642
+ )
1643
+ if save_choice == cancelled:
1644
+ return _SETUP_CANCELLED
1645
+
1646
+ if save_choice == 1:
1647
+ ovcli_path = _mirror_manual_config_to_openviking_store(
1648
+ prompt=prompt,
1649
+ select=select,
1650
+ cancelled=cancelled,
1651
+ values=values,
1652
+ )
1653
+ if ovcli_path is _SETUP_CANCELLED:
1654
+ return _SETUP_CANCELLED
1655
+ _link_ovcli_profile(
1656
+ config=config,
1657
+ provider_config=provider_config,
1658
+ env_path=env_path,
1659
+ ovcli_path=ovcli_path,
1660
+ )
1661
+ _print_openviking_ready("Created and linked OpenViking profile.", ovcli_path)
1662
+ return True
1663
+
1664
+ _save_hermes_only_config(
1665
+ config=config,
1666
+ provider_config=provider_config,
1667
+ env_path=env_path,
1668
+ values=values,
1669
+ )
1670
+ _print_openviking_ready("Connection saved to Hermes .env.")
1671
+ return True
1672
+
1673
+
408
1674
  # ---------------------------------------------------------------------------
409
1675
  # MemoryProvider implementation
410
1676
  # ---------------------------------------------------------------------------
@@ -418,10 +1684,37 @@ class OpenVikingMemoryProvider(MemoryProvider):
418
1684
  self._api_key = ""
419
1685
  self._session_id = ""
420
1686
  self._turn_count = 0
421
- self._sync_thread: Optional[threading.Thread] = None
1687
+ # Guards the (_session_id, _turn_count) pair. sync_turn runs on the
1688
+ # MemoryManager's background sync executor while on_session_end /
1689
+ # on_session_switch run on the caller's thread, so the snapshot+reset
1690
+ # of the turn counter and the session-id rotation must be atomic
1691
+ # against a concurrent increment. See hermes-agent#28296 review.
1692
+ self._session_state_lock = threading.Lock()
1693
+ # Commit only after session writes drain. The set is keyed by the sid
1694
+ # the writer is POSTing under (snapshotted at spawn), so on_session_end
1695
+ # / on_session_switch see every still-alive writer for that sid even
1696
+ # if later writes have replaced the latest-tracked thread.
1697
+ self._inflight_writers: Dict[str, Set[threading.Thread]] = {}
1698
+ self._inflight_lock = threading.Lock()
1699
+ self._deferred_commit_sids: Set[str] = set()
1700
+ self._deferred_commit_threads: Set[threading.Thread] = set()
1701
+ self._deferred_commit_lock = threading.Lock()
1702
+ self._committed_session_ids: Set[str] = set()
1703
+ self._committed_session_lock = threading.Lock()
422
1704
  self._prefetch_result = ""
423
1705
  self._prefetch_lock = threading.Lock()
424
1706
  self._prefetch_thread: Optional[threading.Thread] = None
1707
+ self._runtime_start_lock = threading.Lock()
1708
+ self._runtime_start_thread: Optional[threading.Thread] = None
1709
+ # All prefetch threads ever spawned (daemon, short-lived). Tracked so
1710
+ # shutdown() can drain them and rapid re-queues don't orphan a still-
1711
+ # running thread by overwriting the single _prefetch_thread slot.
1712
+ self._prefetch_threads: Set[threading.Thread] = set()
1713
+ # Set on shutdown so deferred-commit / writer finalizers stop issuing
1714
+ # network writes against a torn-down provider.
1715
+ self._shutting_down = False
1716
+ # Drop prefetch results from older switch generations.
1717
+ self._prefetch_generation = 0
425
1718
 
426
1719
  @property
427
1720
  def name(self) -> str:
@@ -429,7 +1722,16 @@ class OpenVikingMemoryProvider(MemoryProvider):
429
1722
 
430
1723
  def is_available(self) -> bool:
431
1724
  """Check if OpenViking endpoint is configured. No network calls."""
432
- return bool(os.environ.get("OPENVIKING_ENDPOINT"))
1725
+ if os.environ.get("OPENVIKING_ENDPOINT"):
1726
+ return True
1727
+ provider_config = _load_hermes_openviking_config()
1728
+ if not provider_config.get("use_ovcli_config"):
1729
+ return False
1730
+ try:
1731
+ ovcli_path = _resolve_ovcli_config_path(str(provider_config.get("ovcli_config_path") or ""))
1732
+ return bool(_connection_values_from_ovcli(_load_ovcli_config(ovcli_path)).get("endpoint"))
1733
+ except Exception:
1734
+ return False
433
1735
 
434
1736
  def get_config_schema(self):
435
1737
  return [
@@ -448,40 +1750,265 @@ class OpenVikingMemoryProvider(MemoryProvider):
448
1750
  },
449
1751
  {
450
1752
  "key": "account",
451
- "description": "OpenViking tenant account ID ([default], used when local mode, OPENVIKING_API_KEY is empty)",
452
- "default": "default",
1753
+ "description": "OpenViking tenant account ID (blank for user API keys)",
453
1754
  "env_var": "OPENVIKING_ACCOUNT",
454
1755
  },
455
1756
  {
456
1757
  "key": "user",
457
- "description": "OpenViking user ID within the account ([default], used when local mode, OPENVIKING_API_KEY is empty)",
458
- "default": "default",
1758
+ "description": "OpenViking user ID within the account (blank for user API keys)",
459
1759
  "env_var": "OPENVIKING_USER",
460
1760
  },
461
1761
  {
462
1762
  "key": "agent",
463
- "description": "OpenViking agent ID within the account ([hermes], useful in multi-agent mode)",
1763
+ "description": (
1764
+ "Hermes peer ID in OpenViking, sent as the actor peer and "
1765
+ "used for peer-scoped memories"
1766
+ ),
464
1767
  "default": "hermes",
465
1768
  "env_var": "OPENVIKING_AGENT",
466
1769
  },
467
1770
  ]
468
1771
 
1772
+ def get_status_config(self, provider_config: dict) -> dict:
1773
+ provider_config = dict(provider_config or {})
1774
+ if provider_config.get("use_ovcli_config"):
1775
+ ovcli_path = _resolve_ovcli_config_path(str(provider_config.get("ovcli_config_path") or ""))
1776
+ try:
1777
+ settings = _resolve_connection_settings(provider_config)
1778
+ except Exception as e:
1779
+ return {
1780
+ "use_ovcli_config": True,
1781
+ "ovcli_config_path": str(ovcli_path),
1782
+ "error": _format_openviking_exception(e),
1783
+ }
1784
+
1785
+ display = {
1786
+ "use_ovcli_config": True,
1787
+ "ovcli_config_path": str(ovcli_path),
1788
+ "endpoint": settings.get("endpoint") or _DEFAULT_ENDPOINT,
1789
+ "agent": settings.get("agent") or _DEFAULT_AGENT,
1790
+ }
1791
+ if settings.get("account"):
1792
+ display["account"] = settings["account"]
1793
+ if settings.get("user"):
1794
+ display["user"] = settings["user"]
1795
+ env_overrides = [key for key in _OPENVIKING_ENV_KEYS if _env_value(key) is not None]
1796
+ if env_overrides:
1797
+ display["env_overrides"] = ", ".join(env_overrides)
1798
+ return display
1799
+
1800
+ display = dict(provider_config)
1801
+ for key in ("api_key", "root_api_key"):
1802
+ if key in display:
1803
+ display[key] = "(set)"
1804
+ return display
1805
+
1806
+ def post_setup(self, hermes_home: str, config: dict) -> None:
1807
+ """Custom setup that can reuse OpenViking's shared CLI config."""
1808
+ from hermes_cli.config import save_config
1809
+ from hermes_cli.memory_setup import _CANCELLED, _curses_select, _print_cancelled_setup, _prompt
1810
+
1811
+ hermes_home_path = Path(hermes_home)
1812
+ env_path = hermes_home_path / ".env"
1813
+ if not isinstance(config.get("memory"), dict):
1814
+ config["memory"] = {}
1815
+ provider_config = config["memory"].get("openviking", {})
1816
+ if not isinstance(provider_config, dict):
1817
+ provider_config = {}
1818
+
1819
+ print("\n OpenViking memory setup\n")
1820
+
1821
+ profiles = _discover_ovcli_profiles()
1822
+ if profiles:
1823
+ setup_options = [
1824
+ ("Use existing OpenViking profile", "choose from detected ovcli.conf profiles"),
1825
+ ("Create new OpenViking profile", "enter a new URL/API key"),
1826
+ ]
1827
+ choice = _curses_select(
1828
+ " OpenViking config source",
1829
+ setup_options,
1830
+ default=0,
1831
+ cancel_returns=_CANCELLED,
1832
+ )
1833
+ if choice == _CANCELLED:
1834
+ _print_cancelled_setup()
1835
+ return
1836
+
1837
+ if choice == 0:
1838
+ result = _run_existing_profile_setup(
1839
+ profiles=profiles,
1840
+ select=_curses_select,
1841
+ cancelled=_CANCELLED,
1842
+ config=config,
1843
+ provider_config=provider_config,
1844
+ env_path=env_path,
1845
+ )
1846
+ if result is _SETUP_CANCELLED:
1847
+ _print_cancelled_setup()
1848
+ return
1849
+ if result:
1850
+ save_config(config)
1851
+ return
1852
+
1853
+ else:
1854
+ print(" No existing OpenViking CLI profiles found. Creating a new config.")
1855
+
1856
+ result = _run_create_profile_setup(
1857
+ prompt=_prompt,
1858
+ select=_curses_select,
1859
+ cancelled=_CANCELLED,
1860
+ config=config,
1861
+ provider_config=provider_config,
1862
+ env_path=env_path,
1863
+ )
1864
+ if result is _SETUP_CANCELLED:
1865
+ _print_cancelled_setup()
1866
+ return
1867
+ if result:
1868
+ save_config(config)
1869
+
1870
+ def _start_runtime_openviking_waiter(
1871
+ self,
1872
+ *,
1873
+ status_callback=None,
1874
+ warning_callback=None,
1875
+ ) -> None:
1876
+ with self._runtime_start_lock:
1877
+ if self._runtime_start_thread and self._runtime_start_thread.is_alive():
1878
+ return
1879
+ self._runtime_start_thread = threading.Thread(
1880
+ target=self._finish_runtime_openviking_start,
1881
+ kwargs={
1882
+ "status_callback": status_callback,
1883
+ "warning_callback": warning_callback,
1884
+ },
1885
+ daemon=True,
1886
+ name="openviking-runtime-start",
1887
+ )
1888
+ self._runtime_start_thread.start()
1889
+
1890
+ def _finish_runtime_openviking_start(
1891
+ self,
1892
+ *,
1893
+ status_callback=None,
1894
+ warning_callback=None,
1895
+ ) -> None:
1896
+ endpoint = self._endpoint
1897
+ if not _wait_for_openviking_health(
1898
+ endpoint,
1899
+ timeout_seconds=_LOCAL_OPENVIKING_AUTOSTART_TIMEOUT,
1900
+ ):
1901
+ _emit_runtime_warning(
1902
+ _runtime_openviking_timeout_message(endpoint),
1903
+ warning_callback,
1904
+ )
1905
+ return
1906
+
1907
+ try:
1908
+ client = _VikingClient(
1909
+ endpoint,
1910
+ self._api_key,
1911
+ account=self._account,
1912
+ user=self._user,
1913
+ agent=self._agent,
1914
+ )
1915
+ if not client.health():
1916
+ _emit_runtime_warning(
1917
+ f"OpenViking server at {endpoint} is still not reachable after auto-start; "
1918
+ "OpenViking memory disabled for this Hermes run.",
1919
+ warning_callback,
1920
+ )
1921
+ return
1922
+ except ImportError:
1923
+ logger.warning("httpx not installed — OpenViking plugin disabled")
1924
+ return
1925
+ except Exception as e:
1926
+ _emit_runtime_warning(
1927
+ f"OpenViking server at {endpoint} could not be attached after auto-start: {e}. "
1928
+ "OpenViking memory disabled for this Hermes run.",
1929
+ warning_callback,
1930
+ )
1931
+ return
1932
+
1933
+ self._client = client
1934
+ _emit_runtime_status(
1935
+ f"Local OpenViking server at {endpoint} is reachable; OpenViking memory is active for later turns.",
1936
+ status_callback,
1937
+ )
1938
+
1939
+ def _handle_runtime_openviking_unreachable(
1940
+ self,
1941
+ *,
1942
+ status_callback=None,
1943
+ warning_callback=None,
1944
+ ) -> None:
1945
+ endpoint = self._endpoint
1946
+ if not _is_local_openviking_url(endpoint):
1947
+ _emit_runtime_warning(
1948
+ f"Remote OpenViking server at {endpoint} is not reachable; "
1949
+ "OpenViking memory disabled for this Hermes run. "
1950
+ "Check the configured endpoint and network connectivity.",
1951
+ warning_callback,
1952
+ )
1953
+ self._client = None
1954
+ return
1955
+
1956
+ started, start_message = _start_local_openviking_server(endpoint)
1957
+ if not started:
1958
+ _emit_runtime_warning(
1959
+ f"Local OpenViking server at {endpoint} is not reachable. {start_message} "
1960
+ "OpenViking memory disabled for this Hermes run.",
1961
+ warning_callback,
1962
+ )
1963
+ self._client = None
1964
+ return
1965
+
1966
+ self._client = None
1967
+ _emit_runtime_status(
1968
+ f"{start_message} OpenViking memory is starting in the background and will attach when ready.",
1969
+ status_callback,
1970
+ )
1971
+ self._start_runtime_openviking_waiter(
1972
+ status_callback=status_callback,
1973
+ warning_callback=warning_callback,
1974
+ )
1975
+
469
1976
  def initialize(self, session_id: str, **kwargs) -> None:
470
- self._endpoint = os.environ.get("OPENVIKING_ENDPOINT", _DEFAULT_ENDPOINT)
471
- self._api_key = os.environ.get("OPENVIKING_API_KEY", "")
472
- self._account = os.environ.get("OPENVIKING_ACCOUNT", "default")
473
- self._user = os.environ.get("OPENVIKING_USER", "default")
474
- self._agent = os.environ.get("OPENVIKING_AGENT", "hermes")
1977
+ settings = _resolve_connection_settings(_load_hermes_openviking_config())
1978
+ self._endpoint = settings["endpoint"]
1979
+ self._api_key = settings["api_key"]
1980
+ self._account = settings["account"]
1981
+ self._user = settings["user"]
1982
+ self._agent = settings["agent"]
475
1983
  self._session_id = session_id
476
1984
  self._turn_count = 0
1985
+ warning_callback = (
1986
+ kwargs.get("warning_callback")
1987
+ if kwargs.get("platform") == "cli"
1988
+ else None
1989
+ )
1990
+ status_callback = (
1991
+ kwargs.get("status_callback")
1992
+ if kwargs.get("platform") == "cli"
1993
+ else None
1994
+ )
477
1995
 
478
1996
  try:
479
1997
  self._client = _VikingClient(
480
1998
  self._endpoint, self._api_key,
481
1999
  account=self._account, user=self._user, agent=self._agent,
482
2000
  )
483
- if not self._client.health():
484
- logger.warning("OpenViking server at %s is not reachable", self._endpoint)
2001
+ health_state, health_message = _classify_runtime_openviking_health(self._client, self._endpoint)
2002
+ if health_state == "unreachable":
2003
+ self._handle_runtime_openviking_unreachable(
2004
+ status_callback=status_callback,
2005
+ warning_callback=warning_callback,
2006
+ )
2007
+ elif health_state != "healthy":
2008
+ _emit_runtime_warning(
2009
+ f"{health_message} OpenViking memory disabled for this Hermes run.",
2010
+ warning_callback,
2011
+ )
485
2012
  self._client = None
486
2013
  except ImportError:
487
2014
  logger.warning("httpx not installed — OpenViking plugin disabled")
@@ -531,9 +2058,16 @@ class OpenVikingMemoryProvider(MemoryProvider):
531
2058
 
532
2059
  def queue_prefetch(self, query: str, *, session_id: str = "") -> None:
533
2060
  """Fire a background search to pre-load relevant context."""
2061
+ query = _derive_openviking_user_text(query)
534
2062
  if not self._client or not query:
535
2063
  return
536
2064
 
2065
+ # Drop prefetch results from older switch generations.
2066
+ with self._prefetch_lock:
2067
+ gen = self._prefetch_generation
2068
+
2069
+ holder: List[threading.Thread] = []
2070
+
537
2071
  def _run():
538
2072
  try:
539
2073
  client = _VikingClient(
@@ -542,7 +2076,7 @@ class OpenVikingMemoryProvider(MemoryProvider):
542
2076
  )
543
2077
  resp = client.post("/api/v1/search/find", {
544
2078
  "query": query,
545
- "top_k": 5,
2079
+ "limit": 5,
546
2080
  })
547
2081
  result = resp.get("result", {})
548
2082
  parts = []
@@ -556,51 +2090,594 @@ class OpenVikingMemoryProvider(MemoryProvider):
556
2090
  parts.append(f"- [{score:.2f}] {abstract} ({uri})")
557
2091
  if parts:
558
2092
  with self._prefetch_lock:
2093
+ if gen != self._prefetch_generation:
2094
+ return
559
2095
  self._prefetch_result = "\n".join(parts)
560
2096
  except Exception as e:
561
2097
  logger.debug("OpenViking prefetch failed: %s", e)
2098
+ finally:
2099
+ with self._prefetch_lock:
2100
+ if holder:
2101
+ self._prefetch_threads.discard(holder[0])
562
2102
 
563
- self._prefetch_thread = threading.Thread(
2103
+ thread = threading.Thread(
564
2104
  target=_run, daemon=True, name="openviking-prefetch"
565
2105
  )
566
- self._prefetch_thread.start()
2106
+ holder.append(thread)
2107
+ with self._prefetch_lock:
2108
+ self._prefetch_thread = thread
2109
+ self._prefetch_threads.add(thread)
2110
+ thread.start()
2111
+
2112
+ def _spawn_writer(self, sid: str, target: Callable[[], None], name: str) -> None:
2113
+ """Spawn a daemon writer tracked in _inflight_writers[sid].
2114
+
2115
+ Tracking is keyed by sid (not by a single latest-thread slot) so that
2116
+ on_session_end / on_session_switch can drain every still-alive writer
2117
+ for the session being committed.
2118
+ """
2119
+ holder: List[threading.Thread] = []
2120
+
2121
+ def _wrapped():
2122
+ try:
2123
+ target()
2124
+ finally:
2125
+ with self._inflight_lock:
2126
+ workers = self._inflight_writers.get(sid)
2127
+ if workers is not None:
2128
+ workers.discard(holder[0])
2129
+ if not workers:
2130
+ self._inflight_writers.pop(sid, None)
2131
+
2132
+ thread = threading.Thread(target=_wrapped, daemon=True, name=name)
2133
+ holder.append(thread)
2134
+ with self._inflight_lock:
2135
+ self._inflight_writers.setdefault(sid, set()).add(thread)
2136
+ thread.start()
2137
+
2138
+ def _drain_finalizers(self, timeout: float) -> bool:
2139
+ """Join every in-flight async session finalizer within a timeout.
2140
+
2141
+ The switch-path commit runs on a daemon finalizer thread so it never
2142
+ blocks the caller's command thread; this lets shutdown and tests wait
2143
+ for those commits deterministically. Returns True if all drained.
2144
+ """
2145
+ deadline = time.monotonic() + timeout
2146
+ while True:
2147
+ with self._deferred_commit_lock:
2148
+ workers = [t for t in self._deferred_commit_threads if t.is_alive()]
2149
+ if not workers:
2150
+ return True
2151
+ remaining = deadline - time.monotonic()
2152
+ if remaining <= 0:
2153
+ return False
2154
+ for t in workers:
2155
+ slice_left = deadline - time.monotonic()
2156
+ if slice_left <= 0:
2157
+ break
2158
+ # Floor the per-join wait so a thread whose join() returns
2159
+ # instantly while still reporting alive can't hot-spin this loop.
2160
+ t.join(timeout=min(slice_left, 0.05))
2161
+
2162
+ def _drain_writers(self, sid: str, timeout: float) -> bool:
2163
+ """Join every in-flight writer for sid within a shared timeout budget.
2164
+
2165
+ Returns True if all writers drained, False if any are still alive when
2166
+ the budget runs out. Callers use the False return to skip the commit.
2167
+ """
2168
+ if not sid:
2169
+ return True
2170
+ deadline = time.monotonic() + timeout
2171
+ while True:
2172
+ with self._inflight_lock:
2173
+ workers = [t for t in self._inflight_writers.get(sid, ()) if t.is_alive()]
2174
+ if not workers:
2175
+ return True
2176
+ remaining = deadline - time.monotonic()
2177
+ if remaining <= 0:
2178
+ return False
2179
+ for t in workers:
2180
+ slice_left = deadline - time.monotonic()
2181
+ if slice_left <= 0:
2182
+ break
2183
+ t.join(timeout=slice_left)
2184
+
2185
+ def _new_client(self) -> _VikingClient:
2186
+ return _VikingClient(
2187
+ self._endpoint,
2188
+ self._api_key,
2189
+ account=self._account,
2190
+ user=self._user,
2191
+ agent=self._agent,
2192
+ )
2193
+
2194
+ @staticmethod
2195
+ def _text_part(content: str) -> Dict[str, str]:
2196
+ return {"type": "text", "text": content}
2197
+
2198
+ def _turn_batch_payload(self, user_content: str, assistant_content: str) -> Dict[str, Any]:
2199
+ assistant_message: Dict[str, Any] = {
2200
+ "role": "assistant",
2201
+ "parts": [self._text_part(assistant_content)],
2202
+ }
2203
+ if self._agent:
2204
+ assistant_message["peer_id"] = self._agent
2205
+ return {
2206
+ "messages": [
2207
+ {"role": "user", "parts": [self._text_part(user_content)]},
2208
+ assistant_message,
2209
+ ]
2210
+ }
2211
+
2212
+ def _post_session_turn(
2213
+ self,
2214
+ client: _VikingClient,
2215
+ sid: str,
2216
+ user_content: str,
2217
+ assistant_content: str,
2218
+ ) -> None:
2219
+ client.post(
2220
+ f"/api/v1/sessions/{sid}/messages/batch",
2221
+ self._turn_batch_payload(user_content, assistant_content),
2222
+ )
2223
+
2224
+ def _session_has_pending_tokens(self, sid: str) -> bool:
2225
+ try:
2226
+ response = self._client.get(f"/api/v1/sessions/{sid}")
2227
+ except Exception:
2228
+ return False
2229
+ session = self._unwrap_result(response)
2230
+ if not isinstance(session, dict):
2231
+ return False
2232
+ try:
2233
+ return int(session.get("pending_tokens") or 0) > 0
2234
+ except (TypeError, ValueError):
2235
+ return False
2236
+
2237
+ def _has_committed_session(self, sid: str) -> bool:
2238
+ with self._committed_session_lock:
2239
+ return sid in self._committed_session_ids
2240
+
2241
+ def _mark_session_committed(self, sid: str) -> None:
2242
+ with self._committed_session_lock:
2243
+ self._committed_session_ids.add(sid)
2244
+
2245
+ def _session_needs_commit(self, sid: str, turn_count: int) -> bool:
2246
+ # Already-committed sessions never need a second commit, regardless of
2247
+ # the turn counter — a racing sync_turn can re-increment _turn_count
2248
+ # after a commit+reset, so the committed-guard must win over turn_count.
2249
+ if self._has_committed_session(sid):
2250
+ return False
2251
+ if turn_count > 0:
2252
+ return True
2253
+ return self._session_has_pending_tokens(sid)
2254
+
2255
+ def _commit_session(self, sid: str, turn_count: int, *, context: str) -> bool:
2256
+ try:
2257
+ self._client.post(
2258
+ f"/api/v1/sessions/{sid}/commit",
2259
+ {"keep_recent_count": 0},
2260
+ )
2261
+ self._mark_session_committed(sid)
2262
+ logger.info("OpenViking session %s committed %s (%d turns)", sid, context, turn_count)
2263
+ return True
2264
+ except Exception as e:
2265
+ logger.warning("OpenViking session commit failed for %s: %s", sid, e)
2266
+ return False
2267
+
2268
+ def _finalize_session_async(self, sid: str, turn_count: int, *, context: str) -> None:
2269
+ """Drain the old session's writers and commit it on a daemon thread.
2270
+
2271
+ Used by on_session_switch (and the deferred-commit fallback) so the
2272
+ potentially-multi-second drain + pending-token GET + commit POST never
2273
+ runs on the caller's command thread. Deduped by sid so a rapid second
2274
+ switch can't stack two finalizers for the same session, and a no-op
2275
+ once shutdown has begun so we don't POST against a torn-down client.
2276
+ """
2277
+ if not sid:
2278
+ return
2279
+ with self._deferred_commit_lock:
2280
+ if self._shutting_down or sid in self._deferred_commit_sids:
2281
+ return
2282
+ self._deferred_commit_sids.add(sid)
2283
+
2284
+ holder: List[threading.Thread] = []
2285
+
2286
+ def _finalize() -> None:
2287
+ try:
2288
+ if self._shutting_down:
2289
+ return
2290
+ if not self._drain_writers(sid, timeout=_DEFERRED_COMMIT_TIMEOUT):
2291
+ logger.warning(
2292
+ "OpenViking writer for %s still alive after drain — "
2293
+ "leaving session uncommitted",
2294
+ sid,
2295
+ )
2296
+ return
2297
+ if self._shutting_down:
2298
+ return
2299
+ if self._session_needs_commit(sid, turn_count):
2300
+ self._commit_session(sid, turn_count, context=context)
2301
+ finally:
2302
+ with self._deferred_commit_lock:
2303
+ self._deferred_commit_sids.discard(sid)
2304
+ if holder:
2305
+ self._deferred_commit_threads.discard(holder[0])
2306
+
2307
+ thread = threading.Thread(
2308
+ target=_finalize,
2309
+ daemon=True,
2310
+ name=f"openviking-finalize-{sid}",
2311
+ )
2312
+ holder.append(thread)
2313
+ with self._deferred_commit_lock:
2314
+ self._deferred_commit_threads.add(thread)
2315
+ thread.start()
2316
+
2317
+ def _invalidate_prefetch_state(self) -> None:
2318
+ # Bump the generation under the same lock used by prefetch workers so
2319
+ # late results from an older session are discarded deterministically.
2320
+ with self._prefetch_lock:
2321
+ self._prefetch_generation += 1
2322
+ self._prefetch_result = ""
2323
+ # Join EVERY tracked prefetch thread, not just the latest slot — a
2324
+ # rapid re-queue can leave an older thread for the abandoned session
2325
+ # still running (consistent with shutdown()).
2326
+ workers = [t for t in self._prefetch_threads if t.is_alive()]
2327
+ for t in workers:
2328
+ t.join(timeout=3.0)
2329
+ with self._prefetch_lock:
2330
+ self._prefetch_result = ""
2331
+
2332
+ @staticmethod
2333
+ def _message_text(content: Any) -> str:
2334
+ """Extract text from OpenAI-style string/list content."""
2335
+ return flatten_message_text(content)
2336
+
2337
+ @classmethod
2338
+ def _message_matches_text(cls, message: Dict[str, Any], expected: Any) -> bool:
2339
+ expected_text = cls._message_text(expected).strip()
2340
+ if not expected_text:
2341
+ return False
2342
+ actual_text = cls._message_text(message.get("content")).strip()
2343
+ return actual_text == expected_text
2344
+
2345
+ @classmethod
2346
+ def _extract_current_turn_messages(
2347
+ cls,
2348
+ messages: Optional[List[Dict[str, Any]]],
2349
+ user_content: str,
2350
+ assistant_content: str,
2351
+ ) -> List[Dict[str, Any]]:
2352
+ """Slice the completed turn out of Hermes' full canonical transcript."""
2353
+ if not messages:
2354
+ return []
2355
+
2356
+ end_idx: Optional[int] = None
2357
+ if cls._message_text(assistant_content).strip():
2358
+ for idx in range(len(messages) - 1, -1, -1):
2359
+ message = messages[idx]
2360
+ if (
2361
+ isinstance(message, dict)
2362
+ and message.get("role") == "assistant"
2363
+ and cls._message_matches_text(message, assistant_content)
2364
+ ):
2365
+ end_idx = idx
2366
+ break
2367
+ if end_idx is None:
2368
+ for idx in range(len(messages) - 1, -1, -1):
2369
+ message = messages[idx]
2370
+ if isinstance(message, dict) and message.get("role") == "assistant":
2371
+ end_idx = idx
2372
+ break
2373
+ if end_idx is None:
2374
+ end_idx = len(messages) - 1
2375
+
2376
+ start_idx: Optional[int] = None
2377
+ if cls._message_text(user_content).strip():
2378
+ for idx in range(end_idx, -1, -1):
2379
+ message = messages[idx]
2380
+ if (
2381
+ isinstance(message, dict)
2382
+ and message.get("role") == "user"
2383
+ and cls._message_matches_text(message, user_content)
2384
+ ):
2385
+ start_idx = idx
2386
+ break
2387
+ if start_idx is None:
2388
+ for idx in range(end_idx, -1, -1):
2389
+ message = messages[idx]
2390
+ if isinstance(message, dict) and message.get("role") == "user":
2391
+ start_idx = idx
2392
+ break
2393
+ if start_idx is None:
2394
+ return []
2395
+
2396
+ return [message for message in messages[start_idx : end_idx + 1] if isinstance(message, dict)]
2397
+
2398
+ @staticmethod
2399
+ def _tool_call_id(tool_call: Dict[str, Any]) -> str:
2400
+ return str(tool_call.get("id") or tool_call.get("tool_call_id") or "")
2401
+
2402
+ @staticmethod
2403
+ def _tool_call_name(tool_call: Dict[str, Any]) -> str:
2404
+ function = tool_call.get("function")
2405
+ if isinstance(function, dict):
2406
+ return str(function.get("name") or "")
2407
+ return str(tool_call.get("name") or "")
2408
+
2409
+ @staticmethod
2410
+ def _is_openviking_recall_tool_name(tool_name: Any) -> bool:
2411
+ return str(tool_name or "").strip().lower() in _OPENVIKING_RECALL_TOOL_NAMES
2412
+
2413
+ @staticmethod
2414
+ def _tool_call_input(tool_call: Dict[str, Any]) -> Dict[str, Any]:
2415
+ function = tool_call.get("function")
2416
+ raw_args: Any = None
2417
+ if isinstance(function, dict):
2418
+ raw_args = function.get("arguments")
2419
+ if raw_args is None:
2420
+ raw_args = tool_call.get("args")
2421
+ if raw_args is None:
2422
+ return {}
2423
+ if isinstance(raw_args, dict):
2424
+ return raw_args
2425
+ if isinstance(raw_args, str):
2426
+ if not raw_args.strip():
2427
+ return {}
2428
+ try:
2429
+ parsed = json.loads(raw_args)
2430
+ except Exception:
2431
+ return {"value": raw_args}
2432
+ if isinstance(parsed, dict):
2433
+ return parsed
2434
+ return {"value": parsed}
2435
+ return {"value": raw_args}
2436
+
2437
+ @classmethod
2438
+ def _tool_result_status(cls, message: Dict[str, Any]) -> str:
2439
+ raw_status = str(message.get("status") or message.get("tool_status") or "").lower()
2440
+ if raw_status in _TOOL_STATUS_ERROR_ALIASES:
2441
+ return _TOOL_STATUS_ERROR
2442
+ if raw_status in _TOOL_STATUS_COMPLETED_ALIASES:
2443
+ return _TOOL_STATUS_COMPLETED
2444
+
2445
+ text = cls._message_text(message.get("content")).strip()
2446
+ if text:
2447
+ try:
2448
+ parsed = json.loads(text)
2449
+ except Exception:
2450
+ parsed = None
2451
+ if isinstance(parsed, dict):
2452
+ status = str(parsed.get("status") or "").lower()
2453
+ exit_code = parsed.get("exit_code")
2454
+ if (
2455
+ status in _TOOL_STATUS_ERROR_ALIASES
2456
+ or parsed.get("success") is False
2457
+ or bool(parsed.get("error"))
2458
+ or (isinstance(exit_code, int) and exit_code != 0)
2459
+ ):
2460
+ return _TOOL_STATUS_ERROR
2461
+
2462
+ return _TOOL_STATUS_COMPLETED
2463
+
2464
+ @classmethod
2465
+ def _messages_to_openviking_batch(
2466
+ cls,
2467
+ messages: List[Dict[str, Any]],
2468
+ *,
2469
+ assistant_peer_id: str = "",
2470
+ ) -> List[Dict[str, Any]]:
2471
+ """Convert Hermes canonical messages into OpenViking batch payloads."""
2472
+ assistant_peer_id = str(assistant_peer_id or "").strip()
2473
+ tool_calls_by_id: Dict[str, Dict[str, Any]] = {}
2474
+ completed_tool_ids: set[str] = set()
2475
+ skipped_tool_ids: set[str] = set()
2476
+ for message in messages:
2477
+ if not isinstance(message, dict):
2478
+ continue
2479
+ if message.get("role") == "tool":
2480
+ tool_id = str(message.get("tool_call_id") or message.get("id") or "")
2481
+ if tool_id:
2482
+ completed_tool_ids.add(tool_id)
2483
+ if cls._is_openviking_recall_tool_name(message.get("name")):
2484
+ skipped_tool_ids.add(tool_id)
2485
+ continue
2486
+ if message.get("role") != "assistant":
2487
+ continue
2488
+ for tool_call in message.get("tool_calls") or []:
2489
+ if not isinstance(tool_call, dict):
2490
+ continue
2491
+ tool_id = cls._tool_call_id(tool_call)
2492
+ tool_name = cls._tool_call_name(tool_call)
2493
+ if tool_id:
2494
+ tool_calls_by_id[tool_id] = {
2495
+ "tool_name": tool_name,
2496
+ "tool_input": cls._tool_call_input(tool_call),
2497
+ }
2498
+ if cls._is_openviking_recall_tool_name(tool_name):
2499
+ skipped_tool_ids.add(tool_id)
2500
+
2501
+ payload_messages: List[Dict[str, Any]] = []
2502
+ pending_tool_parts: List[Dict[str, Any]] = []
2503
+
2504
+ def payload_message(role: str, parts: List[Dict[str, Any]]) -> Dict[str, Any]:
2505
+ payload: Dict[str, Any] = {"role": role, "parts": parts}
2506
+ if role == "assistant" and assistant_peer_id:
2507
+ payload["peer_id"] = assistant_peer_id
2508
+ return payload
2509
+
2510
+ def flush_tool_parts() -> None:
2511
+ nonlocal pending_tool_parts
2512
+ if pending_tool_parts:
2513
+ payload_messages.append(payload_message("assistant", pending_tool_parts))
2514
+ pending_tool_parts = []
2515
+
2516
+ for message in messages:
2517
+ if not isinstance(message, dict):
2518
+ continue
2519
+
2520
+ role = str(message.get("role") or "")
2521
+ if role in {"system", "developer"}:
2522
+ continue
2523
+
2524
+ if role == "tool":
2525
+ tool_id = str(message.get("tool_call_id") or message.get("id") or "")
2526
+ prior_call = tool_calls_by_id.get(tool_id, {})
2527
+ tool_name = str(message.get("name") or prior_call.get("tool_name") or "")
2528
+ if tool_id in skipped_tool_ids or cls._is_openviking_recall_tool_name(tool_name):
2529
+ continue
2530
+ tool_part = {
2531
+ "type": "tool",
2532
+ "tool_id": tool_id,
2533
+ "tool_name": tool_name,
2534
+ "tool_input": prior_call.get("tool_input", {}),
2535
+ "tool_output": cls._message_text(message.get("content")),
2536
+ "tool_status": cls._tool_result_status(message),
2537
+ }
2538
+ pending_tool_parts.append(tool_part)
2539
+ continue
2540
+
2541
+ if role not in {"user", "assistant"}:
2542
+ continue
2543
+
2544
+ flush_tool_parts()
2545
+ parts: List[Dict[str, Any]] = []
2546
+ text = cls._message_text(message.get("content"))
2547
+ if text:
2548
+ parts.append({"type": "text", "text": text})
2549
+
2550
+ if role == "assistant":
2551
+ for tool_call in message.get("tool_calls") or []:
2552
+ if not isinstance(tool_call, dict):
2553
+ continue
2554
+ tool_id = cls._tool_call_id(tool_call)
2555
+ tool_name = cls._tool_call_name(tool_call)
2556
+ if tool_id in skipped_tool_ids or cls._is_openviking_recall_tool_name(tool_name):
2557
+ continue
2558
+ if tool_id in completed_tool_ids:
2559
+ continue
2560
+ # Reuse the tool_input parsed in the pre-scan when available
2561
+ # (non-empty ids are cached); fall back to parsing for the
2562
+ # uncached empty-id case so we never drop arguments.
2563
+ prior_call = tool_calls_by_id.get(tool_id) if tool_id else None
2564
+ tool_input = (
2565
+ prior_call["tool_input"]
2566
+ if prior_call is not None
2567
+ else cls._tool_call_input(tool_call)
2568
+ )
2569
+ parts.append({
2570
+ "type": "tool",
2571
+ "tool_id": tool_id,
2572
+ "tool_name": tool_name,
2573
+ "tool_input": tool_input,
2574
+ "tool_status": _TOOL_STATUS_PENDING,
2575
+ })
2576
+
2577
+ if parts:
2578
+ payload_messages.append(payload_message(role, parts))
2579
+
2580
+ flush_tool_parts()
2581
+ return payload_messages
567
2582
 
568
- def sync_turn(self, user_content: str, assistant_content: str, *, session_id: str = "") -> None:
2583
+ def sync_turn(
2584
+ self,
2585
+ user_content: str,
2586
+ assistant_content: str,
2587
+ *,
2588
+ session_id: str = "",
2589
+ messages: Optional[List[Dict[str, Any]]] = None,
2590
+ ) -> None:
569
2591
  """Record the conversation turn in OpenViking's session (non-blocking)."""
570
2592
  if not self._client:
571
2593
  return
572
2594
 
573
- self._turn_count += 1
2595
+ user_content = _derive_openviking_user_text(user_content)
2596
+ if not user_content:
2597
+ return
2598
+
2599
+ turn_messages = (
2600
+ self._extract_current_turn_messages(messages, user_content, assistant_content)
2601
+ if messages is not None
2602
+ else []
2603
+ )
2604
+ if turn_messages:
2605
+ turn_messages = [dict(message) for message in turn_messages]
2606
+ for message in turn_messages:
2607
+ if message.get("role") == "user":
2608
+ message["content"] = user_content
2609
+ break
2610
+ batch_messages = self._messages_to_openviking_batch(
2611
+ turn_messages,
2612
+ assistant_peer_id=getattr(self, "_agent", _DEFAULT_AGENT),
2613
+ )
2614
+
2615
+ if _sync_trace_enabled():
2616
+ logger.info(
2617
+ "OpenViking sync_turn trace: session_arg=%r cached_session=%r "
2618
+ "messages_param_supported=true messages_present=%s message_count=%s "
2619
+ "turn_message_count=%d batch_message_count=%d user_len=%d assistant_len=%d "
2620
+ "user_preview=%r assistant_preview=%r",
2621
+ session_id,
2622
+ self._session_id,
2623
+ messages is not None,
2624
+ len(messages) if messages is not None else None,
2625
+ len(turn_messages),
2626
+ len(batch_messages),
2627
+ len(str(user_content or "")),
2628
+ len(str(assistant_content or "")),
2629
+ _preview(user_content),
2630
+ _preview(assistant_content),
2631
+ )
2632
+
2633
+ # Snapshot the sid and bump the turn counter atomically so a
2634
+ # concurrent on_session_switch/on_session_end can't interleave its
2635
+ # snapshot+reset between the read and the increment (lost turn) and so
2636
+ # the turn is unambiguously attributed to the session it targets.
2637
+ with self._session_state_lock:
2638
+ sid = str(session_id or self._session_id).strip()
2639
+ if not sid:
2640
+ return
2641
+ self._turn_count += 1
574
2642
 
575
2643
  def _sync():
576
- try:
577
- client = _VikingClient(
578
- self._endpoint, self._api_key,
579
- account=self._account, user=self._user, agent=self._agent,
2644
+ def _post_turn(client: _VikingClient) -> None:
2645
+ if batch_messages:
2646
+ payload = {"messages": batch_messages}
2647
+ if _sync_trace_enabled():
2648
+ logger.info(
2649
+ "OpenViking sync_turn trace: POST /api/v1/sessions/%s/messages/batch payload=%s",
2650
+ sid,
2651
+ json.dumps(payload, ensure_ascii=False),
2652
+ )
2653
+ try:
2654
+ client.post(f"/api/v1/sessions/{sid}/messages/batch", payload)
2655
+ return
2656
+ except Exception as batch_error:
2657
+ logger.warning(
2658
+ "OpenViking structured sync failed; falling back to text sync: %s",
2659
+ batch_error,
2660
+ )
2661
+
2662
+ self._post_session_turn(
2663
+ client,
2664
+ sid,
2665
+ user_content[:4000],
2666
+ self._message_text(assistant_content)[:4000],
580
2667
  )
581
- sid = self._session_id
582
2668
 
583
- # Add user message
584
- client.post(f"/api/v1/sessions/{sid}/messages", {
585
- "role": "user",
586
- "content": user_content[:4000], # trim very long messages
587
- })
588
- # Add assistant message
589
- client.post(f"/api/v1/sessions/{sid}/messages", {
590
- "role": "assistant",
591
- "content": assistant_content[:4000],
592
- })
2669
+ try:
2670
+ client = self._new_client()
2671
+ _post_turn(client)
593
2672
  except Exception as e:
594
- logger.debug("OpenViking sync_turn failed: %s", e)
595
-
596
- # Wait for any previous sync to finish before starting a new one
597
- if self._sync_thread and self._sync_thread.is_alive():
598
- self._sync_thread.join(timeout=5.0)
2673
+ logger.debug("OpenViking sync_turn failed, reconnecting: %s", e)
2674
+ try:
2675
+ client = self._new_client()
2676
+ _post_turn(client)
2677
+ except Exception as retry_error:
2678
+ logger.warning("OpenViking sync_turn failed: %s", retry_error)
599
2679
 
600
- self._sync_thread = threading.Thread(
601
- target=_sync, daemon=True, name="openviking-sync"
602
- )
603
- self._sync_thread.start()
2680
+ self._spawn_writer(sid, _sync, name="openviking-sync")
604
2681
 
605
2682
  def on_session_end(self, messages: List[Dict[str, Any]]) -> None:
606
2683
  """Commit the session to trigger memory extraction.
@@ -611,25 +2688,103 @@ class OpenVikingMemoryProvider(MemoryProvider):
611
2688
  if not self._client:
612
2689
  return
613
2690
 
614
- # Wait for any pending sync to finish first do this before the
615
- # turn_count check so the last turn's messages are flushed even if
616
- # the count hasn't been incremented yet.
617
- if self._sync_thread and self._sync_thread.is_alive():
618
- self._sync_thread.join(timeout=10.0)
2691
+ # Snapshot sid + turn count atomically against a concurrent sync_turn
2692
+ # increment. on_session_end runs at teardown so the drain+commit stays
2693
+ # synchronous here (we want it to land before the process exits), but
2694
+ # the counter read must still be consistent.
2695
+ with self._session_state_lock:
2696
+ sid = self._session_id
2697
+ turn_count = self._turn_count
2698
+
2699
+ # Commit only after session writes drain.
2700
+ if not self._drain_writers(sid, timeout=_SESSION_DRAIN_TIMEOUT):
2701
+ logger.warning(
2702
+ "OpenViking writer for %s still alive after drain — skipping commit",
2703
+ sid,
2704
+ )
2705
+ return
619
2706
 
620
- if self._turn_count == 0:
2707
+ if not self._session_needs_commit(sid, turn_count):
621
2708
  return
622
2709
 
623
- try:
624
- self._client.post(f"/api/v1/sessions/{self._session_id}/commit")
625
- logger.info("OpenViking session %s committed (%d turns)", self._session_id, self._turn_count)
626
- except Exception as e:
627
- logger.warning("OpenViking session commit failed: %s", e)
2710
+ if self._commit_session(sid, turn_count, context="on session end"):
2711
+ # Mark clean so a follow-up on_session_switch skips its own commit.
2712
+ with self._session_state_lock:
2713
+ if self._session_id == sid:
2714
+ self._turn_count = 0
2715
+
2716
+ def on_session_switch(
2717
+ self,
2718
+ new_session_id: str,
2719
+ *,
2720
+ parent_session_id: str = "",
2721
+ reset: bool = False,
2722
+ **kwargs,
2723
+ ) -> None:
2724
+ """Commit the old session and rotate cached state to the new session_id.
2725
+
2726
+ Fires on /resume, /branch, /reset, /new, and context compression.
2727
+ Without this hook, ``_session_id`` stays stuck at the value
2728
+ ``initialize()`` cached, so subsequent ``sync_turn()`` writes land in
2729
+ the already-closed old session and ``on_session_end()`` tries to
2730
+ commit it a second time. The new session never accumulates messages,
2731
+ and memory extraction never fires for it. See hermes-agent#28296.
2732
+
2733
+ Flushes any in-flight sync under the old session_id, commits the old
2734
+ session if it has pending turns (same extraction semantics as
2735
+ ``on_session_end``), drains and clears any stale prefetch result,
2736
+ then rotates ``_session_id`` and resets ``_turn_count``.
2737
+ """
2738
+ new_id = str(new_session_id or "").strip()
2739
+ if not new_id or not self._client:
2740
+ return
2741
+
2742
+ rewound = bool(kwargs.get("rewound"))
2743
+
2744
+ # Rotate cached session state synchronously (cheap, in-memory) and
2745
+ # snapshot the old session under the lock so a concurrent sync_turn
2746
+ # either lands fully before the rotation (counted under old) or fully
2747
+ # after (counted under new) — never split. The OLD session's commit
2748
+ # (drain + pending-token GET + commit POST, potentially many seconds)
2749
+ # is then offloaded so /new, /branch, /resume, /undo never block the
2750
+ # caller's command thread (cf. the end-of-turn-sync offload in #41945).
2751
+ with self._session_state_lock:
2752
+ old_session_id = self._session_id
2753
+ old_turn_count = self._turn_count
2754
+ rotate = not (rewound or new_id == old_session_id)
2755
+ if rotate:
2756
+ self._session_id = new_id
2757
+ self._turn_count = 0
2758
+
2759
+ # Invalidate stale prefetch OUTSIDE the session lock — it takes its own
2760
+ # _prefetch_lock and may join a prefetch thread for up to 3s, which we
2761
+ # must not do while holding the session lock (would block sync_turn and
2762
+ # risk lock-ordering coupling).
2763
+ self._invalidate_prefetch_state()
2764
+
2765
+ if not rotate:
2766
+ # Same-session rewind (/undo) or no-op rotation: no commit, no
2767
+ # counter reset — just the prefetch invalidation above.
2768
+ logger.debug(
2769
+ "OpenViking on_session_switch invalidated state without rotation: "
2770
+ "session=%s rewound=%s",
2771
+ old_session_id, rewound,
2772
+ )
2773
+ return
2774
+
2775
+ # Drain + commit the OLD session off the command thread.
2776
+ if old_session_id:
2777
+ self._finalize_session_async(old_session_id, old_turn_count, context="on switch")
2778
+
2779
+ logger.debug(
2780
+ "OpenViking on_session_switch: old=%s new=%s parent=%s reset=%s",
2781
+ old_session_id, new_id, parent_session_id, reset,
2782
+ )
628
2783
 
629
2784
  def _build_memory_uri(self, subdir: str) -> str:
630
- """Build a viking:// memory URI under the configured user/subdir."""
2785
+ """Build a viking:// memory URI under the configured peer namespace."""
631
2786
  slug = uuid.uuid4().hex[:12]
632
- return f"viking://user/{self._user}/memories/{subdir}/mem_{slug}.md"
2787
+ return f"viking://user/peers/{self._agent}/memories/{subdir}/mem_{slug}.md"
633
2788
 
634
2789
  def on_memory_write(
635
2790
  self,
@@ -685,11 +2840,28 @@ class OpenVikingMemoryProvider(MemoryProvider):
685
2840
  return tool_error(str(e))
686
2841
 
687
2842
  def shutdown(self) -> None:
688
- # Wait for background threads to finish
689
- for t in (self._sync_thread, self._prefetch_thread):
690
- if t and t.is_alive():
2843
+ # Stop deferred finalizers from issuing new commits against a
2844
+ # torn-down client, then drain everything still in flight.
2845
+ self._shutting_down = True
2846
+ # Wait for every in-flight writer across all tracked sessions.
2847
+ with self._inflight_lock:
2848
+ all_workers = [
2849
+ t for workers in self._inflight_writers.values() for t in workers
2850
+ ]
2851
+ with self._deferred_commit_lock:
2852
+ deferred_workers = list(self._deferred_commit_threads)
2853
+ with self._prefetch_lock:
2854
+ prefetch_workers = list(self._prefetch_threads)
2855
+ for t in all_workers:
2856
+ if t.is_alive():
2857
+ t.join(timeout=5.0)
2858
+ for t in deferred_workers:
2859
+ if t.is_alive():
691
2860
  t.join(timeout=5.0)
692
- # Clear atexit reference so it doesn't double-commit
2861
+ for t in prefetch_workers:
2862
+ if t.is_alive():
2863
+ t.join(timeout=5.0)
2864
+ # Clear atexit reference so it doesn't double-commit.
693
2865
  global _last_active_provider
694
2866
  if _last_active_provider is self:
695
2867
  _last_active_provider = None
@@ -743,14 +2915,16 @@ class OpenVikingMemoryProvider(MemoryProvider):
743
2915
 
744
2916
  payload: Dict[str, Any] = {"query": query}
745
2917
  mode = args.get("mode", "auto")
746
- if mode != "auto":
747
- payload["mode"] = mode
748
2918
  if args.get("scope"):
749
2919
  payload["target_uri"] = args["scope"]
750
2920
  if args.get("limit"):
751
- payload["top_k"] = args["limit"]
2921
+ payload["limit"] = args["limit"]
2922
+
2923
+ endpoint = "/api/v1/search/search" if mode == "deep" else "/api/v1/search/find"
2924
+ if endpoint == "/api/v1/search/search" and self._session_id:
2925
+ payload["session_id"] = self._session_id
752
2926
 
753
- resp = self._client.post("/api/v1/search/find", payload)
2927
+ resp = self._client.post(endpoint, payload)
754
2928
  result = resp.get("result", {})
755
2929
 
756
2930
  # Format results for the model — keep it concise