Commit Graph

332 Commits

Author SHA1 Message Date
nesquena-hermes e68f74ac99 fix(approval): close SSE notify-ordering, head-fidelity, and trailing-approval gaps (Opus MUST-FIX A/C/D)
Pre-release Opus review caught three correctness bugs in the original
PR #1350 SSE wiring beyond the snapshot/subscribe race:

A) **Notify-ordering race (MUST-FIX A):** _approval_sse_notify took _lock
   only for the subscriber-list snapshot, then released it before
   put_nowait. With two parallel submit_pending calls, T2's notify
   could fire before T1's, leaving the UI showing pending_count=1 while
   the server actually had 2 queued.

C) **Trailing approval lost (MUST-FIX C):** _handle_approval_respond
   never called _approval_sse_notify after popping. With parallel
   tool-call approvals (#527), a second approval queued behind the one
   being responded to was invisible until the next event ever fired —
   in practice, the agent thread parked on it would appear hung.

D) **Payload showed tail not head (MUST-FIX D):** payload built from
   the just-appended entry instead of queue[0]. /api/approval/pending
   returns the head; SSE returned the tail. Diverging contracts.

Fix:
- Split into _approval_sse_notify_locked (caller holds _lock, no
  internal locking) and _approval_sse_notify (convenience wrapper).
- submit_pending: call _locked variant inside the queue-mutation lock,
  passing queue_list[0] as head.
- _handle_approval_respond: call _locked variant inside the pop lock,
  passing the new head (or None/0 if queue is empty).
- Restore fallback poll to 1500ms (was bumped to 3000ms; degraded-mode
  parity with v0.50.247 is more important than save 1.5s of polling).

New regression tests in tests/test_pr1350_sse_notify_correctness.py:
- test_second_submit_pending_sends_head_not_tail (D)
- test_respond_to_first_pushes_second_as_new_head (C)
- test_respond_to_only_pending_pushes_empty_state (C edge)
- test_pending_count_is_monotonic_under_contention (A)

Updated test_approval_sse.py to pin the new contract:
- _approval_sse_notify_locked(session_key, head, total)
- 1500ms fallback interval

Total: 3411 tests passing.

Co-authored-by: jasonjcwu <jasonjcwu@users.noreply.github.com>
2026-04-30 18:45:15 +00:00
nesquena-hermes d6b9cfac23 release: v0.50.248
Bundles:
- #1349 fix(ui): show context indicator percentage without explicit context_length
- #1350 feat(approval): SSE long-connection for real-time approval notifications

Pre-release fixes applied:
- Inline subscribe + snapshot under a single _lock acquisition in
  _handle_approval_sse_stream() to close the snapshot/subscribe race
  flagged in pre-release review. A submit_pending() arriving between
  the snapshot read and subscribe call would have been lost (appended
  to _pending after our snapshot AND notified to subscribers before we
  joined). Now atomic.
- Added tests/test_pr1350_sse_atomic_subscribe.py (4 source-level
  invariants covering the atomic-lock-block guarantee).

Co-authored-by: jasonjcwu <jasonjcwu@users.noreply.github.com>
2026-04-30 18:34:37 +00:00
fxd-jason 932694aec6 feat(approval): SSE long-connection for real-time approval notifications (#1350)
Replaces the 1.5s HTTP polling loop with a Server-Sent Events endpoint
at /api/approval/stream that pushes approval events to the browser
instantly. The backend uses a thread-safe subscriber registry
(_approval_sse_subscribers) with bounded queues to prevent memory
leaks from slow clients. Frontend uses EventSource with automatic
fallback to 3s HTTP polling on SSE error.

- Backend: subscribe/unsubscribe/notify lifecycle in api/routes.py
- New route: GET /api/approval/stream?session_id=
- submit_pending() now calls _approval_sse_notify() after queue append
- Frontend: EventSource with onerror -> _startApprovalFallbackPoll()
- 30s keepalive comments, _CLIENT_DISCONNECT_ERRORS handling
- 42 new tests (static analysis + unit + concurrency)

Co-authored-by: jasonjcwu <jasonjcwu@users.noreply.github.com>
2026-04-30 18:31:42 +00:00
nesquena-hermes 880350312a fix(streaming): fallback to model_metadata for context_length when compressor missing (#1318 follow-up) (#1348)
* fix(streaming): fallback to model_metadata for context_length when compressor missing (#1318 follow-up)

PR #1318 (shipped in v0.50.246 via PR #1341 + commit a5c10d5) persisted
context_length on the session so the context-ring indicator survives
page reloads. But the writer only fired when agent.context_compressor
was present and reported a non-zero value. Fresh agents, interrupted
streams, or compressors without the attribute would still leave
s.context_length=0 — and the indicator would still show 0% on reload.

This follow-up adds a fallback that calls
agent.model_metadata.get_model_context_length(model, base_url) when the
compressor didn't populate the value. The function returns a sensible
static context window for any known model (with a 256K default for
unknown models). Wrapped in a broad try/except because older
hermes-agent builds may not expose the helper.

Sourced from PR #1344 (@jasonjcwu) — extracted into this focused
follow-up after #1344 was closed as superseded by #1341.

Adds 6 structural tests covering: import + call presence, falsy-gate,
agent.model/base_url passing, exception swallowing, save() ordering,
result assignment.

Closes the data-flow gap in #1318 for the compressor-missing case.

* test: relax pr1341 block-size assertion to accommodate the new fallback

---------

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-30 10:27:56 -07:00
nesquena-hermes c98fff79c2 perf(cron): memoize ensure_cron_project() per get_cli_sessions() scan
Pre-release Opus review on PR #1345 (Finding #3) flagged that
get_cli_sessions() was calling ensure_cron_project() once per cron
session in the loop — N lock acquires + N disk reads of projects.json
for N cron sessions per sidebar refresh.

Hoist a per-scan lazy memoizer (_cron_pid()) so we pay the resolution
cost at most once per get_cli_sessions() call. The memoizer is local
to the function (closure) so it's naturally scoped to a single scan
and doesn't leak across calls.

Could also have made ensure_cron_project() module-memoized, but that
would need invalidation on project deletion — the per-scan cache is
simpler and correct without coordination.
2026-04-30 17:21:51 +00:00
nesquena-hermes eb678d5b54 feat(cron): auto-assign cron job sessions to dedicated 'Cron Jobs' project (#1079)
From PR #1345.

Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
2026-04-30 17:13:59 +00:00
nesquena-hermes a5c10d594d fix(streaming): persist context_length on session — completes #1318 fix
Pre-release Opus + nesquena review on v0.50.246 caught that PR #1341
added the data-structure scaffolding (Session.__init__ accepts the 3
fields, save() persists them, compact() exposes them, GET /api/session
returns them) but did NOT add the writer that actually populates them.

Without a writer, the user-visible bug (context-ring shows 0% after
page reload) was NOT fixed by #1341 alone — the fields stayed None
forever because nothing wrote to s.context_length anywhere.

Adds the writer at api/streaming.py:2188 (post-merge per-turn save block,
before s.save()) so the values from agent.context_compressor land on
disk and survive page reloads.

Also moves the SSE usage payload comment to clarify that the live SSE
payload and the session-level persistence are now distinct paths
(payload below, persistence above).

Adds tests/test_pr1341_context_window_persistence.py — 6 structural +
round-trip tests covering Session __init__/save/compact, the routes
response, and the streaming.py writer placement.

Closes #1318 (the actual user-visible bug, not just the scaffolding).
2026-04-30 16:42:32 +00:00
nesquena-hermes f328f3b843 fix(cancel): gate substring guard on pending_started_at timestamp (Opus review)
Pre-release Opus review on v0.50.246 caught a SHOULD-FIX in PR #1338's
cancel_stream synthesis: the symmetric substring guard
(_pending_user in _last_content OR _last_content in _pending_user) was too
loose. Common confirmation replies ("ok", "yes", "go") in the prior turn
would match longer follow-up prompts ("ok please continue"), the synthesis
would be skipped, and the user's typed text would be lost — exactly the
data-loss bug #1298 was supposed to fix.

The fix: gate the substring check on a timestamp comparison. Only treat
the latest user turn as 'already merged by the streaming thread' if its
timestamp is at or after pending_started_at. Earlier turns whose content
happens to be a substring of the pending must not short-circuit synthesis.

Also drops the symmetric (_last_content in _pending_user) branch — that
direction was the false-positive vector. Keeps the equality and prefix
match (workspace-prefix tolerance from the streaming thread).

Adds tests/test_issue1298_cancel_and_activity.py::
test_cancel_synthesizes_when_prior_turn_content_is_substring_of_pending —
regression for the exact 'ok' → 'ok please continue' scenario.
2026-04-30 16:28:20 +00:00
nesquena-hermes d4b055c30b fix(streaming+ui): preserve user message on cancel + persist activity-panel expand state (#1298)
From PR #1338. Already independently APPROVED by nesquena before being absorbed into v0.50.246.

CHANGELOG entries from this PR were dropped during squash (the v0.50.245 section is already
shipped); they will be re-added under [v0.50.246] in the release commit.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-30 16:18:41 +00:00
nesquena-hermes fbe84d26e6 fix(ui+pwa): avoid stale Mermaid render errors and bust cached static asset URLs on every release
From PR #1337.

Co-authored-by: Dennis Soong <dso2ng@gmail.com>
2026-04-30 16:18:01 +00:00
nesquena-hermes 09e12e3c60 fix(streaming): handle list fallback_providers config in addition to single fallback_model dict
From PR #1339.

Co-authored-by: Jim Dawdy <jimdawdy@Jims-MacBook-Pro.local>
2026-04-30 16:18:00 +00:00
nesquena-hermes e2d33ffce4 fix(models): persist context_length/threshold_tokens/last_prompt_tokens in Session model (#1318 split)
From PR #1341.

Co-authored-by: fxd-jason <wujiachen7@gmail.com>
2026-04-30 16:17:59 +00:00
nesquena-hermes 4683a4a0d0 fix(models): default model rehydration when providers share slash-qualified IDs (#1313)
From PR #1326.

Co-authored-by: hacker2005 <chen20057275@outlook.com>
2026-04-30 15:24:35 +00:00
nesquena-hermes 92121324a0 fix(models): exempt streaming sessions from Untitled+0-message sidebar filter (#1327)
From PR #1330.

Co-authored-by: Frank Song <franksong2702@gmail.com>
2026-04-30 15:24:33 +00:00
nesquena-hermes 5bde48bb6e fix(streaming): compare compression_count against per-turn snapshot to stop repeated banner
From PR #1316.

Co-authored-by: qxxaa <mrhanoi@outlook.com>
2026-04-30 15:24:31 +00:00
nesquena-hermes d0f6ee2ef9 fix(cron): import run_job inside _run_cron_tracked to fix NameError (#1310)
From PR #1317.

Co-authored-by: fxd-jason <wujiachen7@gmail.com>
2026-04-30 15:24:30 +00:00
nesquena-hermes ded9b7e1c4 release: v0.50.243 (#1302)
release: v0.50.243

Batch release of 2 PRs.

- #1301 — fix: remove PRIMARY chip badge + add Claude Opus 4.7 label
  Drops the chip-projected configured-model badge added in #1287 (chip
  width 235px → 164px). Adds Claude Opus 4.7 label entries so the picker
  no longer renders "Claude Opus 4 7" (missing dot).
  Independently reviewed and approved by nesquena (commit c0bbd23).

- #1297 (@franksong2702) — fix: preserve cron output response snippets
  Fixes #1295. /api/crons/output now preserves the ## Response section
  when a large skill dump appears in the prompt section; falls back to
  file tail when no marker exists.

Tests: 3254 passed, 2 skipped, 3 xpassed.

Independently reviewed and approved by nesquena (commit b262e4d).
2026-04-29 21:06:30 -07:00
nesquena-hermes 20ac6dfe5c release: v0.50.242 — revert assistant serif font + remove Calm theme (#1299)
Reverts the global assistant serif rule and removes the Calm theme that were shipped in v0.50.240 PR #1282. Pure deletion; 3252 tests passing. Override on independent review per Nathan.
2026-04-29 19:59:26 -07:00
nesquena-hermes 0ad95cb16a release: v0.50.241 (#1293)
release: v0.50.241

Batch release of 4 PRs:

- #1290 (@nickgiulioni1) — Inline audio/video media editor with playback
  speed controls and HTTP byte-range streaming. PDF/media previews in
  workspace file browser. Composer tray inline players for audio/video.
  (Rebased from #1232.)

- #1287 (@renatomott) — Configured model badges (Primary / Fallback N) in
  the model picker, carried through to the composer chip. Persists through
  on-disk model cache.

- #1289 (@franksong2702) — Appearance autosave for theme/skin/font-size in
  Settings; inline Saving / Saved / Failed status. Font size now persists
  to config.yaml. Refs #1003.

- #1294 (@franksong2702) — Normalize agent session source metadata
  (raw_source / session_source / source_label) through /api/sessions and
  gateway watcher SSE snapshots. Existing source_tag / is_cli_session
  fields preserved. Refs #1013.

Tests: 3254 passed, 2 skipped, 3 xpassed (was 3199 before this release).

Independently reviewed and approved by nesquena (commit d1738f6).
2026-04-29 19:54:07 -07:00
nesquena-hermes 33a145a669 release: v0.50.240
## Release v0.50.240

Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures).

---

### Added

- **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282
- **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482
- **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479
- **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485
- **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481
- **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568
- **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281
- **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268
- **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269

### Fixed

- **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266
- **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278
- **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267
- **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273

---

### Test results

```
3199 passed, 2 skipped, 3 xpassed in 72.79s
```

### PRs on hold (not included)

#1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-29 17:42:32 -07:00
Hermes Agent eeef360a74 Merge remote-tracking branch pr/1261 into stage/batch-v0.50.238 2026-04-29 15:51:54 +00:00
Hermes Agent bd8fc6a2e2 fix(models): preserve @provider:model hint when hint matches active provider
When the user explicitly selects @provider:model from the picker,
_resolve_compatible_session_model() was stripping the prefix because
the hint matched the active provider (hint_matches_active=True → return bare_model, True).

This caused:
- The picker to snap back to the first duplicate entry on next render
- resolve_model_provider() to use the default provider instead of the
  explicitly selected one, running the agent on the wrong backend

The hint_matches_active branch was intended for normalizing stale cross-
provider session models. But an @provider:model where the hint IS the
active provider is not stale — it is the user's deliberate selection.

Fix: return (model, False) so the full @provider:model survives to
resolve_model_provider() in config.py, which already handles it correctly.

Updates test_active_at_provider_session_model_preserved_with_hint and
adds test_issue1253_duplicate_model_id_active_provider_hint_preserved.

Closes #1253
2026-04-29 15:18:43 +00:00
Hermes Agent 4ee80425f2 Merge remote-tracking branch 'refs/remotes/pr/1229' into stage/batch-v0.50.238 2026-04-29 15:17:57 +00:00
Hermes Agent e2ff00f819 Merge remote-tracking branch pr/1247 into stage/batch-v0.50.238 2026-04-29 15:11:21 +00:00
Hermes Agent 8b9ad761f9 Merge remote-tracking branch pr/1251 into stage/batch-v0.50.238 2026-04-29 15:10:49 +00:00
Hermes Agent 1cf406addb Merge remote-tracking branch 'pr/1246' into stage/batch-v0.50.238 2026-04-29 15:05:09 +00:00
Hermes Agent ea4d381e43 Merge remote-tracking branch 'pr/1248' into stage/batch-v0.50.238 2026-04-29 14:29:05 +00:00
Hermes Agent 2bdf5c77d4 Merge remote-tracking branch 'pr/1245' into stage/batch-v0.50.238 2026-04-29 14:29:05 +00:00
Hermes Agent 26579ba141 Merge remote-tracking branch 'pr/1250' into stage/batch-v0.50.238 2026-04-29 14:29:05 +00:00
Hermes Agent 3feef25737 Merge remote-tracking branch 'pr/1244' into stage/batch-v0.50.238 2026-04-29 14:29:04 +00:00
happy5318 cc45175ee5 docs: add thread safety comment for SESSION_AGENT_CACHE
All LRU cache operations (get, set, move_to_end, popitem) are already
protected by SESSION_AGENT_CACHE_LOCK. This addresses the reviewer's
concern about thread safety in multi-threaded ASGI servers.
2026-04-29 20:08:12 +08:00
KingBoyAndGirl 4e0d8da060 fix: restore GET /api/mcp/servers route inside handle_get()
Problem:
- GET /api/mcp/servers returned 404 error
- MCP servers management UI could not load server list
- Root cause: route was placed outside handle_get(), in unreachable code

Root Cause:
- The MCP servers GET route was incorrectly placed after handle_get() returned False (404)
- handle_get() function returns False at line ~1224, so any code after it won't execute
- The route was also in handle_post() area but without proper method checking

Solution:
- Moved GET /api/mcp/servers route inside handle_get() before the return False statement
- Removed the misplaced route from the old location (originally around line 1636)
- Also updated /api/profiles response format to include full profiles list

Testing:
- After restart: curl http://localhost:8787/api/mcp/servers returns {"servers": []}
- No more 404 errors
- WebUI can now properly load MCP servers list
2026-04-29 17:39:56 +08:00
happy5318 65e5690772 fix: add LRU limit to SESSION_AGENT_CACHE to prevent memory bloat
The agent cache stores full AIAgent instances (each holding complete
conversation history) without size limit. Long-running servers with
many sessions can accumulate unbounded memory usage.

Changes:
- Replace dict with OrderedDict for LRU tracking
- Add SESSION_AGENT_CACHE_MAX = 50 limit
- Evict least-recently-used entries when cache exceeds limit
- Call move_to_end() on cache hits to maintain LRU order

This prevents memory exhaustion on servers with many active sessions.
2026-04-29 17:35:12 +08:00
yzp12138 0fe59831fe tests: add regression tests + magic-byte image validation for native image attachments 2026-04-29 17:01:01 +08:00
Frank Song 1ed1ce219d Preserve transcript across context compaction 2026-04-29 16:37:08 +08:00
KingBoyAndGirl d184613752 fix: fetch live models for custom provider from model.base_url 2026-04-29 16:24:19 +08:00
Frank Song b277e195fe Fix MiniMax China provider visibility 2026-04-29 15:50:32 +08:00
Dennis Soong 8a74ea89e7 fix: apply profile terminal env in webui sessions 2026-04-29 14:12:59 +08:00
KingBoyAndGirl be08842642 fix: trust custom provider base_url in SSRF validation
When using custom providers with private IPs (like AxonHub on internal
networks), the SSRF protection incorrectly blocks API calls to the user's
own configured endpoint.

This fix automatically adds the model.base_url hostname to the SSRF
trusted hosts list, since it's explicitly configured by the user.

Fixes issues where /api/models and /v1/* endpoints fail silently
when using custom providers with private IPs or IPv6 addresses.
2026-04-29 13:45:52 +08:00
Hermes Agent 867f2a3f81 absorb: address Opus review findings (security + correctness)
B1: fix stored XSS in MCP delete button — replace inline onclick with
    data-mcp-name attribute + event delegation (panels.js)
B2: fix zip/tar-slip via startswith prefix collision — use
    is_relative_to(); track actual extracted bytes instead of trusting
    member.file_size (upload.py)
B3: add NVIDIA NIM endpoint to _OPENAI_COMPAT_ENDPOINTS and
    _SUPPORTED_PROVIDER_SETUPS so provider is reachable (routes.py,
    onboarding.py)
H1: add terminalResizeHandle element to index.html and return it from
    _terminalEls() so resize-by-drag works (index.html, terminal.js)
H2: fix dead get_terminal() branch — return None for dead terminals
    instead of always returning term (terminal.py)
H3: replace os.environ.copy() with a safe allowlist in PTY shell env
    so API keys are not exposed inside the terminal (terminal.py)
H5: make model dedup deterministic — sort groups by provider_id
    alphabetically before first-occurrence assignment (config.py)
H7: add pid regex validation before OAuth probe; constrain key_source
    to a closed set of safe values (providers.py)
M8: add double-run guard for cron run-now — reject if job is already
    tracked as running (routes.py)
2026-04-29 05:06:34 +00:00
Frank Song 60a4cb057e Add embedded workspace terminal 2026-04-29 04:35:11 +00:00
bergeouss 9806a42a26 fix: protect secrets from masked-value round-trip overwrite (#1237)
- Add _strip_masked_values() to skip masked placeholders in PUT endpoint,
  preserving the original stored secret values instead of overwriting them
- Fix transport badge to gracefully handle unknown/future transport types
  with a fallback that shows the raw string
- Add TestStripMaskedValues (5 tests) for the round-trip protection logic
- Addresses reviewer feedback on secret masking semantics and transport badge
2026-04-29 04:34:55 +00:00
bergeouss b2771ebf69 feat: MCP server management UI (#538)
- Add GET /api/mcp/servers (list with masked secrets)
- Add PUT /api/mcp/servers/<name> (add/update stdio and http servers)
- Add DELETE /api/mcp/servers/<name> (remove server)
- MCP section in System settings with server list, add/delete form
- Auto-detect transport type (stdio vs http) from server config
- Mask sensitive values (API keys, tokens, passwords) in list response
- Uses showConfirmDialog for delete confirmation (no native confirm)
- i18n: 21 keys across 7 locales
- 21 tests (list, save, delete, mask_secrets, validation)
2026-04-29 04:34:55 +00:00
Frank Song 2487de2cc0 Harden model cache invalidation paths 2026-04-29 04:33:28 +00:00
Frank Song eefa1bbad8 fix(models): preserve model cache metadata 2026-04-29 04:33:28 +00:00
bergeouss 103a9833d5 feat: workspace drag-to-reorder (#492)
- Add POST /api/workspaces/reorder endpoint to reorder workspace list
- Implement HTML5 drag-and-drop in workspace panel (panels.js)
- Add grip-vertical drag handle icon (icons.js)
- Add drag visual states: dragging, drag-over, cursor styles (style.css)
- Add i18n keys (workspace_drag_hint, workspace_reorder_failed) in all 7 locales
- 11 tests: 7 backend (order, strip, preserve, dedup, unknown, validation) + 4 frontend

Closes #492
2026-04-29 04:33:24 +00:00
Andy 9fabd12e41 fix: preserve clarify drafts on timeout 2026-04-29 04:32:40 +00:00
bergeouss 98ed2d804b feat: cron run status tracking and watch mode (#526)
Backend:
- Track running cron jobs in thread-safe dict (job_id → start_time)
- Wrapper _run_cron_tracked() marks done on completion
- New GET /api/crons/status?job_id=... returns {running, elapsed}
- New GET /api/crons/status returns all running jobs

Frontend:
- After 'Run Now', enters watch mode with 3s polling
- Shows running indicator (spinner + elapsed timer) in detail card
- Auto-detects running jobs when opening detail view
- Stops watch and refreshes output on job completion
- Cleanup on detail view switch

Note: True SSE streaming is not possible because the hermes-agent
scheduler writes output files only on completion. This polling
approach provides real-time status feedback within that constraint.
2026-04-29 04:32:00 +00:00
bergeouss f2f7224b8d fix: add zip-bomb protection and partial extraction cleanup
- Add cumulative extraction size limit (_MAX_EXTRACTED_BYTES = 200 MB)
  that tracks uncompressed file sizes during extraction to guard against
  zip/tar bombs (small compressed archives that expand to huge sizes).
- On any extraction failure (disk full, corrupted member, size limit),
  clean up the partially-extracted destination directory to avoid
  leaving orphaned folders in the workspace.
2026-04-29 04:31:59 +00:00
bergeouss 8c24b24dcd feat: upload and extract zip/tar archives into workspace (#525)
- Add extract_archive() with zip-slip and tar-slip protection
- New /api/upload/extract endpoint for archive uploads
- Auto-detect archive files (.zip, .tar.gz, .tgz, .bz2, .xz)
- Archives extracted into named subfolder (avoids overwrites)
- Workspace file tree auto-refreshes after extraction
- Archive extensions added to file picker accept list
- i18n: archive_extracted key in all 7 locales

Security: path traversal blocked via resolve() prefix check,
matching existing safe_resolve_ws() sandbox pattern.
2026-04-29 04:31:59 +00:00