Commit graph

3691 commits

Author SHA1 Message Date
nesquena-hermes
60369cab69
Merge pull request #3428 from nesquena/release/stage-p2d
Release GI — v0.51.215 (deduplicate legacy messages in append-only merge)
2026-06-02 12:27:50 -07:00
nesquena-hermes
9aee98eef6 docs(changelog): v0.51.215 release header for #3393 legacy-dedup fix 2026-06-02 19:20:44 +00:00
nesquena-hermes
1d9b2ed730 fix: deduplicate legacy messages in merge_session_messages_append_only (#3393, @thanhtoantnt)
Adds _session_message_dedup_key (full-precision timestamp) so true duplicates
(same role + content + EXACT timestamp) fold, while legitimately-repeated
identical turns with sub-second-distinct timestamps survive — avoiding the
#3268 data-loss class. Wired into both the no-sidecar path and the merge loop's
seen_dedup_keys guard. Closes #3346.

Co-authored-by: thanhtoantnt <thanhtoantnt@users.noreply.github.com>
2026-06-02 19:20:23 +00:00
nesquena-hermes
a1d44d6482
Merge pull request #3426 from nesquena/release/stage-p2c
Release GH — v0.51.214 (preserve loaded transcript width on same-session external refresh)
2026-06-02 12:17:53 -07:00
nesquena-hermes
3603027b74 test: update loadSession source-assertion tests for #3326 conditional renderMessages arg
#3326 changed loadSession's INFLIGHT + idle render calls from bare
renderMessages() to renderMessages(sameSessionForceReload?{preserveScroll:true}:undefined),
and added reload-width-hint handling inside _ensureMessagesLoaded. Updated 5
source-pinning tests to match the new (behaviorally-equivalent) call form:
- test_regressions / test_issue2341: match 'renderMessages(' call form
- test_parallel_session_switch: match call form + widen 600->850 block window
- test_issue1690: assert the CONDITIONAL preserveScroll (cross-session still bottom-pins)
- test_issue3162: widen _ensureMessagesLoaded slice 2000->2600
2026-06-02 19:11:15 +00:00
nesquena-hermes
d9c1777469 docs(changelog): v0.51.214 release header for #3326 transcript-width fix 2026-06-02 18:53:31 +00:00
nesquena-hermes
7c4c8120a3 fix: preserve loaded transcript width on same-session external refresh (#3326, @viraatdas)
Capture a same-session force-reload hint (loaded renderable/message counts,
known count, truncation flag) BEFORE clearing the in-memory transcript, so the
authoritative reload requests a width that preserves what was loaded instead of
collapsing a long session to the default 30-message tail window mid-read.
Same-session force reloads render with preserveScroll. Resolves conflict with
the shipped #3306 carry-forward snapshot by keeping both (complementary).
Closes #3239.

Co-authored-by: viraatdas <viraatdas@users.noreply.github.com>
2026-06-02 18:53:12 +00:00
nesquena-hermes
e156cbf356
Merge pull request #3423 from nesquena/release/stage-p2b
Release GG — v0.51.213 (keep gateway context visible in chat transcripts)
2026-06-02 11:50:07 -07:00
nesquena-hermes
ebfff8e074 fix(stream): preserve repeated identical visible turns in gateway backfill (Codex MUST-FIX on #3300)
_merge_display_messages_after_agent_result shared one _emitted set between
context-backfill rows and the visible-display backbone; _message_identity
ignores timestamps, so a second identical visible user turn was suppressed as
already-emitted. Now the display backbone is always appended in order; a
separate _context_inserted set + the _display_id_set guard dedupe only the
spliced-in context rows. Adds a regression test with two identical visible
user turns + a context-only gap.
2026-06-02 18:43:25 +00:00
nesquena-hermes
f07629bb4e docs(changelog): v0.51.213 release header for #3300 gateway transcript fix 2026-06-02 18:36:21 +00:00
nesquena-hermes
952930c10e fix: keep gateway context visible in chat transcripts (#3300, @AJV20)
Backfill model-context turns into the visible transcript before saving the
latest reply while keeping hidden [context compaction] markers out of the
visible transcript, so sidebar/header message counts no longer collapse a
compacted gateway session to a 2-message conversation. Resolves the
transcript-loss regression the v0.51.197 Codex gate flagged on the original.

Co-authored-by: AJV20 <24819659+AJV20@users.noreply.github.com>
2026-06-02 18:28:41 +00:00
nesquena-hermes
ab05670daf
Merge pull request #3416 from nesquena/release/stage-batch2
Release GF — v0.51.212 (i18n regenerate-title + self-restart argv + todos cold-load)
2026-06-02 10:44:10 -07:00
Vanshaj Pahwa
39a9029932 i18n: add CHANGELOG entry for session_title_regenerate* localization (#3364) 2026-06-02 17:34:48 +00:00
Vanshaj Pahwa
cdde338fc2 i18n: translate session_title_regenerate* keys in non-English locales (#3364)
The "Regenerate title" session-menu action (#3223) added five i18n keys
(session_title_regenerate, _desc, _regenerating, _regenerated,
_regenerate_failed). They shipped as English text in every non-English
locale block.

Translate all five keys across it, ja, ru, es, de, zh, zh-TW, pt, ko,
fr and tr, matching each locale's existing terminology (e.g. the word
used for "title" in session_rename_desc). zh and zh-TW are written as
\u escapes to match the existing style of those blocks.

Closes #3364
2026-06-02 17:33:49 +00:00
Qi Zhou
5c595c08d6 fix(todos): hydrate cold-load state from session snapshot 2026-06-02 17:33:49 +00:00
PatrickNoFilter
1402b56155 fix(updates): re-exec correctly in both source and frozen builds
You were right — the original analysis was inverted. The unprefixed
form (sys.argv only) is the correct fix for frozen/packaged builds
(PyInstaller, zipapp, etc.) where sys.argv[0] == sys.executable ==
<binary>; master's [sys.executable] + sys.argv form re-inserts the
binary as argv[1] in that case, which the interpreter then tries to
parse as the script to run — leading to a recursive-reexec that
never reaches bind().

But for a source checkout launched as `python server.py` via
bootstrap.py / ctl.sh / start.sh, sys.argv[0] is the SCRIPT path and
sys.executable is the interpreter. CPython treats argv[1] as the
script to run, so we must pass [sys.executable] + sys.argv — the
canonical CPython re-exec idiom.

The two cases are mutually exclusive on argv shape, so a flat swap
can't be right for both. Distinguish with sys.frozen (set by
PyInstaller / zipapp / similar) and pick the right form per case.

Also reworded the last-resort except comment — the prior
"(e.g. frozen binary)" parenthetical was misleading; this branch is
the unconditional fallback for any execv failure, not specifically
the frozen case.
2026-06-02 17:33:49 +00:00
nesquena-hermes
f12608487f
Merge pull request #3415 from nesquena/release/stage-batch1
Release GE — v0.51.211 (reasoning heuristics + /model shortest-match + Copilot env-token filter)
2026-06-02 10:28:27 -07:00
Vanshaj Pahwa
63442869d6 Add CHANGELOG entry for /model prefix-match fix (#3368) 2026-06-02 17:12:07 +00:00
Vanshaj Pahwa
0ea5e4ef69 Fix model selection shadowing shorter model names (#3368)
The /model slash command's fuzzy fallback used a substring match that
took the first option containing the query, so "/model mimo-v2.5"
selected "mimo-v2.5-pro" instead of "mimo-v2.5".

Replace both fallback loops with a shared _bestModelMatch helper that
prefers an exact id/label match and otherwise returns the shortest
option containing the query, so a more specific query is no longer
shadowed by a longer variant. The longer variant is still selectable by
querying its full name.

Closes #3368
2026-06-02 17:11:37 +00:00
b3nw
68716b3e4f fix: generalized reasoning capability heuristics for custom providers (#3377) 2026-06-02 17:11:32 +00:00
happy5318
2892d3d6c0 fix: also filter GH_TOKEN env var from Copilot credential pool
Follow-up to PR #3382 review feedback. There are three GitHub-related
env vars that can be auto-detected:

- COPILOT_GITHUB_TOKEN: user-specific config, should NOT be filtered
- GITHUB_TOKEN: classic PAT, should be filtered (already done)
- GH_TOKEN: another env var, should also be filtered (this commit)

Use a frozenset (_AMBIENT_GH_ENV_SOURCES) to clearly define which
env var sources are ambient vs user-configured.
2026-06-02 17:11:32 +00:00
happy5318
02a1849ca4 fix: filter GITHUB_TOKEN env var from Copilot credential pool
Copilot API does not support classic PATs (ghp_*), only OAuth tokens (gho_*)
or fine-grained PATs (github_pat_*). When GITHUB_TOKEN env var contains a
classic PAT, it should be filtered from the credential pool to prevent
Copilot from appearing in the model picker dropdown.

The _is_ambient_gh_cli_entry() function now also filters entries with
source='env:GITHUB_TOKEN', treating them as ambient/auto-detected rather
than user-explicitly-added credentials.

Fixes issue where Copilot group appeared in WebUI model picker even when
gh auth token was an unsupported classic PAT.
2026-06-02 17:11:32 +00:00
nesquena-hermes
4baa26bb5b
Merge pull request #3380 from nesquena/release/stage-batch1
Release v0.51.210 — Release GD (stage-batch1: model-picker multi-slash fix + extensionless preview highlighting)
2026-06-01 22:23:26 -07:00
nesquena-hermes
7e97cdc335 docs: CHANGELOG for v0.51.210 (stage-batch1) 2026-06-02 05:08:44 +00:00
AJV20
6234473be4 fix: highlight extensionless workspace code files
(cherry picked from commit 3e61de7a734e46fbc5aa4820517ac2c7e6edad7d)
2026-06-02 05:08:30 +00:00
b3nw
d06776a4c8 fix: model picker snaps to wrong model with multi-slash IDs (#3360)
When a custom/proxy provider serves models whose IDs share the same base
name across vendor prefixes (e.g. vendor_a/deepseek/deepseek-v4-pro vs
vendor_b/deepseek/deepseek-v4-pro), several normalization functions use
split('/').pop() (or split('/')[-1]) which discards all segments except
the last.  This causes three user-facing symptoms: (1) clicking one
model selects a different colliding model, (2) configured-model badges
attach to the wrong dropdown entry, and (3) the model-chip label in the
composer bar is truncated to just the base model name.

Root cause: all three callers take only the last slash-segment instead
of stripping only the first (provider) segment and preserving the
remaining vendor hierarchy.

Fix 1 — _findModelInDropdown (static/ui.js): Move the exact string match
before the provider-aware normalized match.  Previously, when all models
share the same provider ID (common with LLM proxy setups), the normalized
match returned whichever colliding option appeared first in DOM order,
even though an exact match existed.

Fix 2 — _normalizeConfiguredModelKey (static/ui.js) and _norm_model_id
(api/config.py): Replace split('/').pop() / split('/')[-1] with a first-
segment-only strip (regex on frontend, split('/',1) on backend), matching
the strategy already used by _findModelInDropdown's norm lambda.  This
prevents multi-slash IDs from colliding in badge assignment and the
configured-entry dedup set.  Additionally, strip colon-qualified provider
prefixes (e.g. custom:name/) before the slash strip so badge-key variants
like 'custom:llm-proxy/opencode_go/model' merge correctly with the bare
'opencode_go/model' in the configured section dedup.

Fix 3 — getModelLabel (static/ui.js) and _get_label_for_model
(api/config.py): Same split('/').pop() to first-segment-strip change so
the composer-bar model chip and backend label preserve vendor context
(e.g. shows 'opencode_go/deepseek-v4-pro' instead of 'deepseek-v4-pro').

Verification: 9 new regression tests (test_issue3360) covering exact-
match priority, multi-slash normalization, and backend/frontend parity.
Updated 1 existing test (test_norm_model_id_trailing_empty_guard) that
asserted the old split('/').pop() pattern.  All 25 related tests pass.

AI Usage: Gemini (gemini-2.5-pro), via Antigravity IDE, pair-programmed.

(cherry picked from commit a454fecd2b3a83f7da34473c883be069171aeac9)
2026-06-02 05:08:30 +00:00
nesquena-hermes
866969161e
Merge pull request #3376 from nesquena/review/pr-2622
Release GC — v0.51.209 (#2622 WebUI dashboard plugin system with iframe isolation)
2026-06-01 21:14:00 -07:00
nesquena-hermes
4b0c0c9c25 release: stamp v0.51.209 (Release GC — dashboard plugin system) 2026-06-02 04:09:42 +00:00
nesquena-hermes
be4496d23f fix(#2622): plugin card UX — legible Open button, single badge, visible toggle + reject protocol-relative tab.path
Nathan screenshot feedback on the Plugins card:
- Open button rendered as a yellow block with INVISIBLE text: --accent-text
  resolves to the same gold as --accent in the default theme (text==bg). Switched
  to a ghost/outline button (accent text + border on the card surface; fills on
  hover) — always legible regardless of theme.
- Removed the redundant DOUBLE 'Enabled' badge (the dashboard-specific badge
  duplicated the generic activation badge; kept the generic one).
- Toggle slider knob was hard to see on the gold 'on' state; added a drop shadow.
Also Opus SHOULD-FIX: _VALID_PLUGIN_TAB_PATH now rejects a leading '//'
(protocol-relative URL → remote origin in iframe.src). Test updated.
2026-06-02 03:58:02 +00:00
nesquena-hermes
63dd0c35e7 fix(#2622): asset route only serves built dist/ assets, not plugin source/config (Codex CORE)
Codex CORE (verified live): _PLUGIN_STATIC_ROOTS points at dashboard/ and
serve_plugin_static() served ANY file beneath it — so /dashboard-plugins/<name>/
plugin_api.py leaked plugin backend SOURCE, and manifest.json / .env were
reachable too. Now constrained: served path must be under dist/ or static/, no
dotfiles in any path segment, and a static-extension allowlist (refuses .py/.json/
.env/.toml/.sh etc.). Verified: plugin_api.py, manifest.json, dist/.env,
dist/config.py all 404; dist/app.js still serves. Regression test added.
2026-06-02 03:47:39 +00:00
nesquena-hermes
816a4a93f9 harden(#2622): DOM-bound plugin handlers + tab.path validation (Codex round-2)
Codex found two more once the config-guard bug was fixed (Opus concurred on #2):
1. panels.js _buildPluginCard built the Open button + enable toggle with inline
   onclick/onchange that interpolated tab.path / plugin.key into a JS-string-in-
   attribute context — HTML-escaping is insufficient there (quote breakout).
   Now rendered inert + bound via addEventListener with RAW closure values.
2. tab.path was unvalidated. Added _VALID_PLUGIN_TAB_PATH (^/[A-Za-z0-9._~/-]{0,255}$)
   in load_plugins() — absolute, no quotes/query/fragment/control chars.
Also Opus nit: deep-merge now coerces dashboard_plugins values to bool + str keys.
Regression tests added for both.
2026-06-02 03:39:41 +00:00
nesquena-hermes
944e739b40 fix(#2622): restore settings allowlist guard (Opus HALT) + validate plugin name
CRITICAL (Opus HALT on prior commit): the PR's edit to save_settings() replaced
'if k in _SETTINGS_ALLOWED_KEYS' with 'if k=="dashboard_plugins": continue' and
orphaned the whole validation body under the continue. Effects: (a) settings save
broken for every key except dashboard_plugins; (b) the allowlist security boundary
gone -> any client key (password_hash, signing_key_*) became settable. Restored the
guard + correct indentation; dashboard_plugins handled by the deep-merge above.
Verified in-process: language persists, password_hash/signing_key injection
rejected, dashboard_plugins still deep-merges.

Also (Opus SHOULD-FIX #3): validate plugin name against ^[a-z][a-z0-9_-]{0,63}$
so a manifest name like '../foo' can't make the URL-space ambiguous.

Regression tests added for both (the allowlist bug had ZERO coverage).
2026-06-02 03:22:25 +00:00
nesquena-hermes
f92373efd2 test(#2622): regression coverage for plugin asset isolation + enable-gate
4 tests: opt-in default-off, enable-gate reads settings, asset route sends
sandbox CSP + nosniff, both routes enforce the enable-gate server-side.
2026-06-02 03:15:01 +00:00
nesquena-hermes
6f9d455348 fix(#2622): harden plugin asset isolation + server-side enable-gate + i18n key
Deep-review (Opus MUST-FIX + Codex kick-back) findings, all confirmed with repros:

1. Same-origin XSS via direct nav to a plugin's raw .html/.svg asset: the
   /dashboard-plugins/<name>/ route served plugin-controlled HTML with text/html
   at the WebUI origin and NO sandbox header (only the in-panel iframe + the
   page route were sandboxed). Verified: <script>alert(document.cookie)</script>
   in a plugin html ran same-origin. Fix: send 'Content-Security-Policy: sandbox
   allow-scripts allow-forms allow-popups' + 'X-Content-Type-Options: nosniff'
   on the asset response (null-origin, same as the page route).

2. 'Disabled' was UI-only: toggling a plugin off just hid the Open button; its
   page + asset URLs kept serving. Fix: new _dashboard_plugin_enabled() gates
   BOTH the asset route and the page route server-side (opt-in, default off,
   disabled => 404).

3. i18n: panels.js referenced t('plugins_enable_toggle') but the PR defined a
   mismatched English-only 'settings_plugins_enable_toggle' (dead key) — toggle
   label fell back to literal AND tripped the locale-parity gate. Renamed to
   plugins_enable_toggle and added to all 12 locales (zh-Hant gets Traditional
   啟用, not Simplified).
2026-06-02 03:12:58 +00:00
nesquena-hermes
852a776823 review(#2622): lint cleanup + harden two no-op tests
- Remove unused imports (importlib, sys in api/plugins.py; pytest in test).
- Two tests asserted 'X or True' (always pass — meaningless). Rewrote them to
  actually verify the /plugins/ route allowlists plugin.css + uses relative_to
  traversal guard, and that manifest fields are html.escape()'d before IIFE-shell
  interpolation. Dropped a dead 'original_plugins' local for a real dict assertion.
2026-06-02 02:51:07 +00:00
nesquena-hermes
bf3ff69c5e feat(plugins): add WebUI dashboard plugin system with iframe isolation (#2622, @pix0127) 2026-06-02 02:48:11 +00:00
nesquena-hermes
3cafec3004
Merge pull request #3372 from nesquena/hotfix/upload-hardening
Release GB — v0.51.208 (workspace upload hardening hotfix)
2026-06-01 19:38:37 -07:00
nesquena-hermes
2ab0b56078 refine(hotfix): Opus SHOULD-FIX — drop redundant equality clause + tighten symlink test
- The is_relative_to() check already covers the workspace==target equality case
  (is_relative_to(A,A) is True), so the '!= workspace' prefix was redundant.
- Symlink test now asserts a hard 403 (proves the guard fires, not just that
  nothing leaked) and cleans up the out-of-workspace escape dir in a finally.
2026-06-02 02:28:44 +00:00
nesquena-hermes
f112d8aa9a fix: keep parse_multipart self-contained (local MAX_UPLOAD_BYTES import)
test_sprint1's parse_multipart tests exec() the function's source in an isolated
namespace with only re/email.parser imported; referencing the MAX_UPLOAD_BYTES
module global there NameErrored. Import it locally inside the function (with a
defensive fallback) so the function is self-contained and the isolated-exec
tests pass.
2026-06-02 02:27:21 +00:00
nesquena-hermes
1702a7604c lint: raise ... from None for the Content-Length parse guard (B904) 2026-06-02 02:20:43 +00:00
nesquena-hermes
fbcae5f71e fix: harden workspace upload surface (#3104 follow-up hotfix)
Codex regression-gate findings on the shipped #3104 upload code, each verified
with a repro and fixed:

1. Negative Content-Length bypassed the size cap → unbounded rfile.read(-1).
   The per-handler 'content_length > MAX_UPLOAD_BYTES' check is False for a
   negative value, so the guard is now centralized in parse_multipart()
   (validates [0, MAX_UPLOAD_BYTES]) — protects all four upload handlers.
2. .tar/.tbz2/.txz uploads silently skipped extraction (is_archive suffix set
   was narrower than extract_archive's) → now matches.
3. Rejected archives (zip-slip/zip-bomb/corrupt/too-many-members) showed a
   misleading 'Uploaded' success toast → workspace.js now surfaces extract_error.
4. An in-workspace symlink subpath let mkdir/writes escape the workspace root →
   target_dir is now required to be is_relative_to(workspace) before mkdir.

Regression tests added (negative+oversize CL, .tar extraction, symlink target).
2026-06-02 02:20:05 +00:00
nesquena-hermes
36faa02364
Merge pull request #3369 from nesquena/review/pr-2931
Release GA — v0.51.207 (#2931 Edge TTS as an alternative speech engine)
2026-06-01 19:15:12 -07:00
nesquena-hermes
08f8aba89d release: stamp v0.51.207 (Release GA — Edge TTS speech engine) 2026-06-02 02:10:43 +00:00
nesquena-hermes
8164c42f94 fix(#2931): restore dropped console.error in top-level boot catch (Codex SILENT finding)
The PR silently removed 'console.error([hermes] boot failed, e)' from the
top-level boot .catch() that exists on master, so caught boot-path failures
(session restore / inflight recovery / gateway startup) would no longer surface
in the console or the browser-smoke gate. Restored to match master.
2026-06-02 02:05:56 +00:00
nesquena-hermes
a1a142f4f7 test(#2931): pin auth OFF in the endpoint tests for suite-order determinism
The 2 endpoint tests passed in isolation but flaked in the full suite: a prior
test can leave is_auth_enabled() True globally, so the no-cookie requests got a
401 before reaching the method/length/voice/rate-limit guards under test. The
autouse fixture now monkeypatches api.auth.is_auth_enabled -> False (the handler
imports it at call time) so the validation-path assertions are deterministic.
2026-06-02 02:04:56 +00:00
nesquena-hermes
08fe4f51f0 fix(#2931): Edge TTS playback path (GET->POST), stoppable hands-free audio, test isolation
Both advisors (Opus MUST-FIX + Codex SHIP-ONLY-WITH-FIXES) caught that the Edge
TTS playback was broken despite the settings selector working:

1. ui.js _playEdgeTts used new Audio('/api/tts?text=...') — a GET — but /api/tts
   is POST-only (405) and registered only in handle_post. The per-message speaker
   button + auto-read silently failed in edge mode, and the GET leaked message
   text into the query string/access log. Rewritten to POST JSON + blob object
   URL (mirrors the working boot.js path), and now surfaces server errors
   (503 not-installed, 429 rate-limit) via toast instead of silent dead air.
2. boot.js hands-free Edge audio was a local var never assigned to the shared
   _playingEdgeAudio handle, so stopTTS() (from _deactivate) couldn't stop it.
   Now registered + cleared on end/error.
3. Test isolation: the _tts_limiter function-attribute singleton persisted across
   the whole suite, flaking 2 of my endpoint tests in the full run. Converted the
   reset to an autouse fixture (before+after each) + unique per-test client IPs.
2026-06-02 01:58:13 +00:00
nesquena-hermes
6801272f51 review(#2931): make edge-tts an optional dependency + add endpoint test coverage
Maintainer review changes on the Edge TTS PR:
- edge-tts was added as a HARD base requirement, but the /api/tts handler is
  designed optional (ImportError returns 503). Moved it out of requirements.txt
  with an OPTIONAL comment + install hint; updated the 503 message from the
  nonexistent 'see docs' to the actual install command. Keeps minimal base deps.
- PR shipped NO tests for a new auth+rate-limited+allowlisted network endpoint.
  Added tests/test_issue2931_edge_tts_endpoint.py covering method (405), missing
  text (400), over-length (400), voice allowlist (400), per-client rate limit
  (429) — all in-process, no real synthesis/network.
2026-06-02 01:28:35 +00:00
nesquena-hermes
1c29d6ac9c feat: add Edge TTS as alternative speech engine (#2931, @liuqiangweb-svg) 2026-06-02 01:21:39 +00:00
nesquena-hermes
f0c3668d97
Merge pull request #3367 from nesquena/review/pr-3104
Release FZ — v0.51.206 (#3104 workspace file upload + drag-and-drop with archive extraction)
2026-06-01 18:18:50 -07:00
nesquena-hermes
bb9b8054b1 release: stamp v0.51.206 (Release FZ — workspace file upload) 2026-06-02 01:15:43 +00:00