Commit Graph
7371 Commits
Author SHA1 Message Date
735ba1c4d3 Release: extension session-open handler + renderTranscript API (#5508, @ChonSong) (#6226)
* feat(core): add registerHermesSessionOpenHandler + renderTranscript extension hooks

- registerHermesSessionOpenHandler(fn): register a handler that fires on
  session open. Return {cancel:true} to prevent navigation.
- renderTranscript(container, messages, opts): render messages into any
  DOM container using core's renderMd pipeline. Skip tool messages.
- Wire _hermesNotifySessionOpen into loadSession: pre-load guard at top,
  post-load notification at end for extensions to hook into.
- Follows existing registerHermesTtsEngine extension registration pattern.

This gives extensions like chat-tiling a sanctioned API instead of DOM
hacking to intercept and render session transcripts.

* fix: address 3 gate-fail blockers from PR #5508 review

Fixes the three core issues identified by nesquena-hermes code review:

1. XSS sink in renderTranscript (boot.js)
   - Fallback to textContent when window.renderMd is unavailable
   - innerHTML exclusively for successful renderMd output

2. Stale _loadingSessionId reset in cancel branch (sessions.js)
   - Remove premature nulling of _loadingSessionId
   - Cancel path simply returns without touching loading-guard state

3. Pre-open veto bypasses profile/import side-effects (sessions.js)
   - Move cancellable preload hook to start of _openSidebarSession,
     before external session import and profile switching
   - Pass internal _preloadNotified flag to skip duplicate preload
     in loadSession while retaining post-load notification

Closes #5508 gate-fail items.

* fix: use module-level flag instead of call argument to avoid test regression

The _preloadNotified approach broke test_static_sessions_js_switches_profile
before_opening_all_profiles_row because it changed the loadSession call
signature from loadSession(sid, loadOpts) to loadSession(sid, Object.assign(...)).

Switch to a module-scoped boolean _hermesSessionOpenAlreadyFired set by
_openSidebarSession before calling loadSession, checked by loadSession's
pre-hook guard. The call signature stays unchanged.

Test 2 (test_load_session_rearms_stream_on_every_early_return) also passes.

* fix: compact pre-hook comment to keep loadSession within test window limits

* fix: replace global _hermesSessionOpenAlreadyFired flag with per-call opts._preloadNotified

The module-level boolean introduced in 381fa0ef had two problems:
1. ReferenceError on direct loadSession() calls — the flag was undeclared
   when called outside _openSidebarSession, breaking saved-session restore.
2. Never reset after first sidebar open — all subsequent direct calls
   silently skipped the cancellable preload handler.

Per the maintainer's review (PR #5508), replace the global with a per-call
option _preloadNotified passed by _openSidebarSession via Object.assign.
This keeps the call signature stable for existing tests and eliminates
the stale global state leak.

Test adjustments:
- test_issue1611: widened loadSession call literal assertion
- test_session_channel_option_x: body slice 14000→15000 to accommodate
  the slightly longer function body

* chore: remove unrelated files from commit

* fix: address 3 review issues — preload-only cancel, drop inner wrapper, pass _preloadNotified on retry

- Only honor {cancel:true} when opts.preload===true (boot.js)
- Drop .msg-body-inner wrapper, render directly into .msg-body (boot.js)
- Carry _preloadNotified:true through cross-profile 409 retry (sessions.js)

* recommit

* Update static/boot.js

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix(extensions): resolve canonical sid before preload hook + pass _preloadNotified on continuation retry

PR #5508 review follow-up (review 4690636760):
1. Move _resolveSessionIdFromSidebarLineage() before the preload hook so
   extensions always see the canonical sid, not the raw sidebar click id.
2. Pass _preloadNotified:true on the continuation-session retry path to
   prevent duplicate preload events to extensions.
3. Add functional test_extension_session_hooks.py — actually drives the
   new hook registration, preload-veto, transcript rendering, and
   _preloadNotified bridge in Node (13 tests, all green).

* Remove unused pytest import

Removed unused import of pytest from test file.

* fix: address 2 gate-blocking veto-ordering defects (PR #5508)

Blocker 1 (CORE): cross-profile retry now passes _preloadNotified:true
so the pre-hook doesn't re-fire after destructive side-effects already
ran (stream teardown, message clear, profile switch). A {cancel:true}
on that second fire was stranding the UI profile-switched with a
cleared transcript.

Blocker 2 (SILENT): closeMobileSidebar() was called synchronously
BEFORE _openSidebarSession()'s veto guard in three places (tap-to-open,
child-session, lineage-segment). A {cancel:true} still closed the
sidebar out from under it. Removed the three premature calls; moved
a single closeMobileSidebar() inside _openSidebarSession AFTER the
veto guard so it only runs when the open actually proceeds.

Added 3 regression tests asserting {cancel:true} leaves NO side-effect.

* CHANGELOG: extension session-open handler + renderTranscript API (#5508)

---------

Co-authored-by: Sean <seanos1a@gmail.com>
Co-authored-by: ChonSong <85378550+ChonSong@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-18 00:53:17 -07:00
02aa91ba6e Release: stop false Compressing-context card (#6184, @carlotestor) (#6223)
* fix: stop false "Compressing context" on non-compress turns

Narrow the agent status → SSE compressing bridge to real Hermes
compaction start notices, and stop snapshot hydration inventing a
running compress divider from terminal/lifecycle rows without cues.

Brand-new low-token chats (and skip/cooldown notices) no longer paint
the live auto-compression worklog row.

* CHANGELOG: false compressing-context card fix (#6184)

---------

Co-authored-by: carlotestor <89560945+carlotestor@users.noreply.github.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-18 00:03:30 -07:00
52c962cf7d Release: Transparent Stream multi-segment prefix dedupe (#6189, @ai-ag2026) (#6217)
* fix(transparent-stream): drop stale final-answer prefix row in multi-segment settle (#5749 follow-up)

A turn with interim assistant messages (prose interleaved with tool calls)
could show the beginning of the final answer TWICE after watching it stream:
once as a settled anchor-scene prose row (the live-token accumulator's last
throttled snapshot) and once as the real assistant segment. The duplicate
persisted until reload.

Root cause: #5758 suppresses the accumulator row only when it sits "after the
last tool row", but _completeSettledAnchorSceneForTurn appends the settled
per-message tool rows AFTER the projected live rows — those re-list tools that
ran EARLIER in the turn, pushing the boundary past the final segment's
accumulator so the guard never fired. The stale prefix snapshot then survived
into the persisted scene and rendered above the settled answer.

Fix: judge final-segment eligibility against the LIVE projection's own
chronology — a live-prose row belongs to the final segment iff no PROJECTED
tool row follows it. Pre-tool narration that happens to prefix the final
answer stays protected (existing #5758 regression tests still pass), and a
new regression test pins the multi-segment shape.

Verified end-to-end by replaying the captured run journal of an affected
session through the real SSE live handlers in headless Chromium: duplicate
before (answer prefix visible twice after settle), gone after; fresh-reload
rendering unchanged.

Rollback: revert this commit; behavior returns to pre-fix (duplicate prefix
row after live-settle of multi-segment turns).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* CHANGELOG: transparent-stream multi-segment prefix dedupe (#6189, @ai-ag2026)

---------

Co-authored-by: ai-ag2026 <m.fuechtenkoetter@posteo.de>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-17 21:51:22 -07:00
ab937ef3ec Release: msg_limit ceiling metadata decoupling (#6214, @webtecnica) (#6216)
* feat: expose msg_limit ceiling via /api/session metadata, frontend reads dynamically (#6177)

Backend exposes _MAX_MSG_LIMIT as _msg_limit_max in every /api/session
response. Frontend reads it dynamically, falling back to _MSG_LIMIT_MAX
for older servers. This removes the hand-mirrored coupling between the
two layers.

Removed test_msg_limit_ceiling_drift.py since the mirror pattern that
required the drift guard is replaced by dynamic metadata.

This is the standalone metadata-decoupling piece from #6206,
without the clamp/paging that already shipped in exp-v0.52.98 via #6152/#6154.

* fix(session): declare _msgLimitMax at module scope + CHANGELOG + gate fixes (#6214 follow-up)

The submitted PR used _msgLimitMax at two read sites (boundedReloadLimit in
_ensureMessagesLoaded, useBeforePaging in _loadOlderMessages) but never declared
it and read it before assignment -> undefined on cold load -> full-transcript
fetch every load (regression) + implicit global. Declared `let _msgLimitMax =
_MSG_LIMIT_MAX;` at module scope so the reload-width paths always read a defined
value (the static fallback) until the server's _msg_limit_max lands.

Also: defined the ceiling globals in the two inline node-harness tests that copy
_ensureMessagesLoaded's body (test_cross_session_message_load_isolation,
test_session_unread_dot_on_visit), updated the source-string assertion in
test_webui_external_refresh_frontend, and added 3 decoupling tests (backend field
present, module-scope declaration + fallback, both paths read the live ceiling)
replacing the deleted drift-guard.

Gate: Codex adversarial SAFE TO SHIP (executable probes: cold-load fallback,
mixed-version omission, live update, cross-session isolation, over-ceiling bare
refresh, msg_before row preservation). Full sharded suite green.

Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>

---------

Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-17 21:21:52 -07:00
ebf64b8a79 docs: fold in Rod's PR-review feedback into GUIDELINES + CONTRIBUTING (#6211)
* docs: fold in Rod's PR-review feedback into GUIDELINES + CONTRIBUTING

Based on rodboev's feedback distilled from 50+ recent PRs. Four accepted
points plus one clause, folded into existing rules rather than adding new ones:

- Rule 6: load the reporter's shipped reproduction, don't rebuild a fixture
  from your reading of it (the one genuine hole — a fix and test from the same
  wrong model agree with each other and certify a no-op).
- Rule 2: confirm a value is authoritative (declared at the point of intent),
  not inferred from id prefix / content shape / emptiness / DOM state.
- Rule 4: read prior PRs and review threads to find a subsystem's real variants
  instead of inventing axes from the single case handed to you.
- Rule 1: the chokepoint is the smallest boundary that contains the fault, not
  the widest you can reach (don't disable a whole pipeline to suppress one output).
- "Show your work": name who owns the truth for any claim the repo doesn't own.

CONTRIBUTING.md carries the two contributor-facing points (repro-loading,
proof-ownership) in the PR-description section, deferring detail to GUIDELINES.md.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>

* docs: tighten Rule 6 escape hatch per Rod — gate on shape under-specified, not file absent

Rod's review: the #5749 no-op came from a fully prose-specified repro (fenced JSON
+ field-level conditions + steps), not a missing file. The old hatch ('why the issue
gave you nothing to load') reads as 'no downloadable attachment', letting someone walk
past a binding JSON block. Gate the hatch on the SHAPE being under-specified instead:
a fenced JSON structure / field conditions / step list pin the shape as bindingly as a
file; if pinned, satisfy every condition and don't add a property the shape never had
to make a guard fire; only say 'constructed, assumed X' when the shape is truly unpinned.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>

* docs: distill Rule 6 repro-shape guidance into a principle (Rod style note)

Rod: 'distill into durable principles, don't enumerate lists; strip negative
conditions that read like narration.' Reworked the addition to lead with the
principle ('a reproduction is whatever pins the bug's shape'), collapse the
capture/JSON/conditions/steps enumeration into flowing prose, and convert the
'don't add a property...' negative into a positive imperative ('bind your fixture
to that shape: satisfy every condition... instead of granting...'). Same for the
CONTRIBUTING bullet.

---------

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-17 20:40:55 -07:00
b470793473 Release: msg_limit ceiling + row-loss-safe pagination (#6152 + #6154, @rh-id) (#6213)
* fix(routes): clamp GET /api/session msg_limit to a server-side ceiling

Memory: ?msg_limit= had no upper bound — a client could request
msg_limit=1000000 (or the frontend's msg_limit=9999 outline-jump path) and
force the server to assemble and serialize an unbounded message payload. The
frontend's real pagination grows windows by ~30 at a time (initial load 30,
load-older +30, msg_before paging 30), so only the outline-jump and
pathological/hostile cases ask for huge values.

Add _MAX_MSG_LIMIT (500) and clamp at the query-parse site
(max(1, min(int(_msg_limit), _MAX_MSG_LIMIT))). The ceiling is generous — far
above any legitimate visible-row window — so real pagination is unaffected. The
existing _messages_truncated signal already covers the clamped case: when the
request exceeds the ceiling, _message_window_for_display returns
_messages_offset > 0 and the client learns more rows exist (load-older still
works). The bare no-msg_limit path is intentionally unchanged — it is the
documented "give me everything" escape hatch used by branch/undo/jump-to-start
flows that need the full transcript for correct global indices.

Test: tests/test_session_msg_limit_ceiling.py — the ceiling constant is
reasonable (100..2000), an over-ceiling request is clamped and signals
truncation, and legit sizes (5/30/60/100) are unaltered. Existing window/tail
tests (19) unchanged.

Contract Routing
- Task type: memory-bound fix (unbounded msg_limit request payload).
- Touched areas: GET /api/session query parsing (api/routes.py).
- Relevant public docs: AGENTS.md, CONTRIBUTING.md, docs/CONTRACTS.md.
- State layer mutated: none (read-only endpoint response shaping).
- Contract change (additive): ?msg_limit=N is now clamped to _MAX_MSG_LIMIT=500.
  Compatibility: the only frontend caller above the ceiling is outline.js's
  msg_limit=9999, which now receives up to 500 rows + _messages_truncated=true
  (load-older still works). All other callers (30/60) are unaffected. The bare
  no-msg_limit path is unchanged.

Release note: GET /api/session now caps ?msg_limit at 500 rows server-side,
bounding the worst-case payload for oversized requests.

Model Used: builtin:zai-coding-plan/GLM-5.2 (ZCode agent).

* fix(frontend): route msg_limit-heavy paths around the server ceiling

PR #6152 clamps GET /api/session ?msg_limit= to a max of 500 server-side. Two
frontend paths relied on unbounded msg_limit and silently broke for sessions
> 500 visible rows:

1. outline.js _jumpToMessage sent msg_limit=9999 as a "give me everything" hack
   so it could scroll to any message by absolute index (msg-user-<rawIdx>).
   Clamped to 500, the target row's DOM id doesn't exist for early messages in
   long sessions, and the jump silently no-ops (line 119 `if (r)` is falsy).
   Fix: drop msg_limit=9999 — use the bare messages=1 path, which #6152 left
   unbounded precisely for callers that need the full transcript. This restores
   exact pre-#6152 behavior for outline jumps.

2. sessions.js _loadOlderMessages grows msg_limit by +_INITIAL_MSG_LIMIT each
   load (30 -> 60 -> ... -> 500 -> clamped). Past 500 the server returns the
   same clamped tail, olderMsgs shrinks to 0, and endless-scroll / "Load
   earlier" silently stalls — the head becomes unreachable. Fix: once the grown
   requestedLimit meets/exceeds the new _MSG_LIMIT_MAX mirror (500), switch the
   primary fetch to msg_before paging (a fixed _INITIAL_MSG_LIMIT backward page
   keyed off _oldestIdx) — the same bounded paging the existing race-fallback
   uses, so the head stays reachable for arbitrarily long transcripts. The
   response is handled by the existing prepend path (tailMatches is naturally
   false for a page, so the !tailMatches branch reuses the fetched data).

Added _MSG_LIMIT_MAX = 500 constant in sessions.js mirroring backend
_MAX_MSG_LIMIT, documented as kept-in-sync.

Verification: node --check passes on both files; the backend window-contract
tests (test_session_tail_payload, test_session_message_window_renderable_tail,
19 tests) still pass. No JS test harness exists in this repo (vanilla JS,
manual browser verification per TESTING.md) — manual verification needed: open a
> 500-message session, (a) click an outline entry for an early message and
confirm it scrolls+flashes, (b) scroll up past 500 loaded rows and confirm
"Load earlier" keeps producing older messages without stalling.

Contract Routing
- Task type: frontend follow-up to a backend memory fix (#6152).
- Touched areas: outline jump-to-message (static/outline.js), endless-scroll /
  load-older (static/sessions.js _loadOlderMessages).
- Relevant public docs: AGENTS.md, CONTRIBUTING.md, docs/CONTRACTS.md.
- State layer mutated: client-side S.messages (the active-session transcript),
  same as the existing load-older path. No backend state change.
- Scope boundaries: does not weaken the #6152 clamp. The bare messages=1 path
  (outline) was intentionally left unbounded by #6152; the msg_before paging
  path (load-older) is bounded by _INITIAL_MSG_LIMIT and never hits the ceiling.

Release note: fixes outline jump-to-message and endless scroll silently
breaking on sessions longer than 500 messages after the server-side msg_limit
ceiling (companion to #6152).

Model Used: builtin:zai-coding-plan/GLM-5.2 (ZCode agent).

* test(frontend): update static-source assertion for refactored _loadOlderMessages fetch

The static-source test test_session_message_loads_keep_explicit_longer_timeouts
asserted the exact multi-line shape of the _loadOlderMessages fetch strings.
The #6154 refactor wrapped them in a useBeforePaging ternary (tail-growth vs
msg_before paging), reindenting them — the contract the test guards (both
fetches keep timeoutMs:120000 for large state.db loads) is preserved, but the
literal source shape changed, so the assertion failed on shard 0.

Updated the assertions to match the new source shape while keeping the test's
intent: assert each fetch URL + its {timeoutMs:120000} survive in sessions.js.

Model Used: builtin:zai-coding-plan/GLM-5.2 (ZCode agent).

* test(msg-limit): stopgap drift guard for _MSG_LIMIT_MAX === _MAX_MSG_LIMIT

Reviewer guidance (#6154 round-2): the stopgap drift-test belongs in THIS PR
(#6152) because _MAX_MSG_LIMIT is defined here. The frontend _MSG_LIMIT_MAX
mirror lives on #6154 (not master yet), so the assertion SKIPS when the JS
constant is absent and activates automatically once both branches land on master.

The guard: extract _MAX_MSG_LIMIT from api/routes.py and _MSG_LIMIT_MAX from
static/sessions.js via regex; assert equality when both present. If they drift,
load-older silently stalls at the wrong value (the exact regression #6154 fixed).
Retired by #6177 (metadata-exposure), which removes the mirror entirely.

Verified: on this branch the cross-check SKIPS (_MSG_LIMIT_MAX absent, as
expected — #6154 not landed) and the backend-existence test PASSES. Extraction
logic confirmed correct against simulated match (500==500 passes) and drift
(999!=500 fails) cases.

Model Used: builtin:zai-coding-plan/GLM-5.2 (ZCode agent).

* fix(session): prevent silent row-loss in msg_limit ceiling paging (gate follow-up)

Adversarial review of #6152/#6154 reproduced two silent row-loss regressions in
static/sessions.js; both fixed here, plus a stale source-string assertion update
and the CHANGELOG entry:

1. _loadOlderMessages: gate the suffix-continuity heuristic on !useBeforePaging so
   a raw-row-heavy msg_before cursor page whose visible text repeats the current
   tail can't be misclassified as the cumulative tail and wholesale-replace it
   (previously dropped 470 of 940 rows). msg_before pages are always prepended.

2. loadSession same-session refresh: only apply the width-hint msg_limit when it
   is <= _MSG_LIMIT_MAX; above the ceiling the backend would clamp and silently
   shrink an already-loaded >500-row transcript to the last 500. Over-ceiling
   reloads now use the bare full-transcript path (no msg_limit / no expand_renderable).

3. tests/test_webui_external_refresh_frontend.py: update the reloadLimitParam
   source assertion to the boundedReloadLimit form.

Re-gate: Codex adversarial SAFE TO SHIP (940/940 rows retained on cursor paging,
1000/1000 on refresh, no seam duplication, small sessions still use the fast tail
window); targeted + sessions/pagination/regression suites green.

Co-authored-by: Ruby Hartono <rh-id@users.noreply.github.com>

* test: fix two node-harness tests broken by the boundedReloadLimit change

- test_issue3162_ensure_messages_loaded: replace the brittle fixed 4500-char
  window (which the boundedReloadLimit lines pushed the carry-forward past) with
  robust brace-balance function-body extraction.
- test_cross_session_message_load_isolation: define _MSG_LIMIT_MAX=500 in the node
  harness env; _ensureMessagesLoaded now references it, and an undefined value made
  boundedReloadLimit=null -> the fetch URL dropped msg_limit/expand_renderable and
  mismatched the ordered api() harness -> stall/timeout.

---------

Co-authored-by: Ruby Hartono <58564005+rh-id@users.noreply.github.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: Ruby Hartono <rh-id@users.noreply.github.com>
2026-07-17 20:15:40 -07:00
nesquena-hermes d65c0baf8a Merge pull request #6193 from starship-s/fix/session-truncate-linear-alignment
perf(session): linearize context truncation alignment
2026-07-17 16:04:14 -07:00
Hermes Agent 64f98dadd0 fix: preserve lazy alignment semantics 2026-07-17 15:36:55 -06:00
Hermes Agent 0e28cc2aa7 test: broaden truncation alignment coverage 2026-07-17 15:36:55 -06:00
Hermes Agent d8360bfe7d refactor: clarify alignment safety checks 2026-07-17 15:36:55 -06:00
Hermes Agent 939848de22 test: cover unsafe alignment metadata 2026-07-17 15:36:55 -06:00
Hermes Agent 509263e161 perf: linearize session truncate alignment 2026-07-17 15:36:55 -06:00
nesquena-hermes 9504bce040 Merge pull request #6186 from franksong2702/franksong2702/investigate-3929-partial-work
Live Stream: recover interrupted Thinking after restart
2026-07-17 14:06:53 -07:00
Frank Song 5c34dd9248 fix(streaming): skip reasoning rows for tool anchors 2026-07-18 04:36:00 +08:00
Frank Song e0dee876a0 fix(streaming): keep recovered tool call anchors 2026-07-18 03:23:45 +08:00
Frank Song 3330bb4bd1 fix(streaming): preserve recovered context shape 2026-07-18 03:23:45 +08:00
Frank Song 59b6022dd2 fix(streaming): harden recovered reasoning context ownership 2026-07-18 03:23:45 +08:00
Frank Song b741e17275 fix(streaming): recover interrupted reasoning 2026-07-18 03:23:45 +08:00
nesquena-hermes 920cdd56c0 Merge pull request #6191 from Gerkinfeltser/fix/codex-handoff-summary-current
fix: omit unsupported Codex handoff token cap
2026-07-17 12:05:10 -07:00
mol 04783d11d8 fix: normalize Codex handoff base URL 2026-07-17 16:33:52 +00:00
mol 6c863ae9c9 fix: omit unsupported Codex handoff token cap 2026-07-17 16:17:19 +00:00
nesquena-hermes f6265cc962 Merge pull request #6182 from starship-s/perf/session-switch-merge-key-cache
perf(sessions): cache reconciliation message keys
2026-07-17 03:04:15 -07:00
nesquena-hermes a3d27eb7ca Merge remote-tracking branch 'origin/master' into pr6182-g 2026-07-17 09:47:27 +00:00
nesquena-hermes d6c23582e2 Merge pull request #6181 from rodboev/pr/6108-sessiondb-retry
fix(#6108): retry transient SessionDB init for WebUI session_search
2026-07-17 02:46:30 -07:00
Rod Boev 73305706c6 fix(#6108): bound SessionDB retries to transient sqlite contention 2026-07-17 05:42:00 -04:00
Hermes Agent 1bcf8f8b46 perf(sessions): cache reconciliation message keys 2026-07-17 02:11:31 -06:00
Rod Boev 2e1867c6e5 fix(#6108): keep session_search alive through transient state.db lock races 2026-07-17 03:55:22 -04:00
nesquena-hermes 92ebc10cac Merge pull request #6134 from franksong2702/franksong2702/fix-6131-provider-aware-model-selection
Settings: preserve provider identity for duplicate model IDs
2026-07-16 20:05:09 -07:00
Frank SongandPaperclip 765b691b91 merge master into provider-aware model selection
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-07-17 09:28:09 +08:00
nesquena-hermes 650810dbb1 Merge pull request #6153 from rh-id/fix/mem-state-db-read-backstop
fix(models): add defensive row backstop to display-path state.db message read
2026-07-16 18:20:34 -07:00
Frank SongandPaperclip 484f00aa62 merge master into provider-aware model selection
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-07-17 09:18:46 +08:00
nesquena-hermes c8edc97e89 Merge remote-tracking branch 'origin/master' into p6153b 2026-07-17 01:16:08 +00:00
nesquena-hermes 196f6e261c Merge pull request #6137 from rh-id/fix/mem-streamchannel-queue
fix(stream): cap StreamChannel per-subscriber queues (drop-oldest on full)
2026-07-16 18:15:22 -07:00
nesquena-hermes 13efcbe06c Merge remote-tracking branch 'origin/master' into p6153b 2026-07-17 01:10:27 +00:00
nesquena-hermesandrh-id 5832b1e06c test(stream): drop unused MagicMock import (ruff F401)
Co-authored-by: rh-id <rh-id@users.noreply.github.com>
2026-07-17 01:10:05 +00:00
nesquena-hermes a8143affe2 Merge remote-tracking branch 'origin/master' into p6137b 2026-07-17 01:09:51 +00:00
Ruby Hartono f445c3ae39 fix(routes): gate state.db row backstop on absence of truncation_boundary prefix
Reviewer regression (reproduced): _STATE_DB_DISPLAY_ROW_BACKSTOP (newest-50k SQL
cap) was applied UNCONDITIONALLY to the GET /api/session display path's state.db
read. But merge_session_messages_append_only needs the rows at/around the
session's truncation_boundary (the preserved prefix) to reconcile correctly. A
newest-N-only cap drops those boundary rows for a >N-row session and corrupts
the merge (silently losing the preserved prefix), and also prevents older-page
(msg_before) requests from ever reaching them. So a pathological-but-real >50k-
row compressed session got a corrupted merge / unreachable older history rather
than a clean bound.

Fix: gate the backstop on the SAME conditions
_state_db_since_timestamp_for_limited_display uses to decide a read is
boundary-free — not msg_before paging, and no truncation_watermark /
truncation_boundary. Those cases stay on the full (uncapped) read; the backstop
is now a defensive net for the common uncompressed initial-tail path only.
Extracted into _state_db_backstop_limit_for_display for direct test coverage.

The get_state_db_session_messages(limit=) signature + newest-rows subquery are
unchanged — only the display-path APPLICATION of the cap is gated.

Test: tests/test_state_db_backstop_boundary_gate.py — backstop applies with no
boundary prefix; SKIPPED when truncation_boundary / truncation_watermark set or
msg_before paging; empty-string markers treated as unset (matching the existing
helper). Existing backstop + reconciliation suites (53 tests) unchanged.

Model Used: builtin:zai-coding-plan/GLM-5.2 (ZCode agent).
2026-07-17 07:19:47 +07:00
Ruby Hartono d57fa7ba3b fix(stream): retry (not break) on queue.Empty so terminal frames survive a concurrent drain
Reviewer regression (reproduced): the broadcast drop-oldest loop did
put_nowait -> on Full, get_nowait -> on Empty, break. But Empty here means a
concurrent consumer (the SSE handler thread) DRAINED the queue between our Full
and get_nowait — the queue now has space, which is exactly the condition to
retry the put. `break` silently discarded `item`, and when `item` was a terminal
frame (stream_end/error/cancel) the subscriber never received it, leaving the
client attached indefinitely (spinner-forever). The "# cap 0 defensive guard;
never reached" comment was the flawed assumption: any concurrent drain makes
the Empty branch reachable at any cap.

Fix: on queue.Empty, `continue` (retry the put into the now-empty queue) in
both the broadcast path and the replay path (for consistency; the replay path
runs under self._lock with a fresh queue so it's unreachable in practice, but
the same race-safe rule applies). No infinite-loop risk: each iteration either
succeeds a put (break) or succeeds a get (progress); the continue only fires on
Empty, after which the next put has space.

Test: test_terminal_frame_survives_concurrent_drain_during_drop_oldest
deterministically simulates the interleaving with a custom queue whose first
post-Full get_nowait raises Empty (as if a concurrent consumer drained it),
then runs BOTH the pre-fix `break` logic (drops the terminal frame — proving
the test bites) and the post-fix `continue` logic (delivers it).

Model Used: builtin:zai-coding-plan/GLM-5.2 (ZCode agent).
2026-07-17 07:04:04 +07:00
Frank SongandPaperclip 17834a6e07 merge master into provider-aware model selection
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-07-17 05:44:58 +08:00
nesquena-hermes eac20f2131 Merge pull request #6092 from pxxD1998/fix/webui-agent-runtime-update-guard
fix: block stale Agent runtime after source updates
2026-07-16 14:32:40 -07:00
nesquena-hermes ea3cba3823 Merge remote-tracking branch 'origin/master' into pr6092-gate 2026-07-16 21:05:14 +00:00
Frank SongandPaperclip f6d22641cc merge master into provider-aware model selection
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-07-17 04:20:34 +08:00
nesquena-hermesandnesquena-hermes 11773af0e3 docs(changelog): add #6143 (SessionDB subagent use-after-close) + #6140 (CLI-cache LRU cap) bullets (#6158)
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-16 13:17:42 -07:00
Frank SongandPaperclip c5ff4f5f3f merge master into provider-aware model selection
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-07-17 04:10:58 +08:00
nesquena-hermes b38586da38 Merge pull request #6140 from rh-id/fix/mem-cli-sessions-cache-cap
fix(models): cap _CLI_SESSIONS_CACHE with LRU eviction (drop-oldest)
2026-07-16 13:08:41 -07:00
nesquena-hermes 1101da4663 Merge remote-tracking branch 'origin/master' into p6140 2026-07-16 20:04:54 +00:00
nesquena-hermesandrh-id ecb66536ec test(diagnostics): add subscriber_dropped_events to diagnostic_snapshot exact-dict assertion
Co-authored-by: rh-id <rh-id@users.noreply.github.com>
2026-07-16 19:48:08 +00:00
nesquena-hermesandrh-id b537be3008 test(core): accept new limit kwarg in get_state_db_session_messages stub
Co-authored-by: rh-id <rh-id@users.noreply.github.com>
2026-07-16 19:47:36 +00:00
Frank SongandPaperclip 3bc757d272 merge master into provider-aware model selection
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-07-17 03:43:09 +08:00
nesquena-hermes fbe10fd535 Merge pull request #6143 from carlotestor/fix/sessiondb-close-under-live-subagents
fix: stop closing SessionDB under live background subagents
2026-07-16 12:39:47 -07:00