Commit Graph
2874 Commits
Author SHA1 Message Date
nesquena-hermesandRod Boev 57a78e9284 fix(#4728): clear stale custom:* base_url + default wins without loaded session (gate findings)
Two gate-found CORE/SILENT edges fixed on-branch:
- api/config.py: switching the main default to a different custom:* provider kept
  the OLD provider's base_url, routing New Chat to the wrong endpoint. The
  base_url-clear on provider-change now covers custom:* too (was exempted).
- static/sessions.js: newSession() skipped the configured default when S.session
  was null but the picker held stale state (e.g. after deleting the last session),
  inheriting the stale model. Default now wins over legacy picker regardless of
  loaded-session state (explicit empty-composer override still takes priority).
- Updated 2 stale source-grep precedence assertions + widened a brittle 6000-char
  newSession() slice window to 7000 (assignment block grew; intent unchanged).
- 2 new non-vacuous regression tests (custom base_url clear; precedence).

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-24 20:17:29 +00:00
Rod Boev 897322eac3 fix(#4728): keep main default-model routes from persisting auto provider 2026-06-24 20:09:30 +00:00
Rod Boev 23b3206f83 fix(#4728): guard settings default-model capture when no option is selected 2026-06-24 20:09:30 +00:00
Rod Boev c388d15f2b fix(#4728): clear stale base URLs when default provider changes 2026-06-24 20:09:30 +00:00
Rod Boev bede035a5e fix(#4728): persist provider-only default-model changes end to end 2026-06-24 20:09:30 +00:00
Rod Boev 69d202b94a fix(#4728): treat provider-only default changes as settings edits 2026-06-24 20:09:30 +00:00
Rod Boev eba089fd90 fix(#4728): preserve fallback-order guarantees for default-model routing 2026-06-24 20:09:30 +00:00
Rod Boev cdb44fb7e0 fix(#4728): sync saved default model provider into new chat state 2026-06-24 20:09:30 +00:00
Rod Boev dde3845171 fix(#4728): keep default-model requests on the configured provider 2026-06-24 20:09:30 +00:00
Rod Boev d8b58da018 fix(#4728): use configured default for new conversations 2026-06-24 20:09:30 +00:00
nesquena-hermesandRod Boev 3c8cfdc589 fix(#4811): regenerate row_id/seq from final order index so anonymous tool rows don't collide (gate finding)
Codex gate found a SILENT drop: reconnect scene rows are built with orderIndex=0,
so two tools WITHOUT a tool id at the same message index both got row_id
'...:tool:<idx>:0'; _completeSettledAnchorSceneForTurn dedupes by row_id, silently
dropping the second. Now the emit loop rewrites order_index AND regenerates the
index-derived row_id + seq + identity.seq from the final per-bucket position
(tid-based tool row_ids, already unique, are left untouched). Regression test
added + proven non-vacuous.

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-24 19:48:56 +00:00
Rod Boev f330fa7f44 perf(reconnect): pre-index toolCalls and remove unreachable guard (#4811) 2026-06-24 19:39:40 +00:00
Rod Boev e86ebb6379 fix: remove unused textwrap import 2026-06-24 19:39:40 +00:00
Rod Boev 173014ae0e fix: update stale string-snapshot assertions for refactored anchor loop 2026-06-24 19:39:40 +00:00
Rod Boev 057b878136 fix(#4811): sort reconnect scene rows by phase and started_at chronology 2026-06-24 19:39:40 +00:00
nesquena-hermesandRod Boev 8d2689ccc0 harden(#4694): broaden query-secret redaction before surfacing fetch diagnostics (gate finding)
Codex gate: the apply path now surfaces sanitized non-network fetch stderr, but
_QUERY_SECRET_RE only redacted access_token/token/password/auth/key — leaving
client_secret, private_token, oauth_token, api_key in a remote URL exposed in
the user-facing error. Broadened the pattern to cover those + app_secret/secret
+ api[_-]?key. Regression test added + proven non-vacuous.

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-24 19:29:06 +00:00
Rod Boev e88e4ecefc fix(#4694): treat fetch timeout diagnostics as network failures 2026-06-24 19:29:06 +00:00
Rod Boev a3eac6a538 fix(#4694): surface non-network git fetch failures in updates 2026-06-24 19:29:06 +00:00
nesquena-hermesandRod Boev 63e3f91654 harden(#4757): gate stripped-MIME drop on text/plain===tracked path + robust drag-flag cleanup (gate finding)
Codex gate found a SILENT hazard: if a workspace drag is abandoned without
ondragend firing (cancel/drop-outside/blur), _wsActiveDragPath lingered and a
later FOREIGN text/plain drag could be misread as a workspace move. Hardening:
- _wsDragSrcPath now only trusts the active flag when the drop's own text/plain
  EQUALS _wsActiveDragPath (a foreign/empty text drag resolves to '' -> no move).
- Shared _clearWsDragData() called from ondragend, a drop finally, and
  window-level dragend/drop/pagehide/blur fallbacks (lost-dragend cleanup).
- 2 regression tests (stale-flag foreign-text -> empty; _clearWsDragData resets),
  proven non-vacuous.

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-24 19:06:30 +00:00
Rod Boev 30c9f5e52a fix(test): use regex for let-declaration extraction resilience (#4757) 2026-06-24 18:58:33 +00:00
Rod Boev 2ef8a5f827 fix: add node portability guard to drag-drop test 2026-06-24 18:58:33 +00:00
Rod Boev 36fee0c6a4 fix(#4757): add active-drag flags so macOS workspace drag-drop lands 2026-06-24 18:58:33 +00:00
nesquena-hermesandAlexeyDsov 6a7fe93e10 fix(#4767): gate boundary-advanced bypass on checkpoint consumption (no stale-suffix resurrection)
Codex final-gate found the boundary<watermark advanced signal made
sidecar_advanced_past_watermark globally true, so a stale ts>watermark suffix
row appearing in state.db BEFORE the edited checkpoint bypassed the skip and
resurrected. Gate the bypass on state_replay_idx >= len(sidecar_visible_sequence)
(checkpoint consumed); apply the same gate to the same-second CORE-B exception.
Regression test added + proven non-vacuous. All legit cases (state-only reply
after checkpoint, CORE-B same-second) still pass.

Co-authored-by: AlexeyDsov <alexeydsov@gmail.com>
2026-06-24 18:39:02 +00:00
nesquena-hermesandAlexeyDsov 695d9ab814 fix(#4767): keep state-only post-edit reply when sidecar tail equals advanced watermark (Codex final gate)
Final-gate Codex repro found a non-empty-sidecar drop: when the post-edit USER
turn is checkpointed into the sidecar (ts == advanced watermark) but its
ASSISTANT reply exists only in state.db (ts > watermark), the
sidecar_advanced_past_watermark guard was False (max_sidecar_timestamp ==
watermark, not >), so the 'timestamp > watermark' skip dropped the legitimate
reply. GET /api/session returned ['first','first reply','edited prompt'] and
omitted the assistant.

Fix: treat truncation_boundary < watermark (genuinely advanced) as advanced too,
so post-watermark state-only rows merge through even when the sidecar tail only
EQUALS the watermark. Conservative skip preserved for boundary None / == watermark.
Hoisted the loop-invariant boundary check out of the per-message loop. Regression
test added + proven non-vacuous.

Co-authored-by: AlexeyDsov <alexeydsov@gmail.com>
2026-06-24 18:21:34 +00:00
nesquena-hermesandAlexeyDsov 7317e29d82 fix(#4767): guard empty-sidecar reconstruction against not-advanced watermark (Opus gate)
Opus deep-gate found a latent data-loss the Codex harnesses missed: the
empty-sidecar branch kept at_or_after (ts>=watermark) UNCONDITIONALLY, which is
only valid once the watermark was ADVANCED strictly past the original cutoff
(a new turn committed -> boundary < watermark). In the not-advanced states the
content above the watermark is the deleted suffix, so it got resurrected — the
same data-loss class this fix exists to kill. Reachable via crash/cold-load
metadata-vs-sidecar divergence.

- Keep at_or_after ONLY when boundary < watermark (genuinely advanced).
- boundary == watermark (just truncated) OR boundary is None (legacy: a positive
  watermark always meant 'frozen at cutoff' pre-#4767) -> conservative ts<=watermark
  filter; never resurrects.
- Replaced the backward-scan heuristic with the provable boundary guard.
- Removed the now-dead 'or (not sidecar_messages and watermark>0)' clause in the
  non-empty-sidecar path (unreachable; empty sidecar returns earlier).
- Added 2 regression tests (boundary==watermark, legacy None-boundary), both proven
  non-vacuous; updated 2 contributor tests that assumed advanced-watermark-without-boundary
  (an unreachable state) to pass the boundary the real persistence path always writes.

Co-authored-by: AlexeyDsov <alexeydsov@gmail.com>
2026-06-24 18:08:50 +00:00
nesquena-hermesandAlexeyDsov 6ce9533d53 test(#4767): route-level CORE-A regression — /api/session full reload honors truncation_boundary
Co-authored-by: AlexeyDsov <alexeydsov@gmail.com>
2026-06-24 18:08:50 +00:00
AlexeyDsov 037253e4fc fix(#4767): review fixes - more edge cases 2026-06-24 18:08:50 +00:00
AlexeyDsov 195d53ea90 fix(#4767): review fixes - edge cases 2026-06-24 18:08:50 +00:00
AlexeyDsov e1bbf3e807 fix(#4767): advance truncation_watermark instead of clearing to None after edit/retry/undo (#2914, #3831) 2026-06-24 18:08:50 +00:00
Ruyan a7788792fb fix: prefer agent cron package for task APIs 2026-06-24 17:52:57 +00:00
Brett Yelverton 2a40ca4240 fix(streaming): restore mobile scroll-jank guard during live renders
A later streaming parse-cache change dropped the #MOBILESCROLL guard from
the live-stream render tick while leaving the helper and CSS in place.
On iOS PWA, Safari can paint a transient scrollTop=0 frame during streamed
DOM rebuilds, forcing the reader to the first/oldest message.

Re-add _fixMobileScrollJank() before streaming DOM work begins and pin it
with a regression test so the third layer of the mobile scroll fix cannot
be silently dropped again.
2026-06-24 17:20:40 +00:00
nesquena-hermes f84db40a23 test(#4813): resolve node via shutil.which + skip if absent (CI portability)
The hardcoded /home/hermes/.local/bin/node doesn't exist in CI runners. Use
shutil.which('node') with a module-level skipif, matching the repo's other
node-driven tests.
2026-06-24 11:07:54 +00:00
nesquena-hermes 984fd9557c Merge remote-tracking branch 'origin/master' into gate/4813-cron-prefix 2026-06-24 11:01:50 +00:00
nesquena-hermes d755ed1c06 test(#4813): cover _cronModelBareName provider-prefix strip matrix
The contributor PR had no test for the new helper. Adds Node-driven coverage:
matching-provider prefix stripped, unprefixed untouched, no/mismatched provider
untouched, empty/null passthrough. Co-authored-by: luandnh
2026-06-24 10:51:24 +00:00
nesquena-hermes 631f1a35da Merge branch 'peek-4832' into gate/4832-kanban-height 2026-06-24 10:38:49 +00:00
nesquena-hermes 7fe6861a54 test(#4702): thread _lastMessageClientHeight through the scroll-pin-reentry JS harness
The #4295 harness runs the extracted scroll-handler body in a Node new Function();
the body now references _lastMessageClientHeight, so it must be a param + threaded
through state (seeded null, samples keep clientHeight stable so grew stays false and
the pin-reentry assertions are unchanged).
2026-06-24 10:15:04 +00:00
nesquena-hermes 671f4504ae fix(#4702): co-seed _lastMessageClientHeight with every programmatic _lastScrollTop write (Codex gate)
Codex found the guard had a first-event gap: programmatic seeds of _lastScrollTop
(_setMessageScrollToBottom, scrollToBottom, snapshot restore paths) didn't seed
_lastMessageClientHeight, so the FIRST native toolbar-collapse scroll saw a null
prior height -> grew=false -> the false-unpin still fired. Now every programmatic
_lastScrollTop=el.scrollTop is paired with _lastMessageClientHeight=el.clientHeight.
2026-06-24 10:02:52 +00:00
nesquena-hermes fe40912eb9 fix(#4702): don't false-unpin a freshly-opened session on iOS portrait toolbar settle
iOS Safari portrait resolves its dynamic toolbar height after first paint; when the
toolbar collapses the #messages scroller grows (clientHeight up), firing a scroll
event with decreased scrollTop that was misread as an upward user scroll -> falsely
set _messageUserUnpinned, and the ResizeObserver settle then self-cancelled, leaving
the reader stranded at the TOP of a freshly-opened conversation (landscape unaffected
since it doesn't resize late). Sibling of #4701.

Fix (touch-safe, no-op on desktop/landscape where clientHeight is stable):
- Track _lastMessageClientHeight; gate movedUp on !grew so a container-height
  INCREASE can't be read as an upward scroll. Reset on session switch.
- For explicit (open) settles, also observe the scroller itself so a late portrait
  toolbar collapse re-anchors the bottom.

Independent of #4818 (overflow-anchor paint-gap) — different mechanism, coexists.
Diagnosis credit: investigation. Co-authored-by: reinocheong <reporter>
2026-06-24 09:56:06 +00:00
nesquena-hermes 77b59f7c85 test(#4768): drop unused 're' import (ruff F401) 2026-06-24 09:29:15 +00:00
nesquena-hermes f4e150e105 Merge remote-tracking branch 'origin/master' into fix/4768-cron-graceful-degrade 2026-06-24 09:23:37 +00:00
nesquena-hermes 1e3132376b fix(#4768): only treat absent cron package as unavailable, re-raise real import bugs (Codex gate)
ModuleNotFoundError fires both when cron.jobs is absent AND when an existing
cron/jobs.py fails to import its own dependency. Catch as exc and only return the
graceful {jobs:[],cron_unavailable:true} when exc.name is 'cron'/'cron.jobs';
re-raise otherwise so a real cron bug isn't silently swallowed.
2026-06-24 09:12:46 +00:00
jkobject 93afd60425 fix(kanban): bound column card list height 2026-06-24 10:57:03 +02:00
nesquena-hermes 1be24e0660 test(#4241): bump remaining hardcoded locale-count assertions for vi (14th locale)
Full-suite caught 7 more per-locale count tests: open_in_vscode (->13, a
pre-existing one-locale gap unrelated to vi), usage_cache_hit_detail/usage_cached_percent
(->14), and the i18n_tooltips LOCALE_COUNT (13->14). vi carries these keys.
2026-06-24 08:50:52 +00:00
nesquena-hermes 34694a1f02 fix(#4768): degrade GET /api/crons gracefully when the cron module is absent
In split-container / minimal Docker deployments the WebUI image may not ship the
agent's cron package on its import path, so the Tasks tab's GET /api/crons did
'from cron.jobs import list_jobs' and 500'd with ModuleNotFoundError — breaking the
whole tab. Now the import is guarded and returns {jobs: [], cron_unavailable: true}
instead, so the Tasks tab loads (empty) instead of erroring. Happy path unchanged.
Co-authored-by: reporter
2026-06-24 08:42:50 +00:00
nesquena-hermes 1fc24ddf68 feat(#4241): add Vietnamese (vi) as 14th UI locale + wire parity
Maintainer-completed @thanhtantran's #4241 (Nathan approved bringing it in):
- vi locale block in static/i18n.js (contributor's 1615-line translation) rebased onto master.
- Added vi to _LOGIN_LOCALE in api/routes.py (login-page localization, issue #1442 parity).
- Bumped the per-locale hardcoded-count assertions 13->14 across the locale-parity
  tests (rtl, quota_chip, hide_suggestions, virtualize_transcript, no_git, help_tab,
  verdigris/zeus skins, auxiliary_models) and added 'vi' to the composer-voice-buttons
  LOCALES tuples. vi already carries all current English keys (verified 14x per key).
All 497 locale/parity tests pass. Co-authored-by: thanhtantran
2026-06-24 08:39:14 +00:00
nesquena-hermes 3de15e53a5 Merge branch 'peek-4241' into gate/4241-vietnamese 2026-06-24 08:26:00 +00:00
nesquena-hermes 1fc86a47c7 Merge remote-tracking branch 'origin/master' into fix/4808-streaming-sessions-cache-ttl 2026-06-24 08:18:44 +00:00
Orange Pi Vietnam d341f2084c Merge branch 'nesquena:master' into master 2026-06-24 15:12:44 +07:00
nesquena-hermes 8eed95a25e fix(#4808): widen /api/sessions cache TTL while streaming to stop per-poll rebuilds
The frontend polls /api/sessions every 5s while a turn streams
(static/sessions.js _streamingPollMs=5000), but the cache TTL was 2.5s
(_SESSIONS_CACHE_TTL_SECONDS) — so every streaming poll landed in a fresh window
and forced a full all_sessions() rebuild on the hot path under the global store
LOCK, pinning CPU to 100% and starving token rendering on large stores
(recurrence of #4672). The #4680 freeze-marker only de-dupes rebuilds WITHIN one
TTL window; with polls spaced wider than the TTL it never helped.

Fix: while _session_list_cache_streaming_freeze_marker() indicates an active
stream, use _SESSIONS_CACHE_STREAMING_TTL_SECONDS=10.0 (> the 5s poll) so the
sidebar holds steady across streaming polls. Live runtime state (active stream,
sort order, pending flags) is overlaid on every response regardless of cache, and
structural/settings changes still evict immediately via the source stamp — only a
streaming session's own persisted title/message_count can lag up to 10s, which the
#4672 design already deems acceptable. Idle path byte-for-byte unchanged.

Diagnosis credit: investigation reproduced 6 streaming polls -> 6 rebuilds before,
1 after. Co-authored-by: batkuip <reporter>
2026-06-24 08:06:45 +00:00
nesquena-hermes 22b98e3922 Merge branch 'peek-4828' into gate/4828-reasoning-echo
# Conflicts:
#	CHANGELOG.md
2026-06-24 07:57:31 +00:00