Commit graph

5741 commits

Author SHA1 Message Date
nesquena-hermes
b8ea322dbb
Merge pull request #5229 — tighten new-conversation empty-state reuse guard (#5206, @franksong2702)
Release v0.51.750 — tighten new-conversation empty-state reuse guard (#5206, @franksong2702)
2026-06-29 11:50:48 -07:00
nesquena-hermes
0256773697 docs(changelog): tighten new-conversation empty-state reuse guard (#5206) 2026-06-29 18:44:28 +00:00
Frank Song
fbb1e9567c test: align new chat guard static coverage
Update the older #1432 static regression to assert the extracted reusable-empty helper owns the message-count and in-flight checks, while both New Chat entry points call the helper. This keeps the test aligned with the shared guard introduced in PR #5206.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-29 18:38:25 +00:00
Frank Song
28c2c37c1c Fix new chat empty-session reuse guard 2026-06-29 18:38:25 +00:00
nesquena-hermes
60a45686e6
Merge pull request #5227 — memoize API-response redactor (#5204, @ducnhung70)
Release v0.51.749 — memoize API-response redactor to stop poll-storm CPU wedge (#5204, @ducnhung70)
2026-06-29 11:36:18 -07:00
nesquena-hermes
bc810c6bf7 docs(changelog): memoize deterministic API response redactor (#5204) 2026-06-29 18:24:25 +00:00
nesquena-hermes
dc38e42a26 test(#5204): lock redactor memoization contract (cached==uncached + oversize bypass) (auto-fix)
Adds the optional regression test the gate recommended: pins that
_redact_fn_cached stays byte-identical to _redact_fn_uncached for sensitive
input, is idempotent, and that >16KB inputs which bypass the lru_cache are
still redacted identically (no cache-skip secret leak). Production code
unchanged.
2026-06-29 18:23:41 +00:00
AnhNVT
9ad1fa0372 fix(redaction): memoize deterministic API response redactor
The combined credential redactor (~15 regex passes per string) runs on the
full message history of every /api/session response. Under concurrent
dashboard polling of unchanged sessions this became the dominant CPU cost
and, behind the GIL in the single-process server, serialized requests to
10-44s each — surfacing as repeated 'connection lost' in the browser as
the proxy/browser timed out and the frontend retried, piling on threads.

The redactor is pure and deterministic (force=True, fixed masking), so
identical strings always map to identical output and are safe to memoize
without invalidation. Wrap it in an lru_cache(maxsize=4096), gated to
strings <=16KB so a few giant tool-output dumps can't evict the many small
recurring strings or balloon RSS. Repeated polls become O(1) cache hits
(measured 3.7ms -> 0.002ms per 3KB marker string).

_redact_fn_cached is preserved as the call-site name so the existing
monkeypatch-based redaction tests bypass the cache unchanged (69/69 pass).
2026-06-29 18:22:18 +00:00
nesquena-hermes
791744ac29
Merge pull request #5226 — bound HTTP worker dispatch under reconnect storms (#5215, @rodboev)
Release v0.51.748 — bound HTTP worker dispatch under reconnect storms (#5215, @rodboev)
2026-06-29 11:20:06 -07:00
nesquena-hermes
6fe7a16fcd docs(changelog): bound HTTP worker dispatch under reconnect storms (#5215) 2026-06-29 18:11:11 +00:00
Rod Boev
d548dabd21 Retrigger CI on validated head 2026-06-29 18:10:20 +00:00
Rod Boev
7744fe3f8b Keep disconnect intent explicit through server cleanup 2026-06-29 18:10:20 +00:00
Rod Boev
ca5b20d2e6 Keep overflow cleanup off the accept loop 2026-06-29 18:10:20 +00:00
Rod Boev
55ad113ee7 fix(#5210): bound HTTP worker dispatch under reconnect storms 2026-06-29 18:10:20 +00:00
nesquena-hermes
68b211e0d5
Merge pull request #5223 — Windows-safe os.replace retry (#5196, @silent-reader-cn)
Release v0.51.747 — Windows-safe os.replace retry on session saves (#5196, @silent-reader-cn)
2026-06-29 11:09:33 -07:00
nesquena-hermes
e7bd74cc13 docs(changelog): Windows-safe os.replace retry (#5196) 2026-06-29 18:06:41 +00:00
nesquena-hermes
683a96acda test(#5196): add _safe_replace retry/backoff coverage (auto-fix)
Adds the test the project's contribution rules require for non-trivial logic
(AGENTS.md). Covers: posix passthrough (1 call, no sleep), Windows
retry-then-succeed, reraise after exhausting 5 attempts (no silent success on a
save path), no-retry on non-PermissionError, and the 50/100/200/400ms backoff
sequence. Production _safe_replace unchanged.
2026-06-29 17:54:37 +00:00
wangyichen
17f2d898f3 fix: retry os.replace() on Windows WinError 5 (PermissionError)
Windows locks session JSON files briefly during antivirus scans or
browser polling, causing os.replace() to raise WinError 5
(ERROR_ACCESS_DENIED). Add _safe_replace() helper that retries with
exponential backoff (50ms to 800ms, max 5 attempts) on PermissionError.
No-op on non-Windows platforms.

Replaces all 5 os.replace() call sites in models.py:
- Session.save() (main file + backup)
- _write_session_index() (full rebuild + fast path)
- prune_session_from_index()
2026-06-29 17:53:41 +00:00
nesquena-hermes
7ef8b636a0
Merge pull request #5222 — Cmd/Ctrl+K skips text inputs (#5208, @nankingjing)
Release v0.51.746 — Cmd/Ctrl+K skips text inputs so kill-line works (#5208, @nankingjing)
2026-06-29 10:50:12 -07:00
nesquena-hermes
a084a040a7 docs(changelog): skip Ctrl+K new-chat in text inputs (#5208) 2026-06-29 17:46:59 +00:00
nesquena-hermes
9dcd31d501 test(#5099): widen ctrl-k branch windows so guard assertions reach the code (auto-fix)
The source-string assertion windows were too short: the Ctrl+K branch window
(900) cut off before newSession()/closeMobileSidebar() (@992/@1155) and the
Ctrl+B forward window (+120) cut off isContentEditable (@119+). Widen to
1500 / +300 so the guards are actually verified (non-vacuous). Codex-recommended
headroom. Production guard unchanged.
2026-06-29 17:46:18 +00:00
nankingjing
0a94e55384 fix(ux): skip Ctrl+K new-chat shortcut in text inputs (#5099)
Let Emacs-adjacent users keep kill-line in the composer and other
editable fields while preserving global Cmd/Ctrl+K elsewhere.

Fixes #5099
2026-06-29 17:36:47 +00:00
nesquena-hermes
2acf88c41b
Merge pull request #5221 — stream-end worklog collapse fixes (#5058, @allenliang2022)
Release v0.51.745 — stream-end worklog collapse fixes for pinned readers (#5058, @allenliang2022)
2026-06-29 10:10:29 -07:00
nesquena-hermes
c4d971bbc5 docs(changelog): stream-end worklog collapse + pure-prose fixes (#5058, #4970) 2026-06-29 17:07:23 +00:00
allenliang2022
955e31b9a1 fix(chat): do not collapse a pure-prose turn into a worklog (stream-done jump)
A turn that streamed only prose (a long plain-text answer, or a degeneration
burst that floods the body with repeated tokens) still projected an anchor
activity scene whose activity_rows were all prose/terminal — zero tool/thinking
rows. The settle path promoted it to a collapsed worklog anyway (the gate only
checked activity_rows.length), hiding the whole answer and shrinking the
transcript by the full streamed height at STREAM_DONE, so the browser clamped a
bottom-pinned viewport back to the top (the 'jump back' report).

Add a worklog-worthiness predicate at both gates: the generation gate
(_anchorSceneHasWorklogWorthyRows in messages.js, decides whether to attach a
scene at all) and the render gate (_anchorSceneSceneHasWorklogWorthyRows in
ui.js, defense-in-depth for already-persisted all-prose scenes). A scene is
worklog-worthy only if it has >=1 tool/thinking row or a compression lifecycle
card; pure prose is not, so the turn renders as normal visible prose and the
viewport stays pinned at the bottom.

Behavioral + structural tests in tests/test_pure_prose_turn_not_worklog.py.
2026-06-29 17:06:45 +00:00
allenliang2022
2aa1d168b0 fix(chat): scope STREAM_DONE keep-open to just-settled turn via one-shot token
Address maintainer must-fix: _shouldKeepSettledWorklogOpenForPinnedFollow()
keyed only on global pin state, so every historical settled worklog re-expanded
on any pinned re-render — defeating the compact default. Gate the keep-open
exception on a one-shot token = the just-settled stream id (armed at STREAM_DONE
renderMessages, disarmed right after), threaded as _shouldKeep...(streamId).
Unpinned path unchanged. Replace source-string test with a behavioral Node test
that drives a second pinned turn and asserts it collapses.
2026-06-29 17:06:44 +00:00
allenliang2022
0d198d677d test(chat): document keep-open precedence over live collapse state
Follow-up to maintainer review on #5058: make the pinned-follower helper comment
explicit that avoiding the visible STREAM_DONE shrink jump intentionally takes
precedence over a transient user-collapsed live worklog while the reader remains
pinned. Add a source lock so that intent stays documented.

Focused scroll locks remain green (22 passed), node --check clean.
2026-06-29 17:06:44 +00:00
allenliang2022
5fd3b1c9d7 fix(chat): prevent stream-end worklog collapse jump for pinned readers
A new post-#4970 scroll-jump class surfaced after Release YH: when a streamed
assistant turn with a large live worklog/tool trace settles, the settled compact
worklog collapses hundreds of pixels at STREAM_DONE. The reader is still pinned
and scroll state is correct, but the net scrollHeight shrink makes the browser
clamp scrollTop down by the same amount, which is visible as a large backward
jump.

Fix by keeping the just-settled activity worklog open for pinned followers
(_scrollPinned && !_messageUserUnpinned), so the live->settled DOM swap is
height-stable. Unpinned readers still get compact settled worklogs and keep their
viewport normally. Also make _anchorSceneWorklogGroup respect opts.collapsed;
previously it hard-coded collapsed: !live so the caller could not request an open
settled group.

Regression locks assert the pinned-follow helper, that settled rendering passes
collapsed:!keepSettledWorklogOpen, and that _anchorSceneWorklogGroup honors an
explicit opts.collapsed. Verified locally with a Playwright repro: before the
fix a large tool-worklog answer produced scrollHeight -367/-422px and scrollTop
-367/-422px at STREAM_DONE; after the fix nShrinks=0 and nBack=0. Local focused
scroll locks pass: 22 passed.
2026-06-29 17:06:44 +00:00
nesquena-hermes
1683cf1a0d
Merge pull request #5219 — no transcript blank-flash on hidden-tab return (#5189, @allenliang2022)
Release v0.51.744 — no transcript blank-flash on hidden-tab return (#5189, @allenliang2022)
2026-06-29 10:05:58 -07:00
nesquena-hermes
b2d2bb2e71 docs(changelog): keep stale transcript mounted on hidden-tab return (#5189) 2026-06-29 17:02:55 +00:00
allenliang2022
860a3c2458 fix(webui): keep stale transcript mounted while hidden-tab return reloads new messages (#5177)
After a hidden interval that added persisted messages (post-turn bg-review
writes, sibling-tab writes, the just-finished main turn flushing), switching
the tab back caused refreshActiveSessionIfExternallyUpdated('visible'|'focus')
to hit remoteCount !== localCount and call loadSession(sid, {force:true}),
which synchronously did S.messages = [] before awaiting the metadata +
messages fetches. The visible result was the entire conversation transcript
blanking for the round-trip and reappearing — '对话突然消失,重刷才回来'.

sessions.js itself warns about this 'disappear and reappear' tradeoff at the
top of the remoteCount/remoteLast guard, but only short-circuits the
metadata-only branch (#5061). #5122's 4-probe staged reconnect covers a
different path (SSE error with ready_state=2 on a visible tab) and does not
apply when the SSE error arrives while visibility_state='hidden' — that
bottoms out through _deferStreamErrorIfPageHidden before the reconnect block
ever runs.

Fix:

- refreshActiveSessionIfExternallyUpdated maps the visibility/focus recovery
  reasons ({visible, focus}) to a new keepStaleUntilLoaded option, forwarded
  to loadSession. The post-stream idle reconcile and the poll/external paths
  keep their existing behaviour.
- loadSession ANDs opts.keepStaleUntilLoaded with sameSessionForceReload (so
  cross-session switches still clear synchronously — leaving a prior
  session's transcript on screen during a navigation is the original bug
  the clear was written for) and, on that path, skips the synchronous
  S.messages/S.toolCalls/_messagesTruncated/_oldestIdx clear. The new
  transcript is SWAPPED into S.messages by _ensureMessagesLoaded(sid,
  {force:true}), so the user sees old DOM directly replaced by new DOM in a
  single render frame.
- _ensureMessagesLoaded grows an opts.force escape hatch so its 'messages
  already populated' early-return cannot skip the swap when stale messages
  are still in place.

Verified live on a source 8701 build:
- before: minHtmlObserved 65874, minKidsObserved 11 — 372 samples at 16ms,
  DOM html length NEVER dipped below the pre-call value during the reload.
- loadSession was called with force:true keepStale:true reason:'visible',
  outcome 'reloaded', sMsgs 21 → 1037, after kids 11 → 134 (single-frame
  swap).

Tests:

- tests/test_issue5177_hidden_tab_blank_gap.py (new, 7 cases): source-locks
  the keep-stale guard shape, the recovery-reason map, the
  _ensureMessagesLoaded opts.force escape hatch, and that the synchronous
  clear is wrapped in if(!_keepStaleUntilLoaded).
- Updated the two existing source-lock tests whose anchor strings included
  the loadSession call signature
  (tests/test_webui_external_refresh_frontend.py +
  tests/test_tars_scroll_reset_regressions.py) to match the new
  loadSession(... keepStaleUntilLoaded:_keepStaleUntilLoaded) form. No
  behavioural assertions changed.

Local: node --check static/sessions.js OK. 122 passed across
test_issue5177_hidden_tab_blank_gap.py + the full adjacent set
(external-refresh / journal-frontend / scroll-reset / reconnect-chronology /
inflight-stream-reuse / sse-error-multi-probe / issue4295 / issue4856).
2026-06-29 17:02:09 +00:00
nesquena-hermes
a1e6532467
Merge pull request #5218 — server-initiated turns render in a hidden tab (#5172, @allenliang2022)
Release v0.51.743 — server-initiated turns render in a hidden tab (#5172, @allenliang2022)
2026-06-29 10:01:25 -07:00
nesquena-hermes
62845eaa0f docs(changelog): render server-initiated turns in a hidden tab (#5172) 2026-06-29 16:56:30 +00:00
allenliang2022
ada8c262ca fix(webui): render server-initiated turns in a hidden tab without a refresh
A turn started SERVER-SIDE (self-wake, cron, restart hook) fans a
`server_turn_started` frame onto the per-session live-view SSE channel so an
open tab renders it live. But while a tab is HIDDEN the WebUI deliberately does
NOT hold that persistent SSE open (connection-pool budget — #3992 / #4151), so
`get_session_channel()` returns None at fan-out time and the turn is dropped for
that tab. The user only saw the turn after a manual interaction (refresh / send)
re-subscribed the channel and replayed it.

Bridge the gap with a lightweight poll of `/api/session/status` (one short GET
per ~6s tick — NOT a held connection, so the connection-pool budget the hidden
guard protects is preserved) that attaches the existing live renderer when it
sees a *live* active stream.

Backend (`api/session_ops.py`):
- `session_status()` now exposes `active_stream_id`, derived through a new
  `_live_active_stream_id()` helper that only returns the id when the stream is
  genuinely live (present in STREAMS / ACTIVE_RUNS). A stale id left by a
  crashed/restarted run surfaces as None, so the poller never attaches a
  renderer to a dead stream. Additive field — existing consumers ignore it.

Frontend (`static/messages.js`):
- `_startHiddenActiveStreamPoll` / `_stopHiddenActiveStreamPoll` /
  `_attachServerInitiatedStream` implement the poll lifecycle. The poll fires
  one immediate tick on hide (so an in-flight turn is caught without waiting a
  full interval), attaches mid-flight turns via the reconnecting/replay path,
  and stops on re-show (the real SSE takes over), session switch, or once a
  stream is rendering.
- Started on BOTH hidden paths: the `visibilitychange` hook's hidden branch (a
  visible tab going to background) and `startSessionStream`'s hidden
  early-return skip (a session opened while already hidden). The visible path
  and `stopSessionStream` clear it.

Tests:
- `tests/test_hidden_tab_server_initiated_turn.py` — backend live-validation
  (stale id → None; STREAMS/ACTIVE_RUNS id returned) and frontend lifecycle /
  both-hidden-paths / replay-attach source locks.
- Widened the brittle fixed-width source-window slices in
  `tests/test_issue3996_sse_visibility.py` (1600/1700 → 2400) so the existing
  `visibilitychange` / hidden-skip assertions still find their markers after the
  poll-start lines were inserted into `startSessionStream`.
2026-06-29 16:28:30 +00:00
nesquena-hermes
8cee867f19
docs: weekly contributor tally refresh (20260629) (#5214)
Regenerated by scripts/regen_contributors.py --apply (markers spliced, prose
untouched, --verify clean). 304 contributors / 1771 credits.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-06-29 09:18:11 -07:00
nesquena-hermes
3b120e70cc
Merge pull request #5194 — de-flake test_skills_stats_cache (#5178)
Release v0.51.742 — de-flake test_skills_stats_cache (#5178)
2026-06-28 23:11:58 -07:00
nesquena-hermes
fa5ff8414e docs(changelog): de-flake test_skills_stats_cache mtime race (#5178) 2026-06-29 06:08:52 +00:00
nesquena-hermes
359eae93de test: de-flake test_skills_stats_cache mtime race
test_skills_stats_cache asserted that adding a skill bumps the skills-dir
mtime so the cheap per-call probe recomputes counts on the next call. Two
_write_skill() calls inside the same coarse filesystem mtime tick could leave
the skills-dir mtime byte-identical to the cached probe value, so the probe
saw no change and served the stale count of 1 (assert enabled==2 failed). This
is an order-dependent flake: passes in isolation, fails intermittently under
full-suite ordering (observed across multiple unrelated PRs).

Force the skills-dir mtime strictly forward after the second write so the
probe is guaranteed to detect a genuine mtime change. The assertion stays
honest (it still proves recompute-on-mtime-change); only the race is removed.
2026-06-29 06:07:44 +00:00
nesquena-hermes
1ae168aa20
Merge pull request #5193 — 5-shard CI split + test-isolation fixes
ci: run tests across 5 shards (up from 3) + fix 2 latent test-isolation leaks
2026-06-28 22:58:08 -07:00
nesquena-hermes
72f7530c45 ci: run tests across 5 shards (up from 3) + fix 2 latent test-isolation leaks
Two cross-file isolation leaks that a finer shard partition exposes are fixed at
root so every shard passes regardless of execution order:
- test_auth_session_persistence.py: bind api.auth STATE_DIR/_SESSIONS_FILE/
  _LOGIN_ATTEMPTS_FILE in setUp + restore in tearDown (was relying on a sibling
  reload test to rebind them).
- test_request_diagnostics_cache.py: pin the session-list cache source stamp via
  an autouse fixture (matches test_session_sidebar_cache.py) so a leaked state.db
  WAL sidecar can't cause a spurious cache-miss.

Verified all shards green under 3-, 4-, and 5-way partitions. CI infra + test
harness only; no application behavior change.
2026-06-29 05:55:16 +00:00
nesquena-hermes
f2ffeacc64
Merge pull request #5192 — runtime-journal anchor-scene parity test (#5183, @franksong2702)
Release v0.51.740 — runtime-journal anchor-scene parity test (#5183, @franksong2702)
2026-06-28 22:40:45 -07:00
nesquena-hermes
744b6480cf docs(changelog): runtime-journal anchor-scene parity regression test (#5183) 2026-06-29 05:37:02 +00:00
Frank Song
058372c15f Preserve empty anchor tool args in parity helper
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-29 05:35:03 +00:00
Frank Song
e670320337 Tighten anchor runtime journal parity test
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-29 05:35:03 +00:00
Frank Song
e1bf1122a7 Test anchor runtime journal parity 2026-06-29 05:35:03 +00:00
nesquena-hermes
537ff28ae0
Merge pull request #5191 — session-backed HTML/PDF media previews (#5157, @santastabber)
Release v0.51.739 — session-backed HTML/PDF media previews (#5157, @santastabber)
2026-06-28 22:22:36 -07:00
nesquena-hermes
2d71598f16 docs(changelog): authorize session-backed HTML/PDF media previews (#5157) 2026-06-29 05:18:53 +00:00
santastabber
178db13512 fix: authorize session html media artifacts 2026-06-29 05:17:33 +00:00
nesquena-hermes
d9c4bc6d76
Merge pull request #5190 — Workspace Todos render parity (#5103, @Stacey2911)
Release v0.51.738 — Workspace Todos render parity (#5103, @Stacey2911)
2026-06-28 22:16:45 -07:00
nesquena-hermes
c998a316ed docs(changelog): align Workspace Todos rendering with sidebar (#5103) 2026-06-29 05:13:02 +00:00