Commit graph

2033 commits

Author SHA1 Message Date
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
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
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
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
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
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
9690725f8e harden(#3104): archive member-count cap + bounded extraction-dir dedup (Opus SHOULD-FIX)
Opus review SHOULD-FIX on the upload surface:
- Add _MAX_ARCHIVE_MEMBERS=10000 cap in extract_archive (both zip + tar loops):
  a tiny archive with millions of members slips under the byte cap but can
  exhaust inodes/fds. Trips before extraction, cleaned up via the existing
  rmtree-on-exception. Regression test added.
- Bound the extraction-dir collision-suffix loop (was while-True) to 1000 tries.
Other Opus SHOULD-FIX items (member-count #1 done; #2 done) filed as follow-up
or N/A: same-field multi-file collapse doesn't apply (frontend sends one request
per file); .tar.gz stem cosmetic.
2026-06-02 00:52:50 +00:00
nesquena-hermes
d0917f2b24 fix(#3104): correct dedup filename reporting + make zip-bomb cap testable
Two issues caught by the PR's own tests against the out-of-process test server:
1. Dedup reporting bug: after a filename collision the file was correctly
   written to e.g. report-1.pdf, but the JSON response reported the ORIGINAL
   name (safe_name) — now reports dest.name. (Real user-facing bug.)
2. Zip-bomb cap was untestable: the test monkeypatched _MAX_EXTRACTED_BYTES in
   the pytest process, which has no effect on the separate server process where
   extraction runs. Made the cap env-configurable (HERMES_WEBUI_MAX_EXTRACTED_MB,
   read at call time via _max_extracted_bytes(); defaults to 10x upload cap),
   set it to 5MB in the conftest server env, and rewrote the test to upload a
   compressible archive that genuinely extracts past the cap. Also asserts no
   partial extraction dir is left behind.

Plus lint: unused field_name loop var -> _field_name, unused os import in test.
2026-06-02 00:41:11 +00:00
nesquena-hermes
ff81591e8e feat(workspace): add file upload + drag-drop with archive extraction (#3104, @antoniocarlos97ss) 2026-06-02 00:26:53 +00:00
nesquena-hermes
8ce02caf11 iterate(stage-hi1): Nathan UX feedback — #3220 clean image+hover-download, #3337 uniform code bg, #3223 menu clip fix
#3220: redesign generated-image rendering from a permanent bordered card
(filename + Open/Download buttons) to a clean inline image with click-to-zoom
lightbox + a hover/focus-revealed Download overlay, matching ChatGPT/Claude/
Gemini. Drops redundant Open (lightbox already covers it). Tests updated.

#3337: fix two-tone code background — Prism's prism-tomorrow theme styled the
parent <pre> gray while the <code> was navy var(--code-bg), so dark theme showed
a gray frame around navy code. Override BOTH .preview-code[class*=language-] and
its <code> to var(--code-bg) (mirrors the chat code-block fix at .msg-body pre).

#3223: fix action-menu clipping — the new Regenerate-title row made the 9-item
menu tall enough to overflow the viewport bottom when opened on a top-anchored
row at short viewports (e.g. 1280x720). _positionSessionActionMenu now clamps
the menu within both viewport edges and caps max-height with scroll when the
menu is taller than the viewport.
2026-06-01 22:48:01 +00:00
nesquena-hermes
c1156b4c67 fix(#3337): prevent Prism highlight leaking across workspace files
Maintainer fix on stage (browser-test catch): Prism.highlightElement()
propagates the language-* class onto the parent <pre>, so previewing a
.css file then a .txt file rendered the plain text with CSS grammar.
Strip any stale language-* from #previewCode before each render and only
call highlightElement when a language was assigned. Adds regression test
(tests/test_issue3337_workspace_preview_highlight.py) and bumps the
_openSessionActionMenu scan window in test_1466 to cover the new
Regenerate-title action lines (#3223).
2026-06-01 21:56:26 +00:00
nesquena-hermes
546afe8374 feat: add manual session title regeneration (#3223, @AJV20)
Closes #3106. Adds /api/session/title/regenerate endpoint + session-action
menu item. Preserves chronology (touch_updated_at=False), guards read-only
and imported sessions, syncs to state.db when Insights sync enabled.

Maintainer refinement (Opus SHOULD-FIX): scope the is_imported guard to the
regenerate action only instead of broadening the shared _isReadOnlySession()
helper, which also gates rename/pin/archive/move/fork. Matches the backend
403 guard. Test updated to assert the scoped shape.
2026-06-01 21:39:51 +00:00
nesquena-hermes
949dc7aac8 feat: render generated media artifact cards (#3220, @AJV20)
- CHANGELOG entry moved to ### Added (feature, not fix)
- zh-Hant locale: use Traditional 開啟/下載 instead of simplified forms
2026-06-01 21:21:32 +00:00
nesquena-hermes
c5d4806a64 fix(#3331): gate findings — empty-session profile retag, root-alias project filter, create-profile validation
Codex+Opus gate findings on the profile-scoping PR:
1. panels.js: retag S.session.profile on ANY profile switch (was inside the
   if(data.default_model) block, so model-less profile switches left a stale chip).
2. sessions.js: project-picker filter now mirrors the server's root-alias
   tolerance (default <-> renamed-root) so a server-approved 'default' project
   isn't hidden for a renamed-root session.
3. routes.py /api/projects/create: validate the optional client-supplied profile
   via _PROFILE_ID_RE before stamping (was trusting raw client input -> could
   create hidden cross-profile rows). Updated the PR's string-assertion test.
2026-06-01 21:05:32 +00:00
nesquena-hermes
3cf4d15dc6 fix: align project/session operations with session profile instead of global active profile (#3331)
Co-authored-by: PINKIIILQWQ <PINKIIILQWQ@users.noreply.github.com>
2026-06-01 20:57:40 +00:00
nesquena-hermes
d09f2e4efb feat: sticky manual unpin for streaming chat scroll (#3343)
Supersedes the v0.51.199 proximity-re-pin (#3330) and the #3250 upward-intent
timeout with a sticky-unpin model (ChatGPT/Claude/Codex behavior): scroll up =
stay put until you return to the bottom or click the scroll-to-bottom control.
Reconciled against the shipped #3330 code: removed the now-dead
_recentMessageUpwardIntent reference from the #3319 rAF retry, kept the
load-time -Infinity intent-init fix, kept the pinned-only >500 catch-up.

Co-authored-by: pamnard <pamnard@users.noreply.github.com>
2026-06-01 20:34:06 +00:00
nesquena-hermes
d40c2e38d0 test: drop unused 're' import in test_live_stream_ux (ruff F401) 2026-06-01 20:21:40 +00:00
nesquena-hermes
7d2be7f52c fix(#3321): drop provider_details_label recovery-control heuristic (over-filtered genuine interruptions)
Codex+Opus regression-gate finding: _isRecoveryControlMessage /
_streamRecoveryControlMessage fell back to matching
provider_details_label==='interruption details'. But a GENUINE 'Response
interrupted' card (Stop button, real provider crash) carries that exact label,
so the filter would drop a real user-facing interruption from the transcript on
the next render/restore — the inverse of the #3300 data-loss class. Require the
explicit server-set recovery_control marker; keep only the two fully-anchored
synthetic-text matches for pre-marker backward-compat. Adds a node-driven
regression test (revert-verified) asserting a label-only interruption card and a
real user turn stay visible while marker + strict text are filtered.
2026-06-01 20:14:29 +00:00
nesquena-hermes
f50b4fc2fa fix: filter interrupted recovery control text from visible transcript (#3321)
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-01 20:07:16 +00:00
nesquena-hermes
94ad2a6b8d docs: v0.51.201 release header + fix #3336 tool-card diff test extraction list 2026-06-01 19:54:45 +00:00
nesquena-hermes
042294e2a1 test: update sessions.js string-assertions for #3313 carry-forward rename
#3313 applies the ephemeral-field carry-forward at all 5 wholesale S.messages
replace sites (loadSession/_ensureMessagesLoaded/_loadOlderMessages/
_ensureAllMessagesLoaded/startGatewaySSE), renaming the replace RHS from
msgs/next to _msgsToAssign/_nextToAssign. These pre-existing tests pinned the
old literals; updated to match the new RHS (or made LHS-agnostic) while
preserving the invariants they protect (bump-before-replace, session-switch
guard, shorter-transcript guard).
2026-06-01 19:46:36 +00:00
nesquena-hermes
0cae7c644c fix: preserve ephemeral turn fields when loadSession force-reloads (#3313)
Co-authored-by: Sanjays2402 <Sanjays2402@users.noreply.github.com>
2026-06-01 19:38:00 +00:00
nesquena-hermes
329aae6979 fix: probe remote gateway via HERMES_API_URL before local fallback (#3312)
Rebased onto master + removed an unused 'io' import flagged by the ruff gate.

Co-authored-by: Sanjays2402 <Sanjays2402@users.noreply.github.com>
2026-06-01 19:38:00 +00:00
nesquena-hermes
04ce7599e3 stage-batch11: #3330 pinned-scroll + #3311 inline-math currency
#3330 Fix pinned chat scroll after message rebuild
Co-authored-by: jianongHe <jianongHe@users.noreply.github.com>

#3311 fix: reject inline math when $ is followed by a digit (currency)
Co-authored-by: toanalien <toanalien@users.noreply.github.com>
2026-06-01 19:08:30 +00:00
nesquena-hermes
de22c607bc stage-batch10: #3327 model-id normalize + #3334 RFC slice doc + #3341 profile skill counts
#3327 fix(reasoning): normalize custom-provider model ids for fallback heuristics
Co-authored-by: Carry00 <Carry00@users.noreply.github.com>

#3334 docs(rfc): mark run-adapter Slice 4f shipped, define Slice 4g gate
Co-authored-by: Michaelyklam <Michaelyklam@users.noreply.github.com>

#3341 fix(profiles): show enabled vs compatible skill counts
Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-06-01 18:24:05 +00:00
nesquena-hermes
bce1f9430b fix(#3102): don't let persisted watermark permanently drop future state.db recovery rows
Codex regression-gate finding: since Session.save() no longer auto-clears the
truncation_watermark, the unconditional 'timestamp > watermark' skip in
merge_session_messages_append_only became a permanent ceiling — a genuine future
state.db-only row (recovery/compaction, missed by the sidecar) would be silently
dropped from /api/session and model-context reconstruction forever. Only apply the
above-watermark skip while the sidecar has NOT advanced past the watermark. Preserves
the #2914 deleted-tail filtering (revert-verified). Adds 2 regression tests.
2026-06-01 18:14:48 +00:00
nesquena-hermes
778652713c Fix #2914: stop agent replaying edited/undone messages; sync context_messages truncation (#3102)
Co-authored-by: AlexeyDsov <AlexeyDsov@users.noreply.github.com>
2026-06-01 18:05:02 +00:00
nesquena-hermes
1c4365ce2a fix(artifacts): guard malformed tool_calls entries in collectSessionArtifacts (#3329)
Codex regression-gate finding: the OpenAI tool_calls loop dereferenced
tc.function with no null/type guard, so a persisted message.tool_calls
array containing a null or non-object entry would throw and abort artifact
collection. Mirror the existing tool_use-block guard. Adds a node-driven
regression test.

Co-authored-by: mysoul12138 <mysoul12138@users.noreply.github.com>
2026-06-01 17:27:02 +00:00
Sanjay Santhanam
92bddca3c7 fix: file manager falls back to state.db for external Telegram/CLI sessions (#3280) 2026-06-01 17:19:48 +00:00
AJV20
ec704356ac fix: hide attachment path markers in chat UI 2026-06-01 05:31:53 +00:00
AJV20
d8471a7d87 fix(profiles): avoid config import cycle 2026-06-01 05:05:16 +00:00
AJV20
ca99d12e04 fix: keep WebUI reply guidance language-neutral 2026-06-01 04:42:23 +00:00
AJV20
7a1b2bfe72 fix: carry WebUI reply polish into gateway chat 2026-06-01 04:42:23 +00:00
xz-dev
239f913485 Fix workspace open in browser inline sandbox 2026-06-01 04:42:05 +00:00
AJV20
18eea5b2ab test(ctl): allow dotenv-free launches 2026-06-01 04:42:05 +00:00
nesquena-hermes
099dcf52fe test: point gateway_chat doc-contract test at docs/advanced-chat-setup.md
The README IA pass moved the Gateway-backed-chat operator docs out of the README
into docs/advanced-chat-setup.md (niche self-hosted feature). The contract test
test_gateway_chat_health_payload_is_documented_as_operator_diagnostic_only
asserted those phrases live in README.md — update it to follow the content to its
new home (CHANGELOG entry unchanged). Contract test moves with the content per
docs/CONTRACTS.md. Full suite: 7138 passed, 0 failed.
2026-06-01 02:36:25 +00:00
nesquena-hermes
7e9f674edf fix(#3263): provider-compatible default match — reject same-bare-name on different providers (Codex over-match MUST-FIX)
Prior round stripped provider prefixes from both sides and matched bare-only,
which over-matched: openai/gpt-4o would match default openrouter/gpt-4o. Now the
matcher compares bare model ids AND rejects when both sides identify DIFFERENT
providers (from provider/ prefix, @provider: qualifier, or the explicit provider
arg). Same-provider / unknown-session-provider still match. Added cross-provider
rejection regression tests.

Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
2026-06-01 01:19:49 +00:00
nesquena-hermes
4147e7f3df fix(#3263): provider-aware default-model match (Codex final-gate MUST-FIX)
The default-only context_length guard compared model.default to the session
model with exact string equality. But model.default and the session model can
be stored in equivalent-but-different shapes (bare 'claude-opus-4.8',
provider-prefixed 'anthropic/claude-opus-4.8', or '@anthropic:claude-opus-4.8').
An exact compare wrongly treats the actual default model as non-default and
drops its configured context_length cap for provider-prefixed configs.

Add api/routes._model_matches_configured_default(session_model, cfg_default,
provider) that normalizes all three shapes, and use it at all 6 guard sites
(routes resolver + the 5 api/streaming.py sites: live-usage snapshot, persistence
_skip_cc_cl, persistence fallback _apply_cfg_ctx, SSE-done _dropped_stale_cap_sse,
SSE fallback _apply_cfg_ctx). Imported function-scoped in streaming to avoid the
routes<->streaming module-level circular import. 10 helper unit tests + a
behavioral test that a prefixed default still receives its cap.

Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
2026-06-01 01:14:22 +00:00
nesquena-hermes
17e7794923 fix(#3263): init _skip_cc_cl/_cc_cl before compressor block (UnboundLocalError on no-compressor path) + relax brittle #1318 source-assertion
The threshold-rescale block runs unconditionally after the fallback and
references _skip_cc_cl/_cc_cl, which were only defined inside 'if _cc_for_save:'.
On the no-compressor path (fresh agent / interrupted stream) that raised
UnboundLocalError (caught by test_issue1857_usage_overwrite). Hoist both inits
above the block (no-op rescale when no compressor). Also widen the #1318
source-assertion test to accept the widened fallback gate (still asserts the
falsy-check invariant).

Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
2026-06-01 00:14:13 +00:00
nesquena-hermes
18a459726b fix(#3263): apply Codex dual-gate MUST-FIX — recompute persisted stale cap + rescale terminal threshold
Codex regression gate (+Opus, both independently) found the default-only guard
dropped the stale compressor cap but two sibling paths stayed inconsistent:
1. Per-turn persistence: fallback resolver only ran when context_length was
   falsy, so a previously-persisted stale 232K survived forever on non-default
   sessions. Now also runs when _skip_cc_cl, and rescales threshold_tokens to
   the recomputed real cap (or clears it).
2. Terminal SSE 'done' payload: re-emitted the stale compressor threshold, so
   messages.js overwrote S.lastUsage and the indicator reverted on stream end.
   Now rescales threshold to the resolved window when the stale cap was dropped.
Added 3 source-structure regression tests pinning both fixes; bumped the brittle
test_pr1341 distance limit 13000→15000 (+ noted it should become structural).

Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
2026-06-01 00:09:21 +00:00
nesquena-hermes
7a734462b0 docs(changelog): v0.51.192 Release FL (stage-batch4) 2026-05-31 23:52:28 +00:00
nesquena-hermes
5768a4bfc8 test(#3263): bump brittle save-distance limit + add default-only context_length guard regression test
- test_pr1341 distance limit 9000→13000 (the PR legitimately added the
  default-only pre-save guard block; the test is designed to be bumped when a
  new pre-save mutation block lands — this was the only CI-red cause on shard 1).
- new tests/test_issue3256_context_length_default_only_guard.py: verifies the
  global model.context_length cap applies ONLY to model.default (revert-fix-
  verified — fails on master, passes with the fix).

Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
2026-05-31 23:51:49 +00:00