@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.
- package/README.md +213 -0
- package/bin/hermes-webui.mjs +588 -0
- package/package.json +25 -0
- package/scripts/sync-vendor.mjs +74 -0
- package/templates/launchd/com.bitseek.hermes-webui.plist +21 -0
- package/templates/systemd/hermes-webui.service +13 -0
- package/templates/windows/hermes-webui-task.ps1 +3 -0
- package/vendor/agent-frontend-shell/.bitseek-source.json +6 -0
- package/vendor/agent-frontend-shell/.dockerignore +7 -0
- package/vendor/agent-frontend-shell/.env.docker.example +89 -0
- package/vendor/agent-frontend-shell/.env.example +34 -0
- package/vendor/agent-frontend-shell/.github/FUNDING.yml +3 -0
- package/vendor/agent-frontend-shell/.github/workflows/browser-smoke.yml +42 -0
- package/vendor/agent-frontend-shell/.github/workflows/docker-smoke.yml +233 -0
- package/vendor/agent-frontend-shell/.github/workflows/native-windows-startup.yml +132 -0
- package/vendor/agent-frontend-shell/.github/workflows/release.yml +57 -0
- package/vendor/agent-frontend-shell/.github/workflows/tests.yml +88 -0
- package/vendor/agent-frontend-shell/.vscode/launch.json +59 -0
- package/vendor/agent-frontend-shell/.vscode/settings.json +13 -0
- package/vendor/agent-frontend-shell/AGENTS.md +80 -0
- package/vendor/agent-frontend-shell/ARCHITECTURE.md +1658 -0
- package/vendor/agent-frontend-shell/BUGS.md +52 -0
- package/vendor/agent-frontend-shell/CHANGELOG.md +7295 -0
- package/vendor/agent-frontend-shell/CONTRIBUTING.md +205 -0
- package/vendor/agent-frontend-shell/CONTRIBUTORS.md +107 -0
- package/vendor/agent-frontend-shell/DESIGN.md +173 -0
- package/vendor/agent-frontend-shell/Dockerfile +91 -0
- package/vendor/agent-frontend-shell/LICENSE +21 -0
- package/vendor/agent-frontend-shell/README-CUSTOM.md +76 -0
- package/vendor/agent-frontend-shell/README.md +705 -0
- package/vendor/agent-frontend-shell/ROADMAP.md +351 -0
- package/vendor/agent-frontend-shell/SPRINTS.md +147 -0
- package/vendor/agent-frontend-shell/TESTING.md +1932 -0
- package/vendor/agent-frontend-shell/THEMES.md +170 -0
- package/vendor/agent-frontend-shell/api/__init__.py +1 -0
- package/vendor/agent-frontend-shell/api/agent_health.py +392 -0
- package/vendor/agent-frontend-shell/api/agent_sessions.py +782 -0
- package/vendor/agent-frontend-shell/api/auth.py +592 -0
- package/vendor/agent-frontend-shell/api/background.py +87 -0
- package/vendor/agent-frontend-shell/api/clarify.py +238 -0
- package/vendor/agent-frontend-shell/api/commands.py +124 -0
- package/vendor/agent-frontend-shell/api/compression_anchor.py +134 -0
- package/vendor/agent-frontend-shell/api/config.py +5178 -0
- package/vendor/agent-frontend-shell/api/dashboard_probe.py +255 -0
- package/vendor/agent-frontend-shell/api/extensions.py +253 -0
- package/vendor/agent-frontend-shell/api/gateway_chat.py +435 -0
- package/vendor/agent-frontend-shell/api/gateway_watcher.py +230 -0
- package/vendor/agent-frontend-shell/api/goals.py +608 -0
- package/vendor/agent-frontend-shell/api/helpers.py +474 -0
- package/vendor/agent-frontend-shell/api/kanban_bridge.py +1255 -0
- package/vendor/agent-frontend-shell/api/metering.py +194 -0
- package/vendor/agent-frontend-shell/api/models.py +4210 -0
- package/vendor/agent-frontend-shell/api/oauth.py +770 -0
- package/vendor/agent-frontend-shell/api/onboarding.py +1046 -0
- package/vendor/agent-frontend-shell/api/passkeys.py +365 -0
- package/vendor/agent-frontend-shell/api/profiles.py +1499 -0
- package/vendor/agent-frontend-shell/api/providers.py +2175 -0
- package/vendor/agent-frontend-shell/api/request_diagnostics.py +160 -0
- package/vendor/agent-frontend-shell/api/rollback.py +320 -0
- package/vendor/agent-frontend-shell/api/routes.py +13990 -0
- package/vendor/agent-frontend-shell/api/run_journal.py +284 -0
- package/vendor/agent-frontend-shell/api/runner_client.py +156 -0
- package/vendor/agent-frontend-shell/api/runtime_adapter.py +431 -0
- package/vendor/agent-frontend-shell/api/session_discoverability.py +640 -0
- package/vendor/agent-frontend-shell/api/session_events.py +45 -0
- package/vendor/agent-frontend-shell/api/session_lifecycle.py +208 -0
- package/vendor/agent-frontend-shell/api/session_ops.py +207 -0
- package/vendor/agent-frontend-shell/api/session_recovery.py +655 -0
- package/vendor/agent-frontend-shell/api/skill_usage.py +32 -0
- package/vendor/agent-frontend-shell/api/startup.py +128 -0
- package/vendor/agent-frontend-shell/api/state_sync.py +187 -0
- package/vendor/agent-frontend-shell/api/streaming.py +7048 -0
- package/vendor/agent-frontend-shell/api/system_health.py +167 -0
- package/vendor/agent-frontend-shell/api/terminal.py +410 -0
- package/vendor/agent-frontend-shell/api/turn_journal.py +214 -0
- package/vendor/agent-frontend-shell/api/updates.py +1261 -0
- package/vendor/agent-frontend-shell/api/upload.py +322 -0
- package/vendor/agent-frontend-shell/api/usage.py +26 -0
- package/vendor/agent-frontend-shell/api/workspace.py +867 -0
- package/vendor/agent-frontend-shell/api/workspace_git.py +1261 -0
- package/vendor/agent-frontend-shell/api/worktrees.py +357 -0
- package/vendor/agent-frontend-shell/bootstrap.py +492 -0
- package/vendor/agent-frontend-shell/ctl.sh +427 -0
- package/vendor/agent-frontend-shell/docker-compose.custom.yml +26 -0
- package/vendor/agent-frontend-shell/docker-compose.three-container.yml +168 -0
- package/vendor/agent-frontend-shell/docker-compose.two-container.yml +147 -0
- package/vendor/agent-frontend-shell/docker-compose.yml +57 -0
- package/vendor/agent-frontend-shell/docker_init.bash +459 -0
- package/vendor/agent-frontend-shell/docs/CONTRACTS.md +207 -0
- package/vendor/agent-frontend-shell/docs/EXTENSIONS.md +212 -0
- package/vendor/agent-frontend-shell/docs/ISSUES.md +23 -0
- package/vendor/agent-frontend-shell/docs/UIUX-GUIDE.md +196 -0
- package/vendor/agent-frontend-shell/docs/advanced-chat-setup.md +83 -0
- package/vendor/agent-frontend-shell/docs/docker.md +337 -0
- package/vendor/agent-frontend-shell/docs/onboarding-agent-checklist.md +207 -0
- package/vendor/agent-frontend-shell/docs/onboarding.md +202 -0
- package/vendor/agent-frontend-shell/docs/remote-access.md +75 -0
- package/vendor/agent-frontend-shell/docs/rfcs/README.md +53 -0
- package/vendor/agent-frontend-shell/docs/rfcs/agent-source-boundary.md +70 -0
- package/vendor/agent-frontend-shell/docs/rfcs/canonical-session-resolution.md +124 -0
- package/vendor/agent-frontend-shell/docs/rfcs/hermes-run-adapter-contract.md +1079 -0
- package/vendor/agent-frontend-shell/docs/rfcs/turn-journal.md +195 -0
- package/vendor/agent-frontend-shell/docs/rfcs/webui-run-state-consistency-contract.md +157 -0
- package/vendor/agent-frontend-shell/docs/supervisor.md +280 -0
- package/vendor/agent-frontend-shell/docs/troubleshooting.md +132 -0
- package/vendor/agent-frontend-shell/docs/ui-ux/index.html +863 -0
- package/vendor/agent-frontend-shell/docs/ui-ux/two-stage-proposal.html +768 -0
- package/vendor/agent-frontend-shell/docs/why-hermes.md +489 -0
- package/vendor/agent-frontend-shell/docs/workspace-git.md +92 -0
- package/vendor/agent-frontend-shell/docs/wsl-autostart.md +126 -0
- package/vendor/agent-frontend-shell/eslint.runtime-guard.config.mjs +35 -0
- package/vendor/agent-frontend-shell/extensions/bitseek-design-system.md +330 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/apple-touch-icon.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/empty-logo.svg +739 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-192.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-32.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.svg +745 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.ico +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.svg +745 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v2.svg +751 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v3.svg +739 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon.svg +745 -0
- package/vendor/agent-frontend-shell/extensions/branding/branding.js +112 -0
- package/vendor/agent-frontend-shell/extensions/branding/config.json +14 -0
- package/vendor/agent-frontend-shell/extensions/branding/manifest.json +53 -0
- package/vendor/agent-frontend-shell/extensions/index.js +67 -0
- package/vendor/agent-frontend-shell/extensions/loader/hermes-loader.js +77 -0
- package/vendor/agent-frontend-shell/extensions/manifest.json +16 -0
- package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.css +333 -0
- package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.js +487 -0
- package/vendor/agent-frontend-shell/extensions/pages/manifest.json +6 -0
- package/vendor/agent-frontend-shell/extensions/pages/registry.css +56 -0
- package/vendor/agent-frontend-shell/extensions/pages/registry.js +302 -0
- package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.css +93 -0
- package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.js +98 -0
- package/vendor/agent-frontend-shell/install.sh +63 -0
- package/vendor/agent-frontend-shell/mcp_server.py +567 -0
- package/vendor/agent-frontend-shell/package.json +12 -0
- package/vendor/agent-frontend-shell/pyproject.toml +56 -0
- package/vendor/agent-frontend-shell/pytest.ini +3 -0
- package/vendor/agent-frontend-shell/requirements.txt +5 -0
- package/vendor/agent-frontend-shell/server.py +624 -0
- package/vendor/agent-frontend-shell/start.ps1 +210 -0
- package/vendor/agent-frontend-shell/start.sh +65 -0
- package/vendor/agent-frontend-shell/static/apple-touch-icon.png +0 -0
- package/vendor/agent-frontend-shell/static/boot.js +1990 -0
- package/vendor/agent-frontend-shell/static/commands.js +1402 -0
- package/vendor/agent-frontend-shell/static/favicon-192.png +0 -0
- package/vendor/agent-frontend-shell/static/favicon-32.png +0 -0
- package/vendor/agent-frontend-shell/static/favicon-512.png +0 -0
- package/vendor/agent-frontend-shell/static/favicon-512.svg +18 -0
- package/vendor/agent-frontend-shell/static/favicon.ico +0 -0
- package/vendor/agent-frontend-shell/static/favicon.svg +20 -0
- package/vendor/agent-frontend-shell/static/i18n.js +15389 -0
- package/vendor/agent-frontend-shell/static/icons.js +92 -0
- package/vendor/agent-frontend-shell/static/index.html +1506 -0
- package/vendor/agent-frontend-shell/static/login.js +177 -0
- package/vendor/agent-frontend-shell/static/manifest.json +53 -0
- package/vendor/agent-frontend-shell/static/messages.js +3521 -0
- package/vendor/agent-frontend-shell/static/onboarding.js +800 -0
- package/vendor/agent-frontend-shell/static/panels.js +7995 -0
- package/vendor/agent-frontend-shell/static/pwa-startup.js +83 -0
- package/vendor/agent-frontend-shell/static/sessions.js +5165 -0
- package/vendor/agent-frontend-shell/static/style.css +4774 -0
- package/vendor/agent-frontend-shell/static/sw.js +173 -0
- package/vendor/agent-frontend-shell/static/terminal.js +632 -0
- package/vendor/agent-frontend-shell/static/ui.js +8997 -0
- package/vendor/agent-frontend-shell/static/vendor/js-yaml/4.1.0/js-yaml.min.js +2 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.css +1 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.js +1 -0
- package/vendor/agent-frontend-shell/static/vendor/smd.min.js +29 -0
- 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)
|