@bitseek/hermes-webui 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/README.md +213 -0
  2. package/bin/hermes-webui.mjs +588 -0
  3. package/package.json +25 -0
  4. package/scripts/sync-vendor.mjs +74 -0
  5. package/templates/launchd/com.bitseek.hermes-webui.plist +21 -0
  6. package/templates/systemd/hermes-webui.service +13 -0
  7. package/templates/windows/hermes-webui-task.ps1 +3 -0
  8. package/vendor/agent-frontend-shell/.bitseek-source.json +6 -0
  9. package/vendor/agent-frontend-shell/.dockerignore +7 -0
  10. package/vendor/agent-frontend-shell/.env.docker.example +89 -0
  11. package/vendor/agent-frontend-shell/.env.example +34 -0
  12. package/vendor/agent-frontend-shell/.github/FUNDING.yml +3 -0
  13. package/vendor/agent-frontend-shell/.github/workflows/browser-smoke.yml +42 -0
  14. package/vendor/agent-frontend-shell/.github/workflows/docker-smoke.yml +233 -0
  15. package/vendor/agent-frontend-shell/.github/workflows/native-windows-startup.yml +132 -0
  16. package/vendor/agent-frontend-shell/.github/workflows/release.yml +57 -0
  17. package/vendor/agent-frontend-shell/.github/workflows/tests.yml +88 -0
  18. package/vendor/agent-frontend-shell/.vscode/launch.json +59 -0
  19. package/vendor/agent-frontend-shell/.vscode/settings.json +13 -0
  20. package/vendor/agent-frontend-shell/AGENTS.md +80 -0
  21. package/vendor/agent-frontend-shell/ARCHITECTURE.md +1658 -0
  22. package/vendor/agent-frontend-shell/BUGS.md +52 -0
  23. package/vendor/agent-frontend-shell/CHANGELOG.md +7295 -0
  24. package/vendor/agent-frontend-shell/CONTRIBUTING.md +205 -0
  25. package/vendor/agent-frontend-shell/CONTRIBUTORS.md +107 -0
  26. package/vendor/agent-frontend-shell/DESIGN.md +173 -0
  27. package/vendor/agent-frontend-shell/Dockerfile +91 -0
  28. package/vendor/agent-frontend-shell/LICENSE +21 -0
  29. package/vendor/agent-frontend-shell/README-CUSTOM.md +76 -0
  30. package/vendor/agent-frontend-shell/README.md +705 -0
  31. package/vendor/agent-frontend-shell/ROADMAP.md +351 -0
  32. package/vendor/agent-frontend-shell/SPRINTS.md +147 -0
  33. package/vendor/agent-frontend-shell/TESTING.md +1932 -0
  34. package/vendor/agent-frontend-shell/THEMES.md +170 -0
  35. package/vendor/agent-frontend-shell/api/__init__.py +1 -0
  36. package/vendor/agent-frontend-shell/api/agent_health.py +392 -0
  37. package/vendor/agent-frontend-shell/api/agent_sessions.py +782 -0
  38. package/vendor/agent-frontend-shell/api/auth.py +592 -0
  39. package/vendor/agent-frontend-shell/api/background.py +87 -0
  40. package/vendor/agent-frontend-shell/api/clarify.py +238 -0
  41. package/vendor/agent-frontend-shell/api/commands.py +124 -0
  42. package/vendor/agent-frontend-shell/api/compression_anchor.py +134 -0
  43. package/vendor/agent-frontend-shell/api/config.py +5178 -0
  44. package/vendor/agent-frontend-shell/api/dashboard_probe.py +255 -0
  45. package/vendor/agent-frontend-shell/api/extensions.py +253 -0
  46. package/vendor/agent-frontend-shell/api/gateway_chat.py +435 -0
  47. package/vendor/agent-frontend-shell/api/gateway_watcher.py +230 -0
  48. package/vendor/agent-frontend-shell/api/goals.py +608 -0
  49. package/vendor/agent-frontend-shell/api/helpers.py +474 -0
  50. package/vendor/agent-frontend-shell/api/kanban_bridge.py +1255 -0
  51. package/vendor/agent-frontend-shell/api/metering.py +194 -0
  52. package/vendor/agent-frontend-shell/api/models.py +4210 -0
  53. package/vendor/agent-frontend-shell/api/oauth.py +770 -0
  54. package/vendor/agent-frontend-shell/api/onboarding.py +1046 -0
  55. package/vendor/agent-frontend-shell/api/passkeys.py +365 -0
  56. package/vendor/agent-frontend-shell/api/profiles.py +1499 -0
  57. package/vendor/agent-frontend-shell/api/providers.py +2175 -0
  58. package/vendor/agent-frontend-shell/api/request_diagnostics.py +160 -0
  59. package/vendor/agent-frontend-shell/api/rollback.py +320 -0
  60. package/vendor/agent-frontend-shell/api/routes.py +13990 -0
  61. package/vendor/agent-frontend-shell/api/run_journal.py +284 -0
  62. package/vendor/agent-frontend-shell/api/runner_client.py +156 -0
  63. package/vendor/agent-frontend-shell/api/runtime_adapter.py +431 -0
  64. package/vendor/agent-frontend-shell/api/session_discoverability.py +640 -0
  65. package/vendor/agent-frontend-shell/api/session_events.py +45 -0
  66. package/vendor/agent-frontend-shell/api/session_lifecycle.py +208 -0
  67. package/vendor/agent-frontend-shell/api/session_ops.py +207 -0
  68. package/vendor/agent-frontend-shell/api/session_recovery.py +655 -0
  69. package/vendor/agent-frontend-shell/api/skill_usage.py +32 -0
  70. package/vendor/agent-frontend-shell/api/startup.py +128 -0
  71. package/vendor/agent-frontend-shell/api/state_sync.py +187 -0
  72. package/vendor/agent-frontend-shell/api/streaming.py +7048 -0
  73. package/vendor/agent-frontend-shell/api/system_health.py +167 -0
  74. package/vendor/agent-frontend-shell/api/terminal.py +410 -0
  75. package/vendor/agent-frontend-shell/api/turn_journal.py +214 -0
  76. package/vendor/agent-frontend-shell/api/updates.py +1261 -0
  77. package/vendor/agent-frontend-shell/api/upload.py +322 -0
  78. package/vendor/agent-frontend-shell/api/usage.py +26 -0
  79. package/vendor/agent-frontend-shell/api/workspace.py +867 -0
  80. package/vendor/agent-frontend-shell/api/workspace_git.py +1261 -0
  81. package/vendor/agent-frontend-shell/api/worktrees.py +357 -0
  82. package/vendor/agent-frontend-shell/bootstrap.py +492 -0
  83. package/vendor/agent-frontend-shell/ctl.sh +427 -0
  84. package/vendor/agent-frontend-shell/docker-compose.custom.yml +26 -0
  85. package/vendor/agent-frontend-shell/docker-compose.three-container.yml +168 -0
  86. package/vendor/agent-frontend-shell/docker-compose.two-container.yml +147 -0
  87. package/vendor/agent-frontend-shell/docker-compose.yml +57 -0
  88. package/vendor/agent-frontend-shell/docker_init.bash +459 -0
  89. package/vendor/agent-frontend-shell/docs/CONTRACTS.md +207 -0
  90. package/vendor/agent-frontend-shell/docs/EXTENSIONS.md +212 -0
  91. package/vendor/agent-frontend-shell/docs/ISSUES.md +23 -0
  92. package/vendor/agent-frontend-shell/docs/UIUX-GUIDE.md +196 -0
  93. package/vendor/agent-frontend-shell/docs/advanced-chat-setup.md +83 -0
  94. package/vendor/agent-frontend-shell/docs/docker.md +337 -0
  95. package/vendor/agent-frontend-shell/docs/onboarding-agent-checklist.md +207 -0
  96. package/vendor/agent-frontend-shell/docs/onboarding.md +202 -0
  97. package/vendor/agent-frontend-shell/docs/remote-access.md +75 -0
  98. package/vendor/agent-frontend-shell/docs/rfcs/README.md +53 -0
  99. package/vendor/agent-frontend-shell/docs/rfcs/agent-source-boundary.md +70 -0
  100. package/vendor/agent-frontend-shell/docs/rfcs/canonical-session-resolution.md +124 -0
  101. package/vendor/agent-frontend-shell/docs/rfcs/hermes-run-adapter-contract.md +1079 -0
  102. package/vendor/agent-frontend-shell/docs/rfcs/turn-journal.md +195 -0
  103. package/vendor/agent-frontend-shell/docs/rfcs/webui-run-state-consistency-contract.md +157 -0
  104. package/vendor/agent-frontend-shell/docs/supervisor.md +280 -0
  105. package/vendor/agent-frontend-shell/docs/troubleshooting.md +132 -0
  106. package/vendor/agent-frontend-shell/docs/ui-ux/index.html +863 -0
  107. package/vendor/agent-frontend-shell/docs/ui-ux/two-stage-proposal.html +768 -0
  108. package/vendor/agent-frontend-shell/docs/why-hermes.md +489 -0
  109. package/vendor/agent-frontend-shell/docs/workspace-git.md +92 -0
  110. package/vendor/agent-frontend-shell/docs/wsl-autostart.md +126 -0
  111. package/vendor/agent-frontend-shell/eslint.runtime-guard.config.mjs +35 -0
  112. package/vendor/agent-frontend-shell/extensions/bitseek-design-system.md +330 -0
  113. package/vendor/agent-frontend-shell/extensions/branding/assets/apple-touch-icon.png +0 -0
  114. package/vendor/agent-frontend-shell/extensions/branding/assets/empty-logo.svg +739 -0
  115. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-192.png +0 -0
  116. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-32.png +0 -0
  117. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.png +0 -0
  118. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.svg +745 -0
  119. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.ico +0 -0
  120. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.svg +745 -0
  121. package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v2.svg +751 -0
  122. package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v3.svg +739 -0
  123. package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon.svg +745 -0
  124. package/vendor/agent-frontend-shell/extensions/branding/branding.js +112 -0
  125. package/vendor/agent-frontend-shell/extensions/branding/config.json +14 -0
  126. package/vendor/agent-frontend-shell/extensions/branding/manifest.json +53 -0
  127. package/vendor/agent-frontend-shell/extensions/index.js +67 -0
  128. package/vendor/agent-frontend-shell/extensions/loader/hermes-loader.js +77 -0
  129. package/vendor/agent-frontend-shell/extensions/manifest.json +16 -0
  130. package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.css +333 -0
  131. package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.js +487 -0
  132. package/vendor/agent-frontend-shell/extensions/pages/manifest.json +6 -0
  133. package/vendor/agent-frontend-shell/extensions/pages/registry.css +56 -0
  134. package/vendor/agent-frontend-shell/extensions/pages/registry.js +302 -0
  135. package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.css +93 -0
  136. package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.js +98 -0
  137. package/vendor/agent-frontend-shell/install.sh +63 -0
  138. package/vendor/agent-frontend-shell/mcp_server.py +567 -0
  139. package/vendor/agent-frontend-shell/package.json +12 -0
  140. package/vendor/agent-frontend-shell/pyproject.toml +56 -0
  141. package/vendor/agent-frontend-shell/pytest.ini +3 -0
  142. package/vendor/agent-frontend-shell/requirements.txt +5 -0
  143. package/vendor/agent-frontend-shell/server.py +624 -0
  144. package/vendor/agent-frontend-shell/start.ps1 +210 -0
  145. package/vendor/agent-frontend-shell/start.sh +65 -0
  146. package/vendor/agent-frontend-shell/static/apple-touch-icon.png +0 -0
  147. package/vendor/agent-frontend-shell/static/boot.js +1990 -0
  148. package/vendor/agent-frontend-shell/static/commands.js +1402 -0
  149. package/vendor/agent-frontend-shell/static/favicon-192.png +0 -0
  150. package/vendor/agent-frontend-shell/static/favicon-32.png +0 -0
  151. package/vendor/agent-frontend-shell/static/favicon-512.png +0 -0
  152. package/vendor/agent-frontend-shell/static/favicon-512.svg +18 -0
  153. package/vendor/agent-frontend-shell/static/favicon.ico +0 -0
  154. package/vendor/agent-frontend-shell/static/favicon.svg +20 -0
  155. package/vendor/agent-frontend-shell/static/i18n.js +15389 -0
  156. package/vendor/agent-frontend-shell/static/icons.js +92 -0
  157. package/vendor/agent-frontend-shell/static/index.html +1506 -0
  158. package/vendor/agent-frontend-shell/static/login.js +177 -0
  159. package/vendor/agent-frontend-shell/static/manifest.json +53 -0
  160. package/vendor/agent-frontend-shell/static/messages.js +3521 -0
  161. package/vendor/agent-frontend-shell/static/onboarding.js +800 -0
  162. package/vendor/agent-frontend-shell/static/panels.js +7995 -0
  163. package/vendor/agent-frontend-shell/static/pwa-startup.js +83 -0
  164. package/vendor/agent-frontend-shell/static/sessions.js +5165 -0
  165. package/vendor/agent-frontend-shell/static/style.css +4774 -0
  166. package/vendor/agent-frontend-shell/static/sw.js +173 -0
  167. package/vendor/agent-frontend-shell/static/terminal.js +632 -0
  168. package/vendor/agent-frontend-shell/static/ui.js +8997 -0
  169. package/vendor/agent-frontend-shell/static/vendor/js-yaml/4.1.0/js-yaml.min.js +2 -0
  170. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.ttf +0 -0
  171. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff +0 -0
  172. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  173. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  174. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  175. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  176. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  177. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  178. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  179. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  180. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  181. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  182. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  183. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  184. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  185. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.ttf +0 -0
  186. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff +0 -0
  187. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff2 +0 -0
  188. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  189. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  190. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  191. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.ttf +0 -0
  192. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff +0 -0
  193. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff2 +0 -0
  194. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.ttf +0 -0
  195. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff +0 -0
  196. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff2 +0 -0
  197. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  198. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  199. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  200. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.ttf +0 -0
  201. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff +0 -0
  202. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff2 +0 -0
  203. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  204. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  205. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  206. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  207. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  208. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  209. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  210. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  211. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  212. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.ttf +0 -0
  213. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff +0 -0
  214. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff2 +0 -0
  215. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.ttf +0 -0
  216. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff +0 -0
  217. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  218. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.ttf +0 -0
  219. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff +0 -0
  220. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  221. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.ttf +0 -0
  222. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff +0 -0
  223. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  224. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.ttf +0 -0
  225. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff +0 -0
  226. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  227. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  228. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  229. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  230. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.css +1 -0
  231. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.js +1 -0
  232. package/vendor/agent-frontend-shell/static/vendor/smd.min.js +29 -0
  233. package/vendor/agent-frontend-shell/static/workspace.js +680 -0
@@ -0,0 +1,208 @@
1
+ """
2
+ Hermes WebUI memory-provider session lifecycle.
3
+
4
+ Batch-extraction memory providers (OpenViking, Holographic) only extract memories
5
+ when AIAgent.commit_memory_session() invokes provider on_session_end(). WebUI
6
+ sessions can be reopened and continued many times, so the lifecycle must guarantee:
7
+
8
+ 1. Only completed, non-ephemeral turns are committable.
9
+ 2. A commit finishing late must not erase work completed while it was in flight.
10
+ 3. A failed commit preserves the uncommitted generation and owning agent handle.
11
+ 4. Replacement/reopened agents cannot steal older dirty generations.
12
+ 5. Overlapping commits are serialised via a per-session in-flight guard.
13
+
14
+ CLI-parity semantics — post-turn marking, boundary extraction/commit:
15
+
16
+ - Completed turn: Hermes core still mirrors the exchange through
17
+ run_agent.py::_sync_external_memory_for_turn(), MemoryManager sync_all(), and
18
+ provider sync_turn() WITHOUT triggering extraction. WebUI then calls
19
+ mark_turn_completed() after the saved/completed-turn boundary so later drains
20
+ know the synced session has uncommitted work and which agent owns it.
21
+
22
+ - Session boundary: commit_session_memory() triggers
23
+ AIAgent.commit_memory_session(), which calls provider on_session_end(),
24
+ posting /api/v1/sessions/<sid>/commit and triggering extraction. This is
25
+ called only at boundaries — /api/session/new with prev_session_id, explicit
26
+ agent eviction, LRU cache eviction, and shutdown drain — matching the CLI's
27
+ AIAgent.commit_memory_session()/shutdown_memory_provider() boundary.
28
+
29
+ The design uses a monotonic generation counter per session plus per-generation
30
+ agent ownership segments. mark_turn_completed() records which agent owns the new
31
+ generation. commit_session_memory() commits the earliest uncommitted segment and
32
+ compare-and-clears only that captured segment after success.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import logging
38
+ import threading
39
+ import time
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+ _lock = threading.Lock()
44
+ _condition = threading.Condition(_lock)
45
+
46
+ _sessions: dict[str, dict] = {}
47
+
48
+
49
+ def _new_entry() -> dict:
50
+ return {
51
+ "generation": 0,
52
+ "committed_generation": 0,
53
+ "agent": None,
54
+ "in_flight": False,
55
+ "segments": [],
56
+ }
57
+
58
+
59
+ def _reset_for_tests() -> None:
60
+ with _condition:
61
+ _sessions.clear()
62
+ _condition.notify_all()
63
+
64
+
65
+ def register_agent(session_id: str, agent) -> None:
66
+ """Register the current agent handle for future completed generations.
67
+
68
+ Existing dirty generations keep their original segment owner. This prevents
69
+ a rebuilt/reopened agent from overwriting the handle needed to retry older
70
+ failed memory-provider work.
71
+ """
72
+ if not session_id:
73
+ return
74
+ with _condition:
75
+ entry = _sessions.setdefault(session_id, _new_entry())
76
+ entry["agent"] = agent
77
+ _condition.notify_all()
78
+
79
+
80
+ def unregister_agent(session_id: str) -> None:
81
+ """Clear the current future-generation agent handle.
82
+
83
+ Dirty segment owners are intentionally preserved so failed work remains
84
+ retryable even if the cache drops the current agent reference.
85
+ """
86
+ if not session_id:
87
+ return
88
+ with _condition:
89
+ entry = _sessions.get(session_id)
90
+ if entry is not None:
91
+ entry["agent"] = None
92
+ _condition.notify_all()
93
+
94
+
95
+ def mark_turn_completed(session_id: str, *, agent=None) -> int:
96
+ if not session_id:
97
+ return 0
98
+ with _condition:
99
+ entry = _sessions.setdefault(session_id, _new_entry())
100
+ if agent is not None:
101
+ entry["agent"] = agent
102
+ owner = agent if agent is not None else entry.get("agent")
103
+ entry["generation"] += 1
104
+ generation = entry["generation"]
105
+ segments = entry["segments"]
106
+ if segments and not entry["in_flight"] and segments[-1].get("agent") is owner:
107
+ segments[-1]["end"] = generation
108
+ else:
109
+ segments.append({"start": generation, "end": generation, "agent": owner})
110
+ _condition.notify_all()
111
+ return generation
112
+
113
+
114
+ def has_uncommitted_work(session_id: str) -> bool:
115
+ if not session_id:
116
+ return False
117
+ with _lock:
118
+ entry = _sessions.get(session_id)
119
+ if entry is None:
120
+ return False
121
+ return entry["generation"] > entry["committed_generation"]
122
+
123
+
124
+ def _first_uncommitted_segment(entry: dict) -> dict | None:
125
+ committed = entry["committed_generation"]
126
+ for segment in entry["segments"]:
127
+ if segment["end"] > committed:
128
+ return segment
129
+ return None
130
+
131
+
132
+ def commit_session_memory(session_id: str, agent=None, *, wait: bool = False, timeout: float | None = None) -> bool:
133
+ if not session_id:
134
+ return False
135
+ deadline = time.monotonic() + timeout if timeout is not None else None
136
+ with _condition:
137
+ entry = _sessions.get(session_id)
138
+ if entry is None:
139
+ return False
140
+ while entry["in_flight"]:
141
+ if not wait:
142
+ return False
143
+ if deadline is None:
144
+ _condition.wait()
145
+ else:
146
+ remaining = deadline - time.monotonic()
147
+ if remaining <= 0:
148
+ return False
149
+ _condition.wait(remaining)
150
+ entry = _sessions.get(session_id)
151
+ if entry is None:
152
+ return False
153
+ if entry["generation"] <= entry["committed_generation"]:
154
+ return False
155
+ segment = _first_uncommitted_segment(entry)
156
+ if segment is None:
157
+ return False
158
+ effective_agent = segment.get("agent")
159
+ if effective_agent is None:
160
+ effective_agent = agent if agent is not None else entry.get("agent")
161
+ if effective_agent is not None:
162
+ segment["agent"] = effective_agent
163
+ if effective_agent is None:
164
+ return False
165
+ captured_generation = segment["end"]
166
+ entry["in_flight"] = True
167
+
168
+ try:
169
+ effective_agent.commit_memory_session()
170
+ except Exception:
171
+ logger.exception("commit_memory_session() failed for session %s", session_id)
172
+ with _condition:
173
+ re_entry = _sessions.get(session_id)
174
+ if re_entry is not None:
175
+ re_entry["in_flight"] = False
176
+ _condition.notify_all()
177
+ return False
178
+
179
+ with _condition:
180
+ re_entry = _sessions.get(session_id)
181
+ if re_entry is not None:
182
+ re_entry["in_flight"] = False
183
+ if captured_generation > re_entry["committed_generation"]:
184
+ re_entry["committed_generation"] = captured_generation
185
+ committed = re_entry["committed_generation"]
186
+ segments = re_entry["segments"]
187
+ while segments and segments[0]["end"] <= committed:
188
+ segments.pop(0)
189
+ if segments and segments[0]["start"] <= committed:
190
+ segments[0]["start"] = committed + 1
191
+ _condition.notify_all()
192
+ return True
193
+
194
+
195
+ def drain_all_on_shutdown() -> None:
196
+ while True:
197
+ with _lock:
198
+ snapshot = [sid for sid, entry in _sessions.items() if entry["generation"] > entry["committed_generation"]]
199
+ if not snapshot:
200
+ return
201
+
202
+ made_progress = False
203
+ for sid in snapshot:
204
+ if commit_session_memory(sid, wait=True):
205
+ made_progress = True
206
+ if not made_progress:
207
+ logger.debug("drain_all_on_shutdown: stopped with uncommitted sessions: %s", sorted(snapshot))
208
+ return
@@ -0,0 +1,207 @@
1
+ """Session-mutation operations for slash commands (/retry, /undo) and
2
+ read-only aggregators (/status, /usage). Operates on the webui's own
3
+ JSON Session store (api/models.py), not on hermes-agent's SQLite.
4
+
5
+ Behavior parity reference: gateway/run.py:_handle_*_command in
6
+ the hermes-agent repo.
7
+ """
8
+ from __future__ import annotations
9
+ import logging
10
+ from typing import Any
11
+
12
+ from api.config import LOCK, _get_session_agent_lock
13
+ from api.models import get_session, SESSIONS
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def _truncate_at_last_user(messages):
19
+ history = messages or []
20
+ last_user_idx = None
21
+ for i in range(len(history) - 1, -1, -1):
22
+ if isinstance(history[i], dict) and history[i].get('role') == 'user':
23
+ last_user_idx = i
24
+ break
25
+ if last_user_idx is None:
26
+ return None
27
+ return history[:last_user_idx]
28
+
29
+
30
+ def _truncation_watermark_for(messages):
31
+ history = list(messages or [])
32
+ if not history:
33
+ return 0.0
34
+ try:
35
+ return float(history[-1].get('timestamp') or 0)
36
+ except (AttributeError, TypeError, ValueError):
37
+ return 0.0
38
+
39
+
40
+ def retry_last(session_id: str) -> dict[str, Any]:
41
+ """Truncate the session to before the last user message, return its text.
42
+
43
+ Mirrors gateway/run.py:_handle_retry_command. Caller (webui frontend)
44
+ is expected to put the returned text back in the composer and call
45
+ send() to resume the conversation -- the agent's gateway calls its own
46
+ _handle_message; the webui has no equivalent in-process pipeline.
47
+
48
+ Raises:
49
+ KeyError: session not found
50
+ ValueError: no user message in transcript
51
+ """
52
+ # Acquire the per-session agent lock as the outermost lock so that the
53
+ # read-modify-write of s.messages is serialised with the periodic
54
+ # checkpoint thread, cancel_stream, and all other session writers.
55
+ # Lock ordering: _agent_lock → LOCK → _write_session_index (LOCK).
56
+ with _get_session_agent_lock(session_id):
57
+ # get_session() and Session.save() both acquire the module-level LOCK
58
+ # internally (the latter via _write_session_index()), and LOCK is a
59
+ # non-reentrant threading.Lock — so they MUST be called outside our
60
+ # own `with LOCK:` block to avoid self-deadlocking.
61
+ #
62
+ # The race we close is the read-modify-write of s.messages: two
63
+ # concurrent /api/session/retry calls could otherwise both compute the
64
+ # same last_user_idx from the same history and double-truncate. We
65
+ # serialize just the in-memory mutation; persistence happens inside
66
+ # the per-session lock so the checkpoint thread cannot race us.
67
+ #
68
+ # Stale-object guard: on a cache miss, two concurrent get_session()
69
+ # calls can each load and cache a *different* Session instance for the
70
+ # same session_id (the second store clobbers the first). Re-bind to
71
+ # the canonical cached instance inside the lock so the mutation lands
72
+ # on the object the next reader will see, not a stale parallel copy.
73
+ s = get_session(session_id) # raises KeyError if missing
74
+ with LOCK:
75
+ s = SESSIONS.get(session_id, s)
76
+ history = s.messages or []
77
+ last_user_idx = None
78
+ for i in range(len(history) - 1, -1, -1):
79
+ if history[i].get('role') == 'user':
80
+ last_user_idx = i
81
+ break
82
+ if last_user_idx is None:
83
+ raise ValueError('No previous message to retry.')
84
+
85
+ last_user_text = _extract_text(history[last_user_idx].get('content', ''))
86
+ removed_count = len(history) - last_user_idx
87
+ s.messages = history[:last_user_idx]
88
+ s.truncation_watermark = _truncation_watermark_for(s.messages)
89
+ if isinstance(getattr(s, 'context_messages', None), list) and s.context_messages:
90
+ truncated_context = _truncate_at_last_user(s.context_messages)
91
+ if truncated_context is not None:
92
+ s.context_messages = truncated_context
93
+ s.save()
94
+ return {'last_user_text': last_user_text, 'removed_count': removed_count}
95
+
96
+
97
+ def undo_last(session_id: str) -> dict[str, Any]:
98
+ """Remove the most recent user message and everything after it.
99
+
100
+ Mirrors gateway/run.py:_handle_undo_command. Returns a preview of the
101
+ removed text so the UI can confirm to the user.
102
+
103
+ Raises:
104
+ KeyError: session not found
105
+ ValueError: no user message in transcript
106
+ """
107
+ # Acquire the per-session agent lock as the outermost lock so that the
108
+ # read-modify-write of s.messages is serialised with the periodic
109
+ # checkpoint thread, cancel_stream, and all other session writers.
110
+ # Lock ordering: _agent_lock → LOCK → _write_session_index (LOCK).
111
+ with _get_session_agent_lock(session_id):
112
+ s = get_session(session_id) # acquires LOCK transiently
113
+ with LOCK:
114
+ # Stale-object guard — see retry_last for the rationale.
115
+ s = SESSIONS.get(session_id, s)
116
+ history = s.messages or []
117
+ last_user_idx = None
118
+ for i in range(len(history) - 1, -1, -1):
119
+ if history[i].get('role') == 'user':
120
+ last_user_idx = i
121
+ break
122
+ if last_user_idx is None:
123
+ raise ValueError('Nothing to undo.')
124
+
125
+ removed_text = _extract_text(history[last_user_idx].get('content', ''))
126
+ removed_count = len(history) - last_user_idx
127
+ s.messages = history[:last_user_idx]
128
+ s.truncation_watermark = _truncation_watermark_for(s.messages)
129
+ if isinstance(getattr(s, 'context_messages', None), list) and s.context_messages:
130
+ truncated_context = _truncate_at_last_user(s.context_messages)
131
+ if truncated_context is not None:
132
+ s.context_messages = truncated_context
133
+ s.save() # outside LOCK -- save() re-acquires LOCK via _write_session_index()
134
+ preview = (removed_text[:40] + '...') if len(removed_text) > 40 else removed_text
135
+ return {
136
+ 'removed_count': removed_count,
137
+ 'removed_preview': preview,
138
+ }
139
+
140
+
141
+ def session_status(session_id: str) -> dict[str, Any]:
142
+ """Return a snapshot of session state for /status.
143
+
144
+ Webui equivalent of gateway/run.py:_handle_status_command. The agent's
145
+ "agent_running" comes from `session_key in self._running_agents`; the
146
+ webui equivalent is whether the session has an active stream
147
+ (active_stream_id is set).
148
+ """
149
+ s = get_session(session_id)
150
+ inp = int(s.input_tokens or 0)
151
+ out = int(s.output_tokens or 0)
152
+ profile = getattr(s, 'profile', None) or 'default'
153
+ try:
154
+ from api.profiles import get_hermes_home_for_profile
155
+ hermes_home = str(get_hermes_home_for_profile(profile))
156
+ except Exception:
157
+ hermes_home = ''
158
+ return {
159
+ 'session_id': s.session_id,
160
+ 'title': s.title,
161
+ 'model': s.model,
162
+ 'profile': profile,
163
+ 'hermes_home': hermes_home,
164
+ 'workspace': s.workspace,
165
+ 'personality': s.personality,
166
+ 'message_count': len(s.messages or []),
167
+ 'created_at': s.created_at,
168
+ 'updated_at': s.updated_at,
169
+ 'agent_running': bool(getattr(s, 'active_stream_id', None)),
170
+ 'input_tokens': inp,
171
+ 'output_tokens': out,
172
+ 'total_tokens': inp + out,
173
+ 'estimated_cost': s.estimated_cost,
174
+ }
175
+
176
+
177
+ def session_usage(session_id: str) -> dict[str, Any]:
178
+ """Return token usage and cost for /usage.
179
+
180
+ Mirrors gateway/run.py:_handle_usage_command's basic counters. The
181
+ agent shows additional fields (rate-limit headroom etc.) that depend
182
+ on provider API responses we don't have in webui -- those are deferred.
183
+ """
184
+ s = get_session(session_id)
185
+ inp = int(s.input_tokens or 0)
186
+ out = int(s.output_tokens or 0)
187
+ return {
188
+ 'input_tokens': inp,
189
+ 'output_tokens': out,
190
+ 'total_tokens': inp + out,
191
+ 'estimated_cost': s.estimated_cost,
192
+ 'model': s.model,
193
+ }
194
+
195
+
196
+ def _extract_text(content: Any) -> str:
197
+ """Flatten message content to plain text. Agent stores either a string
198
+ or a list of {type, text|...} parts; webui needs the user-typed text."""
199
+ if isinstance(content, str):
200
+ return content
201
+ if isinstance(content, list):
202
+ parts = []
203
+ for p in content:
204
+ if isinstance(p, dict) and p.get('type') == 'text':
205
+ parts.append(p.get('text', ''))
206
+ return ' '.join(parts)
207
+ return str(content)