@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,238 @@
|
|
|
1
|
+
"""Clarify prompt state for the WebUI.
|
|
2
|
+
|
|
3
|
+
This mirrors the approval flow structure, but the response is a free-form
|
|
4
|
+
clarification string instead of an approval decision.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import queue
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
import uuid
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from api.session_events import publish_session_list_changed
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
DEFAULT_TIMEOUT_SECONDS = 120
|
|
19
|
+
_lock = threading.Lock()
|
|
20
|
+
_pending: dict[str, dict] = {}
|
|
21
|
+
_gateway_queues: dict[str, list] = {}
|
|
22
|
+
_gateway_notify_cbs: dict[str, object] = {}
|
|
23
|
+
|
|
24
|
+
# ── SSE subscriber registry ─────────────────────────────────────────────
|
|
25
|
+
_clarify_sse_subscribers: dict[str, list[queue.Queue]] = {}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _ClarifyEntry:
|
|
29
|
+
"""One pending clarify request inside a session."""
|
|
30
|
+
|
|
31
|
+
__slots__ = ("event", "data", "result", "clarify_id")
|
|
32
|
+
|
|
33
|
+
def __init__(self, data: dict):
|
|
34
|
+
self.event = threading.Event()
|
|
35
|
+
self.data = data
|
|
36
|
+
self.result: Optional[str] = None
|
|
37
|
+
self.clarify_id: str = data.get("clarify_id", "") or uuid.uuid4().hex[:12]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def register_gateway_notify(session_key: str, cb) -> None:
|
|
41
|
+
"""Register a per-session callback for sending clarify requests to the UI."""
|
|
42
|
+
with _lock:
|
|
43
|
+
_gateway_notify_cbs[session_key] = cb
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _clear_queue_locked(session_key: str) -> list[_ClarifyEntry]:
|
|
47
|
+
entries = _gateway_queues.pop(session_key, [])
|
|
48
|
+
_pending.pop(session_key, None)
|
|
49
|
+
return entries
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def unregister_gateway_notify(session_key: str) -> None:
|
|
53
|
+
"""Unregister the per-session callback and unblock any waiting clarify prompt."""
|
|
54
|
+
with _lock:
|
|
55
|
+
_gateway_notify_cbs.pop(session_key, None)
|
|
56
|
+
entries = _clear_queue_locked(session_key)
|
|
57
|
+
if entries:
|
|
58
|
+
publish_session_list_changed("attention_cleared")
|
|
59
|
+
for entry in entries:
|
|
60
|
+
entry.event.set()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def clear_pending(session_key: str) -> int:
|
|
64
|
+
"""Clear any pending clarify prompts for the session without removing the callback."""
|
|
65
|
+
with _lock:
|
|
66
|
+
entries = _clear_queue_locked(session_key)
|
|
67
|
+
if entries:
|
|
68
|
+
publish_session_list_changed("attention_cleared")
|
|
69
|
+
for entry in entries:
|
|
70
|
+
entry.event.set()
|
|
71
|
+
return len(entries)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _with_timeout_metadata(data: dict) -> dict:
|
|
75
|
+
item = dict(data or {})
|
|
76
|
+
requested_at = float(item.get("requested_at") or time.time())
|
|
77
|
+
timeout_seconds = int(item.get("timeout_seconds") or DEFAULT_TIMEOUT_SECONDS)
|
|
78
|
+
expires_at = float(item.get("expires_at") or requested_at + timeout_seconds)
|
|
79
|
+
item["requested_at"] = requested_at
|
|
80
|
+
item["timeout_seconds"] = timeout_seconds
|
|
81
|
+
item["expires_at"] = expires_at
|
|
82
|
+
return item
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _clarify_sse_notify(session_id: str, head: dict | None, total: int) -> None:
|
|
86
|
+
"""Push a clarify event to all SSE subscribers for a session."""
|
|
87
|
+
payload = {"pending": dict(head) if head else None, "pending_count": total}
|
|
88
|
+
for q in _clarify_sse_subscribers.get(session_id, ()):
|
|
89
|
+
try:
|
|
90
|
+
q.put_nowait(payload)
|
|
91
|
+
except queue.Full:
|
|
92
|
+
pass # drop if subscriber is slow
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def sse_subscribe(session_id: str) -> queue.Queue:
|
|
96
|
+
"""Register a bounded Queue for SSE push to a given session."""
|
|
97
|
+
q: queue.Queue = queue.Queue(maxsize=16)
|
|
98
|
+
with _lock:
|
|
99
|
+
_clarify_sse_subscribers.setdefault(session_id, []).append(q)
|
|
100
|
+
return q
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def sse_unsubscribe(session_id: str, q: queue.Queue) -> None:
|
|
104
|
+
"""Remove a subscriber Queue; clean up empty session entries."""
|
|
105
|
+
with _lock:
|
|
106
|
+
subs = _clarify_sse_subscribers.get(session_id)
|
|
107
|
+
if subs:
|
|
108
|
+
try:
|
|
109
|
+
subs.remove(q)
|
|
110
|
+
except ValueError:
|
|
111
|
+
pass
|
|
112
|
+
if not subs:
|
|
113
|
+
_clarify_sse_subscribers.pop(session_id, None)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def submit_pending(session_key: str, data: dict) -> _ClarifyEntry:
|
|
117
|
+
"""Queue a pending clarify request and notify the UI callback if registered."""
|
|
118
|
+
data = _with_timeout_metadata(data)
|
|
119
|
+
with _lock:
|
|
120
|
+
gw_queue = _gateway_queues.setdefault(session_key, [])
|
|
121
|
+
# De-duplicate while unresolved: if the most recent pending clarify is
|
|
122
|
+
# semantically identical, reuse it instead of stacking duplicates.
|
|
123
|
+
if gw_queue:
|
|
124
|
+
last = gw_queue[-1]
|
|
125
|
+
if (
|
|
126
|
+
str(last.data.get("question", "")) == str(data.get("question", ""))
|
|
127
|
+
and list(last.data.get("choices_offered") or [])
|
|
128
|
+
== list(data.get("choices_offered") or [])
|
|
129
|
+
):
|
|
130
|
+
entry = last
|
|
131
|
+
# Dedup re-uses the existing entry with its original clarify_id.
|
|
132
|
+
# If a future caller pre-populates clarify_id in data, it is
|
|
133
|
+
# silently discarded here — the original entry's id wins.
|
|
134
|
+
# Today no caller sets clarify_id (it's generated by __init__),
|
|
135
|
+
# so this is a non-issue.
|
|
136
|
+
cb = _gateway_notify_cbs.get(session_key)
|
|
137
|
+
# Keep _pending aligned to the oldest unresolved entry.
|
|
138
|
+
_pending[session_key] = gw_queue[0].data
|
|
139
|
+
if cb:
|
|
140
|
+
try:
|
|
141
|
+
cb(dict(entry.data))
|
|
142
|
+
except Exception:
|
|
143
|
+
pass
|
|
144
|
+
# Safe to call while holding _lock: publish() only takes the
|
|
145
|
+
# leaf _SESSION_EVENTS_LOCK and never re-acquires this lock.
|
|
146
|
+
publish_session_list_changed("attention_pending")
|
|
147
|
+
return entry
|
|
148
|
+
|
|
149
|
+
entry = _ClarifyEntry(data)
|
|
150
|
+
# Ensure clarify_id is present in the serialised data the frontend receives.
|
|
151
|
+
entry.data["clarify_id"] = entry.clarify_id
|
|
152
|
+
gw_queue.append(entry)
|
|
153
|
+
_pending[session_key] = gw_queue[0].data
|
|
154
|
+
cb = _gateway_notify_cbs.get(session_key)
|
|
155
|
+
# Notify SSE subscribers from inside _lock for ordering guarantees.
|
|
156
|
+
_clarify_sse_notify(session_key, dict(gw_queue[0].data), len(gw_queue))
|
|
157
|
+
publish_session_list_changed("attention_pending")
|
|
158
|
+
if cb:
|
|
159
|
+
try:
|
|
160
|
+
cb(data)
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
return entry
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def get_pending(session_key: str) -> dict | None:
|
|
167
|
+
"""Return the oldest pending clarify request for this session, if any."""
|
|
168
|
+
with _lock:
|
|
169
|
+
queue = _gateway_queues.get(session_key) or []
|
|
170
|
+
if queue:
|
|
171
|
+
return dict(queue[0].data)
|
|
172
|
+
pending = _pending.get(session_key)
|
|
173
|
+
return dict(pending) if pending else None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def has_pending(session_key: str) -> bool:
|
|
177
|
+
with _lock:
|
|
178
|
+
return bool(_gateway_queues.get(session_key))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def pending_count(session_key: str) -> int:
|
|
182
|
+
"""Return the number of unresolved clarify prompts for a session."""
|
|
183
|
+
with _lock:
|
|
184
|
+
queue = _gateway_queues.get(session_key) or []
|
|
185
|
+
if queue:
|
|
186
|
+
return len(queue)
|
|
187
|
+
return 1 if _pending.get(session_key) else 0
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def resolve_clarify(session_key: str, response: str, resolve_all: bool = False) -> int:
|
|
191
|
+
"""Resolve the oldest pending clarify request for a session."""
|
|
192
|
+
with _lock:
|
|
193
|
+
q = _gateway_queues.get(session_key)
|
|
194
|
+
if not q:
|
|
195
|
+
_pending.pop(session_key, None)
|
|
196
|
+
return 0
|
|
197
|
+
entries = list(q) if resolve_all else [q.pop(0)]
|
|
198
|
+
if q:
|
|
199
|
+
_pending[session_key] = q[0].data
|
|
200
|
+
_clarify_sse_notify(session_key, dict(q[0].data), len(q))
|
|
201
|
+
else:
|
|
202
|
+
_clear_queue_locked(session_key)
|
|
203
|
+
_clarify_sse_notify(session_key, None, 0)
|
|
204
|
+
publish_session_list_changed("attention_resolved")
|
|
205
|
+
count = 0
|
|
206
|
+
for entry in entries:
|
|
207
|
+
entry.result = response
|
|
208
|
+
entry.event.set()
|
|
209
|
+
count += 1
|
|
210
|
+
return count
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def resolve_clarify_by_id(session_key: str, clarify_id: str, response: str) -> bool:
|
|
214
|
+
"""Resolve a specific pending clarify request by its stable id.
|
|
215
|
+
|
|
216
|
+
Returns True if the id was found and resolved, False otherwise.
|
|
217
|
+
"""
|
|
218
|
+
with _lock:
|
|
219
|
+
q = _gateway_queues.get(session_key)
|
|
220
|
+
if not q:
|
|
221
|
+
_pending.pop(session_key, None)
|
|
222
|
+
return False
|
|
223
|
+
for i, entry in enumerate(q):
|
|
224
|
+
if entry.clarify_id == clarify_id:
|
|
225
|
+
q.pop(i)
|
|
226
|
+
if q:
|
|
227
|
+
_pending[session_key] = q[0].data
|
|
228
|
+
_clarify_sse_notify(session_key, dict(q[0].data), len(q))
|
|
229
|
+
else:
|
|
230
|
+
_clear_queue_locked(session_key)
|
|
231
|
+
_clarify_sse_notify(session_key, None, 0)
|
|
232
|
+
# Safe to call while holding _lock: publish() only takes the
|
|
233
|
+
# leaf _SESSION_EVENTS_LOCK and never re-acquires this lock.
|
|
234
|
+
publish_session_list_changed("attention_resolved")
|
|
235
|
+
entry.result = response
|
|
236
|
+
entry.event.set()
|
|
237
|
+
return True
|
|
238
|
+
return False
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Expose hermes-agent's COMMAND_REGISTRY to the webui frontend.
|
|
2
|
+
|
|
3
|
+
This module is the single integration point with hermes_cli.commands.
|
|
4
|
+
If hermes-agent is unavailable the endpoint degrades to an empty list
|
|
5
|
+
so the frontend can still load with WEBUI_ONLY commands.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
# Commands that are gateway_only in the agent registry -- webui never
|
|
14
|
+
# wants to expose them (sethome, restart, update etc.) even if a future
|
|
15
|
+
# agent version drops the gateway_only flag. /commands is the agent's
|
|
16
|
+
# own command-listing command; webui has its own /help that calls
|
|
17
|
+
# cmdHelp() locally, so /commands would be redundant and confusing.
|
|
18
|
+
_NEVER_EXPOSE: frozenset[str] = frozenset({
|
|
19
|
+
'sethome', 'restart', 'update', 'commands',
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def list_commands(_registry=None) -> list[dict[str, Any]]:
|
|
24
|
+
"""Return COMMAND_REGISTRY entries as JSON-friendly dicts.
|
|
25
|
+
|
|
26
|
+
Returns empty list if hermes_cli is not installed (graceful
|
|
27
|
+
degradation -- the frontend has its own fallback minimum set).
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
_registry: Optional injected registry for testing. When None
|
|
31
|
+
(production), imports COMMAND_REGISTRY from hermes_cli.
|
|
32
|
+
"""
|
|
33
|
+
if _registry is None:
|
|
34
|
+
try:
|
|
35
|
+
from hermes_cli.commands import COMMAND_REGISTRY as _registry
|
|
36
|
+
except ImportError:
|
|
37
|
+
logger.warning("hermes_cli.commands not importable -- /api/commands returns []")
|
|
38
|
+
return []
|
|
39
|
+
|
|
40
|
+
out: list[dict[str, Any]] = []
|
|
41
|
+
for cmd in _registry:
|
|
42
|
+
if cmd.gateway_only:
|
|
43
|
+
continue
|
|
44
|
+
if cmd.name in _NEVER_EXPOSE:
|
|
45
|
+
continue
|
|
46
|
+
out.append({
|
|
47
|
+
'name': cmd.name,
|
|
48
|
+
'description': cmd.description,
|
|
49
|
+
'category': cmd.category,
|
|
50
|
+
'aliases': list(cmd.aliases),
|
|
51
|
+
'args_hint': cmd.args_hint,
|
|
52
|
+
'subcommands': list(cmd.subcommands),
|
|
53
|
+
'cli_only': bool(cmd.cli_only),
|
|
54
|
+
'gateway_only': bool(cmd.gateway_only),
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
# Include plugin-registered slash commands
|
|
58
|
+
try:
|
|
59
|
+
from hermes_cli.plugins import get_plugin_commands
|
|
60
|
+
plugin_cmds = get_plugin_commands() or {}
|
|
61
|
+
existing_names = {c['name'] for c in out}
|
|
62
|
+
for cmd_name, cmd_info in plugin_cmds.items():
|
|
63
|
+
if cmd_name in existing_names or cmd_name in _NEVER_EXPOSE:
|
|
64
|
+
continue
|
|
65
|
+
out.append({
|
|
66
|
+
'name': cmd_name,
|
|
67
|
+
'description': str(cmd_info.get('description', 'Plugin command')),
|
|
68
|
+
'category': 'Plugin',
|
|
69
|
+
'aliases': [],
|
|
70
|
+
'args_hint': str(cmd_info.get('args_hint', '')),
|
|
71
|
+
'subcommands': [],
|
|
72
|
+
'cli_only': False,
|
|
73
|
+
'gateway_only': False,
|
|
74
|
+
})
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
return out
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def execute_plugin_command(command: str) -> str:
|
|
82
|
+
"""Execute a plugin-registered slash command and return printable output.
|
|
83
|
+
|
|
84
|
+
Unknown commands raise ``KeyError`` so the HTTP layer can return 404.
|
|
85
|
+
Plugin handler failures are returned as output text instead of surfacing as
|
|
86
|
+
transport errors, matching Hermes' existing slash-command UX.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
raw = str(command or "").strip()
|
|
90
|
+
if not raw:
|
|
91
|
+
raise ValueError("command is required")
|
|
92
|
+
|
|
93
|
+
cmd_text = raw[1:] if raw.startswith("/") else raw
|
|
94
|
+
cmd_parts = cmd_text.split(maxsplit=1)
|
|
95
|
+
cmd_base = (cmd_parts[0] if cmd_parts else "").strip().lower()
|
|
96
|
+
cmd_arg = cmd_parts[1] if len(cmd_parts) > 1 else ""
|
|
97
|
+
if not cmd_base:
|
|
98
|
+
raise ValueError("command is required")
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
from hermes_cli.plugins import (
|
|
102
|
+
get_plugin_command_handler,
|
|
103
|
+
resolve_plugin_command_result,
|
|
104
|
+
)
|
|
105
|
+
except ImportError as exc:
|
|
106
|
+
raise RuntimeError("plugin command runtime unavailable") from exc
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
handler = get_plugin_command_handler(cmd_base)
|
|
110
|
+
except Exception as exc:
|
|
111
|
+
raise RuntimeError(f"plugin command lookup failed: {exc}") from exc
|
|
112
|
+
|
|
113
|
+
if not handler:
|
|
114
|
+
raise KeyError(cmd_base)
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
result = resolve_plugin_command_result(handler(cmd_arg))
|
|
118
|
+
return str(result or "(no output)")
|
|
119
|
+
except Exception as exc:
|
|
120
|
+
# Don't leak raw exception str (paths, env, internal state) to the
|
|
121
|
+
# user-facing chat. Type name is enough for the user to know what
|
|
122
|
+
# class of failure occurred; full traceback lives in the server log.
|
|
123
|
+
logger.warning("Plugin command %r failed", cmd_base, exc_info=exc)
|
|
124
|
+
return f"Plugin command error: {type(exc).__name__}"
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared helpers for session compression anchor metadata.
|
|
3
|
+
|
|
4
|
+
Manual compression anchoring versus automatic compression paths
|
|
5
|
+
===============================================================
|
|
6
|
+
|
|
7
|
+
When ``auto_compression=True`` is passed to ``visible_messages_for_anchor()``,
|
|
8
|
+
the function accepts a broader set of message content types (including
|
|
9
|
+
provider-style ``input_text`` / ``output_text`` parts) and metadata markers
|
|
10
|
+
(``reasoning``, ``thinking``, etc.) from any non-tool role. This enables the
|
|
11
|
+
streaming auto-compression path to determine which messages should anchor
|
|
12
|
+
compression UI metadata without being limited to the legacy manual-compression
|
|
13
|
+
rules.
|
|
14
|
+
|
|
15
|
+
When ``auto_compression=False`` (the default), the function applies the
|
|
16
|
+
historical manual-compression rules: only plain ``text`` content parts from
|
|
17
|
+
non-assistant roles are counted.
|
|
18
|
+
|
|
19
|
+
Why this module exists
|
|
20
|
+
======================
|
|
21
|
+
|
|
22
|
+
Compression anchoring needs to identify which messages in a session transcript
|
|
23
|
+
are semantically significant enough to seed the compression UI metadata (e.g.,
|
|
24
|
+
message count, token budget display). The original implementation hard-coded
|
|
25
|
+
these rules in multiple places. This module consolidates the logic so that:
|
|
26
|
+
|
|
27
|
+
1. Manual compression anchoring (CLI/legacy path) uses the stricter ruleset.
|
|
28
|
+
2. Automatic compression (streaming/agent path) can leverage the relaxed ruleset
|
|
29
|
+
when it knows it is handling provider-style messages.
|
|
30
|
+
|
|
31
|
+
Callers specify ``auto_compression=True`` when the messages may originate from
|
|
32
|
+
an automatic/compression-aware pipeline, and ``False`` (default) for manual
|
|
33
|
+
compression contexts.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _content_text(content, *, part_types):
|
|
38
|
+
if isinstance(content, list):
|
|
39
|
+
return "\n".join(
|
|
40
|
+
str(part.get("text") or part.get("content") or "")
|
|
41
|
+
for part in content
|
|
42
|
+
if isinstance(part, dict) and part.get("type") in part_types
|
|
43
|
+
).strip()
|
|
44
|
+
return str(content or "").strip()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _content_has_part_type(content, part_types):
|
|
48
|
+
if not isinstance(content, list):
|
|
49
|
+
return False
|
|
50
|
+
return any(
|
|
51
|
+
isinstance(part, dict) and part.get("type") in part_types
|
|
52
|
+
for part in content
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def is_context_compression_marker(message):
|
|
57
|
+
"""Return true for synthetic compression/reference cards, not user turns."""
|
|
58
|
+
if not isinstance(message, dict):
|
|
59
|
+
return False
|
|
60
|
+
role = message.get("role")
|
|
61
|
+
if not role or role == "tool":
|
|
62
|
+
return False
|
|
63
|
+
text = _content_text(
|
|
64
|
+
message.get("content", ""),
|
|
65
|
+
part_types={"text", "input_text", "output_text"},
|
|
66
|
+
).lower().lstrip()
|
|
67
|
+
return (
|
|
68
|
+
text.startswith("[context compaction")
|
|
69
|
+
or text.startswith("context compaction")
|
|
70
|
+
or text.startswith("[your active task list was preserved across context compression]")
|
|
71
|
+
or text.startswith("[session arc summary")
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _is_context_compression_marker(message):
|
|
76
|
+
"""Backward-compatible alias for callers that have not switched yet."""
|
|
77
|
+
return is_context_compression_marker(message)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def visible_messages_for_anchor(messages, *, auto_compression: bool = False):
|
|
81
|
+
"""Return transcript messages that can anchor compression UI metadata.
|
|
82
|
+
|
|
83
|
+
Manual compression historically only counted plain ``text`` content parts
|
|
84
|
+
for non-assistant messages, while the streaming auto-compression path also
|
|
85
|
+
accepted provider-style ``input_text`` / ``output_text`` parts and metadata
|
|
86
|
+
markers on any non-tool role. Keep that difference explicit at the call site
|
|
87
|
+
instead of carrying two near-identical helper implementations.
|
|
88
|
+
"""
|
|
89
|
+
out = []
|
|
90
|
+
text_part_types = {"text", "input_text", "output_text"} if auto_compression else {"text"}
|
|
91
|
+
for message in messages or []:
|
|
92
|
+
if not isinstance(message, dict):
|
|
93
|
+
continue
|
|
94
|
+
role = message.get("role")
|
|
95
|
+
if not role or role == "tool":
|
|
96
|
+
continue
|
|
97
|
+
if _is_context_compression_marker(message):
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
content = message.get("content", "")
|
|
101
|
+
has_attachments = bool(message.get("attachments"))
|
|
102
|
+
text = _content_text(content, part_types=text_part_types)
|
|
103
|
+
|
|
104
|
+
if auto_compression:
|
|
105
|
+
has_tool_calls = bool(
|
|
106
|
+
isinstance(message.get("tool_calls"), list) and message.get("tool_calls")
|
|
107
|
+
)
|
|
108
|
+
has_tool_use = _content_has_part_type(content, {"tool_use"})
|
|
109
|
+
has_reasoning = bool(message.get("reasoning"))
|
|
110
|
+
if not text:
|
|
111
|
+
has_reasoning = has_reasoning or _content_has_part_type(
|
|
112
|
+
content,
|
|
113
|
+
{"thinking", "reasoning"},
|
|
114
|
+
)
|
|
115
|
+
if text or has_attachments or has_tool_calls or has_tool_use or has_reasoning:
|
|
116
|
+
out.append(message)
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
if role == "assistant":
|
|
120
|
+
has_tool_calls = bool(
|
|
121
|
+
isinstance(message.get("tool_calls"), list) and message.get("tool_calls")
|
|
122
|
+
)
|
|
123
|
+
has_tool_use = _content_has_part_type(content, {"tool_use"})
|
|
124
|
+
has_reasoning = bool(message.get("reasoning")) or _content_has_part_type(
|
|
125
|
+
content,
|
|
126
|
+
{"thinking", "reasoning"},
|
|
127
|
+
)
|
|
128
|
+
if text or has_attachments or has_tool_calls or has_tool_use or has_reasoning:
|
|
129
|
+
out.append(message)
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
if text or has_attachments:
|
|
133
|
+
out.append(message)
|
|
134
|
+
return out
|