@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,214 @@
|
|
|
1
|
+
"""Crash-safe WebUI turn journal helpers.
|
|
2
|
+
|
|
3
|
+
The journal is deliberately tiny: one JSONL file per session, append-only events,
|
|
4
|
+
and read helpers that tolerate malformed lines. Recovery and repair can then
|
|
5
|
+
reason about submitted turns without depending on in-memory stream state.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import time
|
|
13
|
+
import uuid
|
|
14
|
+
from contextlib import contextmanager
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Iterable
|
|
17
|
+
|
|
18
|
+
try: # pragma: no cover - fcntl is unavailable on Windows.
|
|
19
|
+
import fcntl as _fcntl
|
|
20
|
+
except ImportError: # pragma: no cover
|
|
21
|
+
_fcntl = None
|
|
22
|
+
|
|
23
|
+
TURN_JOURNAL_DIR_NAME = "_turn_journal"
|
|
24
|
+
_TERMINAL_EVENTS = {"completed", "interrupted"}
|
|
25
|
+
_SESSION_ID_RE = re.compile(r"^[A-Za-z0-9_.-]+$")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _default_session_dir() -> Path:
|
|
29
|
+
from api.models import SESSION_DIR
|
|
30
|
+
|
|
31
|
+
return Path(SESSION_DIR)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _journal_path(session_id: str, session_dir: Path | None = None) -> Path:
|
|
35
|
+
sid = str(session_id or "").strip()
|
|
36
|
+
if not sid or "/" in sid or "\\" in sid or not _SESSION_ID_RE.fullmatch(sid):
|
|
37
|
+
raise ValueError("invalid session_id")
|
|
38
|
+
root = Path(session_dir) if session_dir is not None else _default_session_dir()
|
|
39
|
+
return root / TURN_JOURNAL_DIR_NAME / f"{sid}.jsonl"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _make_turn_id() -> str:
|
|
43
|
+
return f"{time.strftime('%Y%m%dT%H%M%SZ', time.gmtime())}-{uuid.uuid4().hex[:12]}"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@contextmanager
|
|
47
|
+
def _journal_file_lock(file_obj):
|
|
48
|
+
"""Serialize multi-process journal writes when advisory locks exist.
|
|
49
|
+
|
|
50
|
+
``O_APPEND`` keeps normal same-process appends simple, but a long JSONL event
|
|
51
|
+
can exceed POSIX's small atomic-write boundary. On Unix, take an advisory
|
|
52
|
+
lock around the single event write+fsync so two WebUI worker processes cannot
|
|
53
|
+
interleave large submitted-message payloads into corrupted JSONL. Platforms
|
|
54
|
+
without ``fcntl`` keep the previous best-effort append behavior.
|
|
55
|
+
"""
|
|
56
|
+
if _fcntl is None:
|
|
57
|
+
yield
|
|
58
|
+
return
|
|
59
|
+
_fcntl.flock(file_obj.fileno(), _fcntl.LOCK_EX)
|
|
60
|
+
try:
|
|
61
|
+
yield
|
|
62
|
+
finally:
|
|
63
|
+
_fcntl.flock(file_obj.fileno(), _fcntl.LOCK_UN)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def append_turn_journal_event(
|
|
67
|
+
session_id: str,
|
|
68
|
+
event: dict,
|
|
69
|
+
*,
|
|
70
|
+
session_dir: Path | None = None,
|
|
71
|
+
) -> dict:
|
|
72
|
+
"""Append one turn journal event and fsync it before returning.
|
|
73
|
+
|
|
74
|
+
The returned event is the exact payload written, with default ``version``,
|
|
75
|
+
``session_id``, ``turn_id``, and ``created_at`` fields filled in.
|
|
76
|
+
"""
|
|
77
|
+
if not isinstance(event, dict):
|
|
78
|
+
raise TypeError("event must be a dict")
|
|
79
|
+
event_name = str(event.get("event") or "").strip()
|
|
80
|
+
if not event_name:
|
|
81
|
+
raise ValueError("event is required")
|
|
82
|
+
payload = dict(event)
|
|
83
|
+
payload.setdefault("version", 1)
|
|
84
|
+
payload["session_id"] = str(session_id)
|
|
85
|
+
payload.setdefault("turn_id", _make_turn_id())
|
|
86
|
+
payload.setdefault("created_at", time.time())
|
|
87
|
+
|
|
88
|
+
path = _journal_path(session_id, session_dir=session_dir)
|
|
89
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
line = json.dumps(payload, ensure_ascii=False, separators=(",", ":")) + "\n"
|
|
91
|
+
fd = os.open(path, os.O_CREAT | os.O_APPEND | os.O_WRONLY, 0o600)
|
|
92
|
+
with os.fdopen(fd, "a", encoding="utf-8") as fh:
|
|
93
|
+
with _journal_file_lock(fh):
|
|
94
|
+
fh.write(line)
|
|
95
|
+
fh.flush()
|
|
96
|
+
os.fsync(fh.fileno())
|
|
97
|
+
o_directory = getattr(os, "O_DIRECTORY", None)
|
|
98
|
+
if o_directory is not None:
|
|
99
|
+
try:
|
|
100
|
+
dir_fd = os.open(path.parent, o_directory)
|
|
101
|
+
try:
|
|
102
|
+
os.fsync(dir_fd)
|
|
103
|
+
finally:
|
|
104
|
+
os.close(dir_fd)
|
|
105
|
+
except OSError:
|
|
106
|
+
pass
|
|
107
|
+
return payload
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def read_turn_journal(session_id: str, *, session_dir: Path | None = None) -> dict:
|
|
111
|
+
"""Read a session journal, returning valid events plus malformed lines."""
|
|
112
|
+
path = _journal_path(session_id, session_dir=session_dir)
|
|
113
|
+
events: list[dict] = []
|
|
114
|
+
malformed: list[dict] = []
|
|
115
|
+
try:
|
|
116
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
117
|
+
except FileNotFoundError:
|
|
118
|
+
return {"session_id": str(session_id), "events": [], "malformed": []}
|
|
119
|
+
for line_no, raw in enumerate(lines, start=1):
|
|
120
|
+
if not raw.strip():
|
|
121
|
+
continue
|
|
122
|
+
try:
|
|
123
|
+
event = json.loads(raw)
|
|
124
|
+
except json.JSONDecodeError:
|
|
125
|
+
malformed.append({"line": line_no, "raw": raw})
|
|
126
|
+
continue
|
|
127
|
+
if isinstance(event, dict):
|
|
128
|
+
events.append(event)
|
|
129
|
+
else:
|
|
130
|
+
malformed.append({"line": line_no, "raw": raw})
|
|
131
|
+
return {"session_id": str(session_id), "events": events, "malformed": malformed}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def derive_turn_journal_states(events: Iterable[dict]) -> tuple[dict[str, dict], list[dict]]:
|
|
135
|
+
'''Return the latest event per ``turn_id`` and any terminal-collision entries.
|
|
136
|
+
|
|
137
|
+
The first element is the latest event per turn_id (same overwrite-by-timestamp
|
|
138
|
+
behaviour as before). The second element is a list of collision records, one
|
|
139
|
+
per turn_id that had more than one terminal event. Each collision record
|
|
140
|
+
contains ``turn_id`` and the ``events`` list (in ascending created_at order).
|
|
141
|
+
|
|
142
|
+
A collision means the same logical turn recorded both ``completed`` and
|
|
143
|
+
``interrupted`` terminal events -- the derived state still picks the latest
|
|
144
|
+
by timestamp, but callers can now detect and audit the double-terminal
|
|
145
|
+
situation explicitly rather than having it silently collapse.
|
|
146
|
+
'''
|
|
147
|
+
states: dict[str, dict] = {}
|
|
148
|
+
# Collect all terminal events per turn_id to detect collisions
|
|
149
|
+
terminal_events: dict[str, list[dict]] = {}
|
|
150
|
+
for event in events:
|
|
151
|
+
if not isinstance(event, dict):
|
|
152
|
+
continue
|
|
153
|
+
turn_id = str(event.get('turn_id') or '').strip()
|
|
154
|
+
if not turn_id:
|
|
155
|
+
continue
|
|
156
|
+
# Track terminal events for collision detection
|
|
157
|
+
if is_terminal_turn_event(event):
|
|
158
|
+
terminal_events.setdefault(turn_id, []).append(event)
|
|
159
|
+
# Existing latest-by-timestamp derivation
|
|
160
|
+
previous = states.get(turn_id)
|
|
161
|
+
if previous is None or float(event.get('created_at') or 0) >= float(previous.get('created_at') or 0):
|
|
162
|
+
states[turn_id] = event
|
|
163
|
+
|
|
164
|
+
# Build collision list: turn_ids with more than one terminal event
|
|
165
|
+
collisions = [
|
|
166
|
+
{'turn_id': tid, 'events': sorted(evts, key=lambda e: float(e.get('created_at') or 0))}
|
|
167
|
+
for tid, evts in terminal_events.items()
|
|
168
|
+
if len(evts) > 1
|
|
169
|
+
]
|
|
170
|
+
return states, collisions
|
|
171
|
+
|
|
172
|
+
def _latest_turn_id_for_stream(events: Iterable[dict], stream_id: str) -> str | None:
|
|
173
|
+
stream = str(stream_id or "").strip()
|
|
174
|
+
if not stream:
|
|
175
|
+
return None
|
|
176
|
+
latest: str | None = None
|
|
177
|
+
for event in events:
|
|
178
|
+
if not isinstance(event, dict):
|
|
179
|
+
continue
|
|
180
|
+
if str(event.get("stream_id") or "") != stream:
|
|
181
|
+
continue
|
|
182
|
+
turn_id = str(event.get("turn_id") or "").strip()
|
|
183
|
+
if turn_id:
|
|
184
|
+
latest = turn_id
|
|
185
|
+
return latest
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def append_turn_journal_event_for_stream(
|
|
189
|
+
session_id: str,
|
|
190
|
+
stream_id: str,
|
|
191
|
+
event: dict,
|
|
192
|
+
*,
|
|
193
|
+
session_dir: Path | None = None,
|
|
194
|
+
) -> dict:
|
|
195
|
+
"""Append a lifecycle event for the turn associated with ``stream_id``."""
|
|
196
|
+
payload = dict(event)
|
|
197
|
+
payload["stream_id"] = str(stream_id)
|
|
198
|
+
if not payload.get("turn_id"):
|
|
199
|
+
journal = read_turn_journal(session_id, session_dir=session_dir)
|
|
200
|
+
turn_id = _latest_turn_id_for_stream(journal.get("events") or [], stream_id)
|
|
201
|
+
if turn_id:
|
|
202
|
+
payload["turn_id"] = turn_id
|
|
203
|
+
return append_turn_journal_event(session_id, payload, session_dir=session_dir)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def iter_turn_journal_session_ids(session_dir: Path) -> list[str]:
|
|
207
|
+
journal_dir = Path(session_dir) / TURN_JOURNAL_DIR_NAME
|
|
208
|
+
if not journal_dir.exists():
|
|
209
|
+
return []
|
|
210
|
+
return sorted(path.stem for path in journal_dir.glob("*.jsonl") if path.is_file())
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def is_terminal_turn_event(event: dict) -> bool:
|
|
214
|
+
return str((event or {}).get("event") or "") in _TERMINAL_EVENTS
|