00afdf7fb05ddec0da532fedf025a20dfa4229ec
37
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a41bfe8690 |
fix(lineage): honor _reset_from, fail closed on untrusted markers, hash full watcher payload
Layer three additive pieces on top of the #7021 / #6931 continuation predicate without changing its shape or CONTINUATION_STARTED_AT_TOLERANCE_SECONDS: - _reset_from is a boundary marker alongside _branched_from/_delegate_from (Hermes Agent stamps it in gateway/session_recovery.py and filters it in _NON_CONTINUATION_CHILD_FILTER_SQL). The same predicate drives the sidebar listing, lineage metadata/report and the transcript stitcher. - _branch_markers() returns ('none' | 'markers' | 'unknown', markers); unparsable, non-object, too-deep, or non-string marker payloads are 'unknown' and fail closed as a lineage boundary instead of degrading to "no markers" (fail open). - gateway_watcher._snapshot_hash() hashes the complete canonicalized published payload, and the cheap fingerprint covers model_config, so a projection change to title/created_at/metadata with fixed (id, updated_at, message_count) emits sessions_changed. |
||
|
|
68008e824b |
fix: classify Signal gateway sessions as messaging (#7572)
Classify Signal gateway sessions as messaging so they group, label, and list like other gateway platforms. Co-authored-by: webtecnica <webtecnica@gmail.com> |
||
|
|
dcb515611d |
Release — SSE Last-Event-ID resumption fix (#6886) (#7129)
* fix(sse): scope gateway SSE probe and honor Last-Event-ID on /api/chat/stream Two narrowly-scoped cross-client SSE fixes from the hermes-android#58 follow-up: 1. The /api/sessions/gateway/stream?probe=1 response only describes the optional gateway/agent-sessions stream, but its payload gave clients no way to tell that. A 404 'agent sessions not enabled' result could be misread as SSE being unsupported server-wide, even though the persistent per-session stream (/api/session/stream) is always on and independent of the show_cli_sessions setting. Add additive scope markers to the probe payload (scope, session_stream_available, session_stream_path) and clarify the handler docstring. 2. /api/chat/stream emitted id: stream_id:seq on every journaled event but ignored the Last-Event-ID request header, so any spec-compliant SSE client that auto-reconnects (browser EventSource, Android/CLI clients) replayed the journal from seq 0 and double-rendered the transcript. _chat_stream_resume_after_seq() now resolves the resume cursor with explicit after_event_id/after_seq query params first and the Last-Event-ID header as fallback — the same resolution chain api/kanban_bridge.py already uses. Both the dead-stream journal replay and the live-subscribe gap check consume it; direct callers of the gap check that only pass query params keep historical behavior. Also documents the cross-client SSE endpoint contract in docs/sse-streams.md. Related hermes-webui/hermes-android#58 * fix(sse): split cursor presence from validity on /api/chat/stream resume Address PR #6886 review (gate-fail): the resume logic conflated "no cursor" with "invalid cursor" into a single None, which broke replay on three edges. Root cause and fix: Cursor presence vs validity — _chat_stream_resume_cursor() now returns (after_seq, resume_requested) instead of a bare seq. A client that SUPPLIED any cursor (query params, replay=1, or Last-Event-ID) asked to resume; an invalid / foreign-run / unparseable cursor resolves to after_seq=None but keeps resume_requested=True, and is normalized to replay-from-start so no journal events are silently skipped. A genuinely cursor-less request stays a fresh subscribe. 1. Explicit-query precedence is now decided by parameter PRESENCE, not successful parsing: a malformed/foreign after_event_id/after_seq never falls through to let the Last-Event-ID header override it and skip events. 2. Invalid/foreign cursors no longer bypass replay + gap detection and silently drain a truncated buffer; an ahead-of-stream cursor is no longer clamped onto the snapshot's terminal fence (which emptied the dead-stream replay by filtering the terminal frame). 3. The runner-local observe path now receives the resolved header cursor (retaining explicit-query precedence) so header-only runner reconnects resume instead of duplicating events from the start. Regression tests cover invalid-cursor replay-from-start, ahead-of-stream terminal survival, header-only runner reconnect, and explicit-query precedence; the stale clamp test is updated to the corrected contract. Related hermes-webui/hermes-android#58 * fix(sse): consume resolved resume cursor at every consumer on /api/chat/stream Address PR #6886 r2 review (gate-fail). The r1 presence/validity split fixed the entry resolution, but the individual consumers still re-derived ahead/equality/re-parse decisions, so the invariant leaked at the edges. This makes every consumer honor the resolved (after_seq, resume_requested) pair and normalizes ahead-of-stream in one place. 1. Equality off-by-one: a valid cursor EQUAL to the snapshot cutoff now enters the drain dedup bound (the event at the cursor was already delivered), instead of being treated as "ahead" and double-sending the retained buffer. Ahead is now strictly after_seq > snapshot_cutoff_seq. 2. Numeric ahead-of-stream cursors are normalized to replay-from-start: on the active path, after_seq > snapshot_cutoff_seq resets to 0 before gap-coverage/replay so a truncated buffer can no longer falsely declare coverage and lose the pre-tail events; on the dead-stream path the cursor is normalized against the journal summary's authoritative last_seq before _replay_run_journal, so an ahead cursor returns the full replay instead of an empty SSE body. 3. Runner-local now consumes the resolved cursor instead of re-parsing after_event_id or re-reading Last-Event-ID. The resolver also returns the opaque raw cursor so the runner uses the already-precedence-checked value; explicit opaque cursor= keeps precedence, invalid/foreign cursors pass no cursor (full replay), and the header is honored only when no explicit cursor was supplied. Regression tests cover the equality dedup, active+dead ahead-of-stream normalization (with after_seq-filtering mocks that assert the real replay body), and the three runner precedence probes (malformed-explicit blocks header, foreign passes no cursor, opaque cursor= wins). Related hermes-webui/hermes-android#58 * fix(sse): carry runner-cursor provenance through /api/chat/stream resume Address PR #6886 r3 review (gate-fail). The (after_seq, resume_requested, raw_cursor) tuple carried the cursor VALUE but not its PROVENANCE, so the runner handoff couldn't distinguish a valid opaque runner cursor from a journal-shaped one from a malformed one — and gated the runner handoff on journal-specific validity, which is wrong for the runner adapter (its cursors are opaque). The resolver now returns a distinct, centrally-resolved runner cursor: - a valid after_seq PAIRS with whatever after_event_id shape was supplied — including an opaque runner id like event:2 that the journal parser reads as a foreign run — so the paired runner cursor resumes at the seq (never None / full replay, which would duplicate tokens and tool events). after_seq is read directly (via _parse_run_journal_after_seq_value) rather than through the after_event_id-first journal parser, which would swallow a paired opaque runner id. - a header-only opaque runner id (Last-Event-ID: event:2) is preserved as-is so the runner resumes from it. - a malformed or foreign explicit cursor WITHOUT a valid paired after_seq still yields no runner cursor, blocking Last-Event-ID and replaying from start — the r2 malformed-blocks-header guarantee is unchanged. Regression tests pin the paired opaque id, header-only opaque id, and malformed-with-valid-seq runner resumes, plus the unchanged malformed-without-pair replay-from-start behavior. Related hermes-webui/hermes-android#58 * fix(sse): normalize ahead cursor when the snapshot cutoff is unknown Address PR #6886 r4 review (gate-fail). The ahead-cursor normalization was guarded by `snapshot_cutoff_seq is not None`, so when the subscribe snapshot had no parseable last_event_id (channel has not seen an id-bearing frame yet), a positive ahead cursor (e.g. Last-Event-ID run_1:999) was never normalized — it was installed verbatim as the live dedup bound, filtering EVERY queued frame including the terminal stream_end fence. The reconnect stalled on heartbeats with an empty SSE body. origin/master ignored the header on this path and delivered the events, so this was a new regression. Fix: treat an unknown, non-truncated snapshot cutoff as fence 0 before the ahead-cursor normalization, so a positive client cursor can't become the live dedup bound with no cutoff to bound it — it fails closed to replay-from-start, delivering the buffered events rather than silently losing them. Frames dropped while the cutoff is unknown still cannot prove gap coverage (cutoff None → not covered), so the recovery_control fail-closed path is preserved. Regression test: Last-Event-ID run_1:999 + snapshot_cutoff_seq=None delivers queued events run_1:1 and run_1:2 exactly once, emits the terminal stream_end, and releases the subscriber. Without the fix the test stalls on heartbeats (pytest-timeout kills it), proving the stall. Related hermes-webui/hermes-android#58 * Release — SSE Last-Event-ID resumption fix (#6886) --------- Co-authored-by: Paladin173 <35980893+Paladin173@users.noreply.github.com> Co-authored-by: n <a@n> |
||
|
|
6c8d130a7b | fix: classify Matrix gateway sessions as messaging | ||
|
|
b9e8ade4b6 |
fix(#5587): keep active CLI sessions visible before final output
Self-built stage of contributor PR; gate-pass. Co-authored-by: rodboev <rodboev@users.noreply.github.com> |
||
|
|
63d0c00f8d |
harden #5504 on rodboev's latest head: complete the tombstone wiring + CHANGELOG
@rodboev independently pushed fixes for messaging-tombstone (matches ours) and an
audit orphan-.bak skip — but the audit skip used _marks_deleted_webui_session
(index heuristic), which re-introduces the crash-suppression bug, and the other 4
resurrection/recovery paths were still open. Rebuilt the stage on rodboev's new
head (
|
||
|
|
5263b9acf7 |
feat: add Webhooks sidebar filter parity
Co-authored-by: Kilian Tyler <kilian@kil.dev> |
||
|
|
75016c99f6 | test(sidebar): align legacy list checks with visible-session filtering (#4775) | ||
|
|
dbeb437bcc |
rebase #2980 onto master (v0.51.548) for ship-gate — recover mobile reloads after compression rotation
george-andra: GET /api/session surfaces continuation_session_id for a hidden pre-compression snapshot; loadSession follows it so a mobile reload mid-compression recovers to the visible continuation instead of a hidden snapshot (chat looked 'lost'). Resolved 2 conflicts (1615 behind): sessions.js merged w/ #2971 re-arm guard; routes.py merged w/ _session_source_is_webui branch. Co-authored-by: george-andra <george-andra@users.noreply.github.com> |
||
|
|
4bf9ff7518 |
stage #4594 onto master (v0.51.550) — preserve visible transcript after manual /compress (#3133)
Manual /compress wrote the compressed payload into BOTH s.messages (visible) and s.context_messages (model-facing) + wiped s.tool_calls -> messages vanished from the window. Fix keeps s.messages + tool history durable, deep-copies compressed only into s.context_messages, anchors off visible msgs. Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com> |
||
|
|
1c3b1ecdc9 |
test(gateway_sync): fix intermittent StopIteration flake (test-isolation root cause)
The A3 flake (test_sessions_response_backfills_imported_messaging_source_metadata failing with StopIteration on a sharded CI run while passing in isolation + sequentially) had two compounding causes, both fixed: 1. ROOT CAUSE — show_cli_sessions setting leak across shard-ordered tests. 33 gateway_sync tests flip show_cli_sessions=True but only ~30 reset it, and cleanup_test_sessions didn't reset it. Under pytest-shard ordering a test could run with the setting in an unexpected state, so its session was occasionally absent from /api/sessions. cleanup_test_sessions now resets show_cli_sessions to its default (off) after every test, the same way it already resets last_workspace. 2. SYMPTOM — bare next(generator) with no default raised an opaque StopIteration (read as a mystery flake) instead of a diagnosable failure when the session was absent. The 5 bare next() call sites over /api/sessions results now use next(..., None) + an explicit assertion naming the missing session, so any future isolation regression fails clearly instead of masquerading as a flake. No production code change; tests + fixture only. |
||
|
|
2fa67ad699 |
Release PL (v0.51.451): show named-profile external sessions in the all-profiles list (#4067) (#4296)
* Release PL (v0.51.451): show named-profile external sessions in the all-profiles list (#4067, #4065) All-profiles session enumeration now scans across profiles only when the request explicitly asks (all-profiles view); cross-profile import resolves to the active profile's state.db for unqualified requests (foreign id -> 404), mirroring the #3982/#3991 detail/export profile-scoping gate. Co-authored-by: rodboev * gate fix (Codex CORE + Opus): close cross-profile leak on the import_cli refresh/existing-session branch The already-imported branch of _handle_session_import_cli forced allow_all_profiles=True whenever the globally-stored session carried a profile, with no active-profile visibility gate — so an unqualified request from profile A could read/refresh a session imported under profile B's live state.db (both advisors reproduced the foreign-transcript leak). Now gated exactly like the /api/session detail + export endpoints: unqualified requests require _session_visible_to_active_profile; explicit all_profiles requires a matching profile. + 3 regression tests. * gate fix (Codex SILENT, re-gate): thread source_filter into the all-profiles CLI scan (models.py:4113) The all-profiles branch keys its cache on source_filter but called _load_cli_sessions_uncached without it, so /api/sessions?all_profiles=1 with a source filter returned every-source sessions under a filtered key. + regression test. --------- Co-authored-by: nesquena-hermes <agent@nesquena-hermes> |
||
|
|
ee7fda019f |
test-infra: hard-isolate test state from production + resilient teardown (#4289)
* test-infra: hard-isolate test state from production + robust teardown Two production-safety fixes to tests/conftest.py + tests/_pytest_port.py: 1. TEST_STATE_DIR now defaults under the OS temp dir (/tmp/hermes-webui-tests/) instead of HERMES_HOME/webui-test-<hash> — which had been writing test state INTO the real ~/.hermes/profiles/webui/ (144 leaked webui-test-* dirs found). Added a hard-stop guard: refuse to run if the resolved test state dir is inside the production Hermes home, so a misconfigured env can never let tests wipe/clobber production profiles/sessions/credentials. 2. _rmtree_retry is now resilient on Linux (was a bare shutil.rmtree): retries transient OSError [Errno 39] Directory-not-empty races (a background daemon writing into the tree during teardown) + a final ignore_errors sweep, and warns instead of raising so a benign cleanup race can't spuriously ERROR a test (the failure mode seen near #4283 context-reconciliation tests). * test-infra: anchor production-proximity guard on literal ~/.hermes (not $HERMES_HOME) The first guard compared TEST_STATE_DIR against $HERMES_HOME, which is overridden to TEST_STATE_DIR itself during a run -> false-tripped collection (6 modules) with 'test dir X is inside production home X'. Now anchor on the literal user-home ~/.hermes and explicitly allow any dir under the OS temp root, so the temp default never trips while a real ~/.hermes target still hard-stops. * test-infra: close TMPDIR-under-~/.hermes guard hole (Codex) Codex flagged: the 'temp is always allowed' exception could be exploited if TMPDIR itself is set under ~/.hermes — then a 'temp' path is a production path and _under_temp would wrongly suppress the guard. Now the temp exception only holds when the temp root is itself OUTSIDE the production home. Verified: normal /tmp default allowed; TMPDIR=~/.hermes/tmp now trips the guard. * test-infra: mirror production-proximity guard into _pytest_port.py (Opus defense-in-depth) * test-infra: remove ~/.hermes production-path fallbacks from 4 test files (Codex) Codex's kickback found the real gap: test_sprint5, test_sprint45, test_onboarding_existing_config, test_regressions each hardcoded a ~/.hermes/webui-mvp-test fallback for their state dir (used when running standalone or if the env var is unset). Repointed all to the shared isolated TEST_STATE_DIR from _pytest_port (temp-rooted). Also refreshed stale ~/.hermes/webui-mvp-test references in test_gateway_sync comment + ARCHITECTURE.md. Now ZERO production-path fallbacks remain in tests. --------- Co-authored-by: nesquena-hermes <agent@nesquena-hermes> |
||
|
|
6bf455fa40 |
Release v0.51.306 — Release JV (stage-a2 — branchy compression lineage freshest-tip) (#3761)
* fix(sessions): resolve branchy compression lineage to the freshest tip (#3751) Compression parents can have multiple continuation-looking children when a stale segment is resumed after a newer compressed branch already exists. The previous projection followed the newest DIRECT child only, so it could hide the deeper branch with the latest real activity and make a conversation look missing/stale after compaction or session rotation. - compression_tip() now walks ALL reachable continuation descendants (DFS) and selects the freshest importable (messageful) tip by (last_activity, depth). - read_session_lineage_metadata() expands descendants from the materialized ancestors via the parent index (scoped, 20-hop cap), pulls per-session message stats from the messages table, and exposes a canonical _lineage_tip_id so the WebUI sidebar collapse picks the same tip as the projection. Backward-compat hardening (two release-gate Codex findings, both fixed + tested): The new message-stats / tip-scoring code must not raise on older/minimal or non-standard state.db schemas, which previously collapsed the whole projection: - messages table with NO `timestamp` column → MAX(timestamp) raised in SQL → read_session_lineage_metadata returned {} (lost all lineage metadata). - ISO-8601 TEXT messages.timestamp → float()/raw comparison raised TypeError; in read_importable_agent_session_rows that propagated through compression_tip and get_cli_sessions() swallowed it, hiding ALL imported agent rows. Fixes: - PRAGMA table_info(messages) detection: require session_id, only SELECT MAX(timestamp) when present (else NULL + COUNT only); fall back to message_count. - new _as_score() helper (first numerically-coercible value, else next candidate e.g. started_at) used at EVERY tip-scoring / sort site in both compression_tip() and freshest_continuation_tip(), plus the projection sort. - regression tests: lineage metadata survives REAL/absent/TEXT messages.timestamp, and read_importable_agent_session_rows survives a TEXT timestamp (no empty hide). Co-authored-by: ai-ag2026 <[email protected]> * docs(changelog): stamp v0.51.306 — Release JV (stage-a2 #3751) --------- Co-authored-by: nesquena-hermes <[email protected]> |
||
|
|
0b223e91bc |
Release v0.51.287 — Release JC (stage-r22 — WeCom session classification #3653 + worker-profile picker hiding #3662) (#3695)
* feat(sessions): classify WeCom gateway sessions as messaging (#3653) Co-authored-by: Frank Song <franksong2702@gmail.com> * feat(profiles): hide worker profiles from chat picker (#3662) Co-authored-by: Rod Boev <rod.boev@gmail.com> * docs(changelog): v0.51.287 — Release JC (stage-r22) --------- Co-authored-by: Frank Song <franksong2702@gmail.com> Co-authored-by: Rod Boev <rod.boev@gmail.com> Co-authored-by: nesquena-hermes <[email protected]> |
||
|
|
4cf40a317a |
Release v0.51.264 — Release IF (stage-r14) (#3636)
## Release v0.51.264 — Release IF (stage-r14) Un-held sibling pair (#3585 + #3586) — both addressed the findings from the earlier hold; re-reviewed fresh. ### Fixed | Issue/PR | Author | Fix | |----------|--------|-----| | #3585 | @rodboev | Cron sessions no longer flood the CLI sidebar window (restored the `("cron","webui")` exclusion in `_load_cli_sessions_uncached`). | | #3586 | @rodboev | Messaging sessions keep their source label after a refresh **and open + send correctly** — `is_cli_session_row()` classifies them non-CLI, and the sidebar open/import path now uses `_isMessagingSession()` so a reclassified Discord/Telegram/Slack row is imported on open (no transient stub → no `/api/chat/start` 404). | ### Un-hold note These were held earlier today because the `is_cli_session_row()` reclassification (#3586) created a CORE open-path regression — opening a reclassified messaging session 404'd on the next send. The author pushed a fix adding the `_isMessagingSession()` import gate at all open/lineage/refresh paths (+ regression test `test_issue3603_external_session_import_gate.py`), and Codex confirmed both that AND the secondary webui-recovery concern (cron-only exclusion now keeps `source='webui'` sidecar-less recovery rows) are resolved. ### Gate - Full pytest suite: **7729 passed, 0 failed** - ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN - Codex (regression): **SAFE TO SHIP** — open→import→send path verified (messaging rows go through `/api/session/import_cli` before `/api/chat/start`); `is_cli_session_row` classification correct; the pair composes in `_load_cli_sessions_uncached`. Co-authored-by: rodboev <rodboev@users.noreply.github.com> |
||
|
|
e68c670bba | fix: keep agent cron sessions in project chips | ||
|
|
cd029d801a | fix: align messaging session display counts | ||
|
|
8286061fb0 |
Stage 383: PR #2294
# Conflicts: # tests/test_gateway_sync.py |
||
|
|
3a53592107 | Add previous messaging session controls | ||
|
|
98f2814ecd | Fix empty gateway session hiding messaging history | ||
|
|
f56ec1ec35 | fix: label email gateway sessions | ||
|
|
d76ef2a2b6 | Cover CLI compression lineage filtering | ||
|
|
3c93d5a702 | fix: keep cross-source continuations separate in sidebar | ||
|
|
c7e52084ba | Harden messaging channel handoff | ||
|
|
20ef643bb8 | Add messaging session handoff summary | ||
|
|
9c0a16fdd6 | fix: recover WebUI-origin state.db sessions | ||
|
|
7da1e074e4 |
fix: expose session lineage metadata in API (#1370)
PR #1358 added the client-side lineage collapse helper, but /api/sessions often did not include _lineage_root_id for the WebUI JSON sessions visible in the sidebar. In that case the helper has no grouping key and multiple same-title continuation rows remain visible. This PR: - Reads parent_session_id and end_reason from state.db.sessions for the WebUI sidebar's session ids - Walks the parent chain when end_reason is 'compression' or 'cli_close', producing _lineage_root_id and _compression_segment_count - Cycle-detects via a 'seen' set - Preserves projected lineage metadata on imported/gateway session rows - Allows sidebar collapse to group cross-surface continuation chains (CLI-close → WebUI continuation) while keeping non-continuation parent rows flat Co-authored-by: Dennis Soong <dso2ng@gmail.com> |
||
|
|
70dac0135c |
fix: preserve imported session source metadata (#1357)
Session.load_metadata_only().compact() was dropping is_cli_session, source_tag, session_source, and source_label, so imported CLI/gateway sessions lost their provenance in sidebar/API payloads. Adds these to METADATA_FIELDS and Session.compact(). Co-authored-by: Dennis Soong <dso2ng@gmail.com> |
||
|
|
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). |
||
|
|
8b8ff3328a |
fix: batch triage — 12 contributor PRs (v0.50.227) (#1168)
Merged as v0.50.227. 2634 tests passing, browser QA 21/21 (desktop + mobile). Full attribution below. Thanks to all 12 contributors: @jundev0001 (#1138), @franksong2702 (#1142, #1157, #1162), @dso2ng (#1143), @bergeouss (#1145, #1146, #1156, #1159), @jasonjcwu (#1149), @ccqqlo (#1161), @frap129 (#1165) Two fixes applied during integration and two more by the independent reviewer (@nesquena): - messages.js: per-turn cost delta capture order (#1159) - workspace.py: symlink target blocked-roots check + HOME sanity guard (#1149, #1165) - panels.js: cron unread counter bookkeeping (in-loop increment bug) - tests/test_symlink_cycle_detection.py: register workspace before session/new |
||
|
|
58ad315dca |
v0.50.216: compression chains, renderer fixes, HTML preview, approval z-index, /steer fix, reasoning chip (#1075)
* fix(workspace): add .html/.htm to MIME_MAP so HTML preview renders correctly
MIME_MAP was missing entries for .html and .htm. The server fell back to
Content-Type: application/octet-stream, which browsers refuse to render as
HTML in an iframe — causing a blank white preview.
The rest of the pipeline was already correct: the iframe exists in
static/index.html, openFile() in static/workspace.js routes .html to
showPreview('html'), and _handle_file_raw() in api/routes.py sets the
correct CSP sandbox header when ?inline=1 is present. The only missing
piece was the MIME type.
* test(workspace): lock in MIME_MAP entry for .html/.htm
PR #1070 added .html/.htm → text/html to MIME_MAP in api/config.py
to fix the blank workspace HTML preview iframe. Without a direct
assertion on the MIME_MAP entries, the fix could silently regress
(the existing test_779_html_preview.py tests cover the iframe wiring,
the inline=1 query handling, and the CSP sandbox header — but none of
them touch MIME_MAP itself).
Add a single regression test that asserts MIME_MAP['.html'] and
MIME_MAP['.htm'] are both 'text/html' so any future removal of those
entries fails CI immediately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(composer): raise .approval-card.visible z-index above .queue-card
.queue-card has z-index:2. .approval-card.visible had no z-index, so the
queue flyout would render on top of the approval card when both were visible
simultaneously — obscuring the Allow/Deny buttons.
Fix: add z-index:3 to .approval-card.visible so approvals always render
above the queue flyout. Approval is a blocking, security-relevant interaction
and must never be obscured by passive UI elements.
* test(composer): pin approval-card z-index > queue-card invariant
PR #1071 raises .approval-card.visible to z-index:3 so the security-
relevant Allow / Deny buttons stay clickable when the queue flyout is
also open. Without a regression test, a future CSS edit could silently
drop the z-index back below queue-card (z-index:2) and reintroduce the
bug — there is no automated UI test covering this stacking interaction.
Add a focused regex check that pins the invariant:
.approval-card.visible z-index must be strictly greater than
.queue-card z-index.
Modeled on the existing CSS-regex regression style in
tests/test_mobile_layout.py (test_profile_dropdown_not_clipped_by_overflow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: intercept /steer /interrupt /queue before busy-mode routing in send()
Root cause: slash commands entered while the agent is busy never reached
the command dispatcher. send() enters the busy block and returns early at
line ~50, so the slash-command intercept (~line 56) is never reached.
The text was queued as a plain message. When it drained after the turn
ended, cmdSteer / cmdInterrupt ran on an idle session, saw no active stream,
and showed "No active task to stop."
Fix: at the top of the busy block, before checking busyMode, check if the
text starts with / and is one of the three control commands. If so, dispatch
the handler immediately and return. This lets the user type /steer, /interrupt,
or /queue at any time — including while the agent is mid-stream — and have
them execute against the live session.
Two new regression tests added:
- test_slash_commands_intercepted_before_busymode_routing: verifies the
intercept appears before the busyMode routing in the busy block
- test_steer_intercept_calls_handler_directly: verifies the intercept calls
_bc.fn(_pc.args) and returns, not queues
* test(busy-intercept): pin sync input-clear before await in slash intercept
PR #1072's intercept clears the msg input before awaiting the handler.
Order matters: if the await happens first (or if the clear is moved
inside the handler), the input still shows '/steer foo' for the duration
of the await. A reflexive second Enter press during that window — common
while waiting for the toast — re-runs send(): either re-fires the
handler (double-steer) or, if the turn just ended, falls through to the
non-busy slash dispatcher and drops a confusing "No active task to stop."
Add test_steer_intercept_clears_input_before_await pinning the order so
this UX invariant cannot silently regress.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: update steer i18n and settings copy — steer no longer interrupts
With the real /steer implementation (agent.steer() via /api/chat/steer),
steer injects a correction mid-turn WITHOUT interrupting the current stream.
The previous copy said "falls back to interrupt", "Steer (interrupt + send)",
etc. — accurate only for the old placeholder, not the real implementation.
Changes across all 6 locales (en/ru/es/de/zh/zh-Hant):
cmd_steer: "falls back to interrupt" removed
settings_busy_input_mode_steer: "interrupt + send" → "mid-turn correction"
cmd_steer_fallback: "interrupted" → "queued for next turn"
busy_steer_fallback: "interrupted instead" → "queued for next turn"
settings_desc_busy_input_mode: "currently falls back to interrupt" removed
Also:
static/index.html: inline fallback text updated to match
static/commands.js: internal comment clarified (fallback = queue+cancel,
not "interrupt mode" which implies the primary action)
* fix(renderer): group consecutive blockquote lines into single element
Root cause: the old rule `s.replace(/^> (.+)$/gm, ...)` had three bugs:
1. `.+` required at least one character — bare `>` lines (blank
continuation lines) did not match and passed through as literal `>`
2. Each matching line became its own `<blockquote>` element — a 10-line
blockquote produced 10 stacked `<blockquote>` tags with no grouping
3. When a fenced code block sat inside a blockquote, the fence-stash
pass consumed the code content and left orphaned `>` lines that the
old `.+` pattern could not match
Fix: replace the single-line regex with a group-based approach that matches
one or more consecutive `>` lines as a single block, strips the `>` prefix
from each line, passes each non-empty line through inlineMd(), turns blank
`>` lines into `<br>`, and wraps the entire group in one `<blockquote>`.
14 regression tests added covering:
- Single-line blockquotes (regression)
- Multi-line grouping (2 and 10 lines)
- Two separate blockquotes staying separate
- Bare `>` and `>text` (no space) edge cases
- Blank continuation lines → <br>
- Bold / italic / inline-code inside blockquotes
- Blockquote followed by normal paragraph
* fix(renderer): drop empty trailing line from blockquote match
The new group-based blockquote rule introduced in this PR captures the
trailing newline in its (?:\n|$) clause. After block.split('\n') that
trailing newline produces an empty final element. The original filter
only dropped lone bare '>' artifacts on the last line, so the empty
final element survived, and the .map(blank → '<br>') step turned it
into a phantom <br> immediately before </blockquote>.
Visible symptom: any blockquote whose source ends with \n (the common
case — a quote followed by another paragraph or end-of-message) renders
with an extra blank line at the bottom of the quote.
Reproducer:
'> Hello\n\nThe rest of the message.'
→ '<blockquote>Hello\n<br></blockquote>\nThe rest of the message.'
^^^ phantom <br>
Fix: replace the single-line filter with a while-loop that pops trailing
lines while they are either empty OR a bare '>'. This matches the
intent the Python test mirror in tests/test_blockquote_rendering.py
already had (the mirror was correct; the JS was not — that's why
the original tests passed despite the bug).
Also add four new regression tests in TestNoPhantomTrailingBr that pin
the no-trailing-<br> invariant for the common shapes:
- input ending with \n
- quote followed by paragraph (the real-world case)
- multi-line quote ending with \n
- quote with blank continuation + trailing \n (internal <br> stays,
trailing <br> does not)
Verified end-to-end with node against the actual JS regex.
244 renderer-adjacent tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(renderer): comprehensive markdown fixes — strikethrough, task lists, CRLF, nested blockquotes
Five additional fixes on top of the blockquote grouping from the initial commit:
1. CRLF normalisation: strip \r\n → \n at start of renderMd so Windows
line endings do not produce stray \r characters in rendered output
2. Strikethrough: ~~text~~ → <del>text</del> in both inlineMd() (for use
inside blockquotes/lists) and the outer pass (for plain paragraphs).
Added <del> to SAFE_TAGS and SAFE_INLINE so it is not HTML-escaped.
3. Task lists: - [x] / - [ ] items in unordered lists render as ✅/☐
via task-done/task-todo span wrappers. Checks [X] (uppercase) too.
4. Nested blockquotes: >> / >>> etc. now recurse so each level gets its
own <blockquote> element rather than passing through as literal >.
Implemented by extracting the blockquote rule into _applyBlockquotes()
which calls itself recursively on the stripped inner content.
5. Lists inside blockquotes: > - item now renders <ul><li> inside the
blockquote instead of a literal "- item" string. Task list items work
inside blockquotes too (> - [x] done → ✅ inside <blockquote><ul>).
Also fixed test_issue342.py search window (5000→10000 chars) — the CRLF
strip at the top of renderMd pushed the autolink regex past the old limit.
68 new tests in test_renderer_comprehensive.py + test_blockquote_rendering.py
covering all constructs, edge cases, and combinations.
* fix(renderer): restore space in blockquote prefix-strip regex
Commit 04e7b53 changed the blockquote prefix-strip regex from
/^>[ \t]?/ (consume "> ", "\t>", or just ">")
to
/^>[\t]?/ (only consume "\t>" or just ">")
The space character was dropped from the character class. Since
practically every blockquote an LLM produces is "> " (greater-than
followed by a space), this leaves a leading space artifact on every
stripped blockquote line. Worse, the leading space breaks the
list-detection regex `^(?: )?[-*+] ` inside the new `_applyBlockquotes`
helper — that regex requires either zero or two leading spaces, never
one — so the new "list inside blockquote" feature never fired for
the canonical input shape `> - item`.
Reproducer (against the actual ui.js via node, before the fix):
> Hello world → <blockquote> Hello world</blockquote>
^ phantom leading space
> Steps: → <blockquote>Steps:
> - one - one
> - two - two</blockquote>
^ literal text, NOT a <ul>; lists-in-quote feature broken
> - [x] done → blockquote with literal "[x] done", no checkbox span
Tests passed despite the bug because tests/test_blockquote_rendering.py
and tests/test_renderer_comprehensive.py validate against a Python
mirror (`_apply_blockquotes`) whose strip regex is `^>[ \t]?` — i.e.
the mirror is correct, the JS is not, and the static-mirror tests
can't catch the divergence. Same shape of bug as commit 94d63d0
(phantom <br> in trailing line) where the mirror was right and the JS
was wrong.
Fix: restore the space character in the strip regex's character class.
Add tests/test_renderer_js_behaviour.py — 11 tests that drive the
ACTUAL renderMd via node and assert on rendered output for the most
common LLM shapes (single-line quote, multi-line quote, list inside
quote, task list inside quote, nested >>>, strikethrough inside and
outside quote, top-level task list, quote followed by heading,
multi-paragraph quote with list, CRLF normalisation).
Verified: the buggy regex makes 6 of those 11 tests fail; the corrected
regex makes all 11 pass.
Suite: 2354 passed, 0 new failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Collapse agent session compression chains
* Restore upstream changelog entries
* fix(agent_sessions): bubble active compression chains to top by tip last_activity
The original PR merge kept the chain head's id/title/started_at and overrode
id/model/message_count/ended_at/end_reason from the tip — but did NOT override
last_activity. Since the projected list is sorted by last_activity DESC and
the WebUI sidebar surfaces updated_at = last_activity, an actively-used
compression chain whose tip is being edited NOW would sort by the ROOT's
old last_activity and fall below recently touched standalone sessions.
Reproducer (with the harness against actual code, before the fix):
- root: started 30 days ago, last msg 30 days ago
- tip: started 28 days ago (parent_session_id=root), last msg 5 seconds ago
- standalone: last msg 2 days ago
Sidebar order with original PR:
[0] standalone (48h ago)
[1] active_tip (last_activity=root's 720h ago) ← wrong
Sidebar order after fix:
[0] active_tip (last_activity=tip's 0h ago) ← correct
[1] standalone (48h ago)
This matches Hermes Agent's own list_sessions_rich projection at
hermes_state.py:903-909, which overrides "last_active" from the tip
exactly so that the agent CLI's session list orders the same way.
Add ``last_activity`` to the merge-from-tip key list, update the existing
test_compression_chain_collapses_to_latest_tip_in_sidebar assertion to
expect tip-derived updated_at, and add
test_compression_chain_bubbles_to_top_by_tip_activity locking in the
bubble-to-top invariant — without this regression test the previous
behaviour passed CI because no test exercised the sort order against a
mixed set of chains and standalone sessions.
The chain head's started_at (created_at) and title remain preserved, so
users can still find the conversation by its original date and name.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: v0.50.216 release notes and version bump
Compression chains, renderer fixes, HTML preview, approval z-index, /steer fix.
* chore: gitignore local-only review harness directory
Adds .local-review/ to .gitignore so renderer drivers, sample inputs,
fixture builders, and other reviewer scratch files do not accidentally
get committed. Nothing under that path is ever shared in the repo;
keeping the entry tracked makes the boundary explicit for any future
contributor who creates the directory locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Keep reasoning chip visible for None effort
* test(reasoning): pin chip render output via node, not just source regex
The PR's static checks in test_reasoning_chip_btw_fixes.py validate the
shape of _applyReasoningChip (no display='none' literal, the right
classList.toggle call exists, the right label literals are in the
function body) but pass even if the runtime detail is wrong — for
example if `inactive` were inverted, _normalizeReasoningEffort
mishandled whitespace, or _formatReasoningEffortLabel returned the
wrong literal for an unknown input.
Add tests/test_reasoning_chip_js_behaviour.py — 11 tests that drive
the actual _applyReasoningChip() via node and assert on the rendered
DOM state for each effort value:
TestChipAlwaysVisible
- empty / null -> "Default" label, inactive=true
- "none" -> "None" label, inactive=true
- "low"/"high" -> verbatim label, inactive=false
TestNormalizationEdgeCases
- "NONE" -> normalises to "None"
- " none " -> trims and normalises
- unknown junk -> falls through visible, never hidden
TestTitleAttributeAccessibility
- title attribute carries the human-readable label for tooltip /
screen-reader use
Sanity-checked against master's pre-fix ui.js: 11/11 fail (bug caught).
Against this PR's ui.js: 11/11 pass.
This pattern (drive the actual JS via node) caught two regex-only
regressions in PR #1073 where the Python mirror was correct while the
JS was broken. Same protection added here so the chip-visibility
contract can't silently break in a future refactor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: add #1074 to v0.50.216 changelog, bump test count to 2428
* fix(i18n): restore broken Unicode in Russian and Spanish steer strings
Commit 56c7a14 (fix: update steer i18n and settings copy) accidentally
stripped the `\u` prefix from Unicode escape sequences in two locales,
producing garbled literal hex strings visible to users:
Spanish (es):
- cmd_steer: correcci00f3n → corrección
- cmd_steer_fallback: 2014 en cola → — en cola
- busy_steer_fallback: 2014 en cola → — en cola
- settings_desc_busy_input_mode: qu00e9, est00e1, correcci00f3n → qué, está, corrección
- settings_busy_input_mode_steer: correcci00f3n → corrección
Russian (ru):
- settings_desc_busy_input_mode: the entire Cyrillic string was
replaced with raw 4-hex-char code-points without the \u prefix
(041e043f... instead of actual Cyrillic). Decoded:
"Определяет поведение при отправке сообщения во время работы
агента. Очередь ждёт; Прерывание отменяет и начинает заново;
Steer внедряет коррекцию без прерывания."
Fix: write the correct characters directly (UTF-8 is the file encoding
so embedding them literally is cleaner than \u escapes for long text).
All other locales (en, de, zh, zh-Hant) were not affected — confirmed
by grepping for bare hex run-ons in the updated file.
Verified: node --check static/i18n.js passes; full pytest suite green
(2365 passed, 47 skipped).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: remove duplicate compression chain entry from [Unreleased]
---------
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Frank Song <franksong2702@gmail.com>
|
||
|
|
ad8e10304c |
v0.50.207: batch of 10 PRs — TPS stat, SSE guard, session polish, cron UX, folder create, model errors, session speed, title gen (#1031)
* fix: remove orphaned i18n keys from top-level LOCALES object Three Traditional Chinese translation keys (cmd_status, memory_saved, profile_delete_title) were placed outside any locale block between the en and ru blocks in static/i18n.js. They became top-level properties of the LOCALES object, causing them to appear as invalid language options in the Settings > Preferences dropdown. The correct translations already exist in the zh-Hant locale block. Fixes #1008 * fix: block stale SSE events from polluting new session's DOM - appendThinking(): guard with !S.session||!S.activeStreamId to drop events from a previous session's SSE stream during a session switch - appendLiveToolCard(): same guard for consistency - finalizeThinkingCard(): scroll thinking-card-body to top when scroll is pinned, so completed response is immediately visible - appendThinking(): auto-scroll thinking card body to bottom while streaming if user is watching (scroll pinned) * Fix empty agent sessions in sidebar * fix: resolve cron UI UX issues — icon ambiguity, toast overlap, running status Fixes #995 — three sub-issues in the Cron Jobs UI: 1. Dual play icons ambiguous: Resume button now shows a distinct play+bar icon (play triangle + vertical line) instead of the identical triangle used by Run now. 2. Toast notification overlapping header buttons: Added position:relative; z-index:10 to .main-view-header so it stacks above the fixed toast (z-index:100 within its layer). 3. No running status after trigger: After triggering a job, the status badge immediately shows 'running…' with a CSS spinner animation, and polls the cron list every 3s (up to 30s) to refresh when the job completes. - Added cron_status_running i18n key in all 5 locales (en, es, de, ru, zh, zh-Hant) - Added .detail-badge.running CSS class with spinner animation - New functions: _setCronDetailStatus(), _startCronRunningPoll() * fix(#1011): address review feedback — poll cleanup, badge persistence, 30s fallback - _clearCronDetail() now clears _cronRunningPoll interval on navigation - Poll re-applies 'running' badge after loadCrons() re-render (prevents flicker) - When poll ends (30s max), detail re-renders with actual status as fallback * feat: create folder and add space directly from UI (#782) - After creating a folder via the file tree New folder button, offer to add it as a space via confirm dialog - Add Create folder if it doesnt exist checkbox in the New Space form - Backend: support create flag in /api/workspaces/add to mkdir before validation - i18n: 4 new keys (folder_add_as_space_title/msg/btn, workspace_auto_create_folder) in all 6 locales * fix: validate workspace path before mkdir to prevent orphan directories Review feedback (critical): the previous code called mkdir() before validate_workspace_to_add(), which meant a rejected path (e.g. system dir) would leave an orphan directory on disk. New flow: 1. Resolve path and check against blocked system roots BEFORE any mutation 2. mkdir() only if path passes the blocklist check 3. Full validation (exists, is_dir) after mkdir Also imports _workspace_blocked_roots for the pre-mutation blocklist check. * fix(#1014): classify model-not-found errors with helpful message - Add model_not_found error type to streaming.py exception classifier - Detect 404, 'not found', 'does not exist', 'invalid model' patterns - Strip HTML tags from provider error messages (nginx 404 pages, etc.) - Add model_not_found branch to apperror handler in messages.js - Add i18n key model_not_found_label in all 6 locales - 15 tests covering detection, sanitization, frontend, and i18n * feat(ui): add live TPS stat to header Adds a TPS (Tokens Per Second) chip to the right of the header title bar that updates live while AI output is streaming. Metering (api/metering.py) - Tracks per-session output + reasoning tokens via GlobalMeter singleton - Per-session TPS = total_tokens / elapsed_time - Global TPS = average of active sessions' TPS values - HIGH/LOW are max/min of global_tps snapshots over a 60-minute rolling window (only recorded when > 0, so idle periods are excluded) - Thread-safe with a single lock Metering events emitted from streaming.py - Throttled at 100ms from token/reasoning/tool callbacks so the display updates rapidly during fast token streams - 1Hz ticker as fallback for slow streams (exits when no active sessions) - Final stats emitted on stream end Routes (api/routes.py) - Removed POST /api/metering/interval endpoint (dynamic interval via focus/blur was replaced with simple always-1s-when-active approach) UI (static/messages.js, index.html, style.css) - TPS chip in titlebar: shows 'N.N t/s . N.N high . N.N low' - Default: '0.0 t/s . 0.0 high' when idle - Display updates on every metering SSE event (throttled to 100ms) * feat: session restore speed + title gen reasoning hardening (#1025, #1026) PR #1025 (@franksong2702): Speed up large session restore paths - GET /api/session?messages=0 now parses only metadata before the messages array - Metadata-only loads no longer populate the full-session LRU cache - Frontend lazy fetch uses resolve_model=0 to avoid cold model-catalog lookup - Hard reload no longer waits for populateModelDropdown() before restoring session PR #1026 (@franksong2702): Harden auto title generation for reasoning models - Raises title-gen completion budget to 512 tokens (reasoning-safe) - Retries once with 1024 tokens on empty content / finish_reason:length - Applies retry to both auxiliary and active-agent fallback routes - Preserves underlying failure reason in title_status on local fallback Co-authored-by: Frank Song <franksong2702@gmail.com> * feat: session attention indicators in right slot + last_message_at timestamps (#1024) PR #1024 (@franksong2702): Polish session attention indicators - Streaming spinners and unread dots now reuse the right-side actions slot - Running/unread rows hide timestamps; idle/read rows keep right-aligned timestamps - Date group carets point down when expanded, right when collapsed - Pinned group no longer repeats pinned-star icon per row - Running indicators appear immediately after send (local busy state while /api/sessions catches up) - Sidebar sorting/grouping/timestamps now prefer last_message_at (derived from last real message) so metadata-only saves don't make old sessions appear under Today Co-authored-by: Frank Song <franksong2702@gmail.com> * docs: v0.50.207 release notes — 10 PRs, 2169 tests (+36) --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: Josh <josh@fyul.link> Co-authored-by: Frank Song <franksong2702@gmail.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> |
||
|
|
d4a3adb7b1 |
fix(sessions): surface gateway SSE failures and add polling fallback (#828)
* fix(sessions): surface gateway SSE failures and add polling fallback - add a JSON probe mode for the gateway SSE endpoint - detect watcher-unavailable 503s from the browser - fall back to periodic session refresh with a toast - add probe payload tests and endpoint coverage Fixes #635 * fix(sessions): surface gateway SSE failures and add polling fallback (#826) Absorbed from PR #826 by @cloudyun888 (fixes #635). When the gateway watcher thread is not running, the browser now shows a toast notification and falls back to 30-second periodic polling for session sync. Previously the SSE failure was completely silent with no user feedback. Changes from original PR: - Deleted misplaced test_gateway_sse_probe_unit.py (was at repo root, not discovered by `pytest tests/`); unit tests moved into tests/test_gateway_sync.py - _gateway_sse_probe_payload now checks watcher._thread.is_alive() rather than just watcher is not None — a watcher instance with a dead poll thread now correctly reports unavailable and activates the polling fallback - probeGatewaySSEStatus catch(e) now starts the polling fallback on network error rather than silently swallowing the failure - Added 5 unit tests covering all watcher-alive/dead/missing/disabled branches Co-authored-by: cloudyun888 <269269188+86cloudyun-afk@users.noreply.github.com> * cleanup(gateway): public is_alive() + dedup probe/live watcher-alive check + changelog Three small cleanups on top of @cloudyun888's PR #826 absorption: 1. Add GatewayWatcher.is_alive() public accessor so routes.py doesn't reach into the private _thread attribute. The existing private- attribute check stays as a defensive fallback for any older in- memory instance or test double that doesn't implement the full API. 2. Dedupe the watcher_alive computation in _handle_gateway_sse_stream: the live-SSE path now calls _gateway_sse_probe_payload(...) and reads its watcher_running field instead of re-deriving the same logic inline. Keeps probe and SSE in sync automatically. 3. CHANGELOG trailer was (#826, fixes #635, @cloudyun888) — this PR is #828, so updated to (#828, absorbs PR #826 by @cloudyun888, fixes #635) matching the repo convention for absorbed PRs (see #805). Added two regression tests: - test_gateway_watcher_is_alive_public_method — covers the three lifecycle states (before start, while running, after stop). - test_probe_payload_prefers_public_is_alive — asserts the probe uses watcher.is_alive() rather than poking _thread when the public method exists. Full suite: 1735 passed, 0 new failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: cloudyun888 <269269188+86cloudyun-afk@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c3251ea97d | fix(tests): auto-derive unique port+state-dir per worktree (fixes parallel pytest) | ||
|
|
04ed0ff43d |
v0.50.25: mobile scroll, import timestamps, profile security, mic fallback (#404)
* fix: restore mobile chat scrolling and drawer close (#397) - static/style.css: add min-height:0 to .layout and .main (flex shrink chain fix for mobile scroll) - static/style.css: add -webkit-overflow-scrolling:touch, touch-action:pan-y, overscroll-behavior-y:contain to .messages - static/boot.js: call closeMobileSidebar() on new-conversation button onclick and Ctrl+K shortcut - tests/test_mobile_layout.py: 41 new lines covering all three CSS fixes and both JS call sites Original PR by @Jordan-SkyLF * fix: preserve imported session timestamps (#395) - api/models.py: add touch_updated_at: bool = True param to Session.save(); import_cli_session() accepts created_at/updated_at kwargs and saves with touch_updated_at=False - api/routes.py: extract created_at/updated_at from get_cli_sessions() metadata and forward to import_cli_session(); use touch_updated_at=False on post-import save - tests/test_gateway_sync.py: +53 lines — integration test verifying imported session keeps original timestamp and sorts correctly vs newer sessions; also fix: add WebUI session file cleanup in finally block Original PR by @Jordan-SkyLF * fix(profiles): block path traversal in profile switch and delete flows (#399) Master was vulnerable: switch_profile and delete_profile_api joined user-supplied profile names directly into filesystem paths with no validation. An attacker could send '../../etc/passwd' as a profile name to traverse outside the profiles directory. - api/profiles.py: add _resolve_named_profile_home(name) — validates name with ^[a-z0-9][a-z0-9_-]{0,63}$ regex then enforces path containment via candidate.resolve().relative_to(profiles_root); use in switch_profile() - api/profiles.py: add _validate_profile_name() call to delete_profile_api() entry - api/routes.py: add _validate_profile_name() call at HTTP handler level for both /api/profile/switch and /api/profile/delete (fail-fast at API boundary) - tests/test_profile_path_security.py: 3 tests — traversal rejected, valid name passes Cherry-picked commit aae7a30 from @Hinotoi-agent (PR was 62 commits behind master) * feat: add desktop microphone transcription fallback (#396) Mic button now works in browsers that support getUserMedia/MediaRecorder but lack SpeechRecognition (e.g. Firefox desktop, some Chromium builds). - static/boot.js: detect _canRecordAudio (navigator.mediaDevices + getUserMedia + MediaRecorder); keep mic button enabled when either SpeechRecognition or MediaRecorder is available; MediaRecorder fallback records audio, sends blob to /api/transcribe, inserts transcript into the composer; _stopMic() handles all three states (recognition, mediaRecorder, neither) - api/upload.py: add transcribe_audio() helper — saves uploaded blob to temp file, calls transcription_tools.transcribe_audio(), always cleans up temp file - api/routes.py: add /api/transcribe POST handler — CSRF protected, auth-gated, 20MB limit, returns {text:...} or {error:...} - api/helpers.py: change Permissions-Policy microphone=() to microphone=(self) (required to allow getUserMedia in the same origin) - tests/test_voice_transcribe_endpoint.py: 87 new lines — 3 tests with mocked transcription - tests/test_sprint19.py: +1 regression guard (microphone=(self) in Permissions-Policy) - tests/test_sprint20.py: 3 updated tests for new fallback-capability checks Original PR by @Jordan-SkyLF * docs: v0.50.25 release — version badge and CHANGELOG --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> |
||
|
|
711bb5a6c9 |
feat: real-time gateway session sync (Phase 1) (#274)
* feat: add real-time gateway session sync (Phase 1) - Add gateway_watcher.py: background daemon polling state.db every 5s for gateway session changes (telegram, discord, slack, etc.) - Extend get_cli_sessions() to include all non-webui sources - Add SSE endpoint /api/sessions/gateway/stream for real-time push - Add dynamic source badges (telegram=blue, discord=purple, slack=dark purple) - Rename 'Show CLI sessions' to 'Show agent sessions' - Wire watcher lifecycle into server start/stop - 10 tests covering metadata, filtering, SSE, and watcher lifecycle - Activated via the same checkbox as CLI session import Addresses GitHub issue #272 * fix: SSE event name mismatch, TLS attribute, remove PLAN.md - Fix critical SSE bug: frontend listened for 'gateway_session_update' but backend sends 'sessions_changed' -- events were silently dropped - Fix frontend field check: data.changed -> data.sessions (matches the actual payload structure from gateway_watcher) - Fix TLS: ssl.TLSv1_2 -> ssl.TLSVersion.TLSv1_2 (the bare attribute does not exist, would crash TLS setup and silently fall back to HTTP) - Remove PLAN.md: implementation plan should not be committed to repo Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: test isolation and slow-consumer sentinel in gateway sync tests/test_gateway_sync.py: - Fix _get_test_state_dir() path mismatch: the function was computing HERMES_HOME/webui-mvp-test but conftest.py sets HERMES_HOME=TEST_STATE_DIR, so state.db was written to a double-nested path the server never read. Now uses HERMES_WEBUI_STATE_DIR first (which conftest sets directly to TEST_STATE_DIR), fixing the 7/10 test failures in full-suite ordering. - Fix conn cleanup: removed conn.close() from inside try blocks so the connection stays valid for _remove_test_sessions() in the finally block. Previously the closed conn caused ProgrammingError in finally (swallowed by bare except), leaving ghost sessions in state.db on test failure. api/gateway_watcher.py: - Fix slow-consumer queue eviction: when a subscriber queue fills (>10 events) and is removed from _subscribers, now puts a None sentinel into it so the SSE handler unblocks and closes the connection, letting EventSource auto-reconnect. Without this the connection stayed open but received no further events. * fix: test isolation — set HERMES_WEBUI_TEST_STATE_DIR in conftest The gateway sync tests write directly to state.db and must use the same path the test server reads from. Previously they computed the path independently, which broke when test_auth_sessions.py set a different HERMES_WEBUI_STATE_DIR in the test-process environment at import time. tests/conftest.py: - Set HERMES_WEBUI_TEST_STATE_DIR=TEST_STATE_DIR in the test process's os.environ (via setdefault) so gateway tests can read it reliably. Using setdefault preserves any explicit override the caller may pass. tests/test_gateway_sync.py: - Simplify _get_test_state_dir(): check HERMES_WEBUI_TEST_STATE_DIR first (now reliably set by conftest), fall back to HERMES_HOME/webui-mvp-test. Remove the workaround that tried to snapshot HERMES_HOME at import time. Result: 658/658 tests pass in full-suite ordering (was 651 pass / 7 fail). --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |