Commit Graph
7367 Commits
Author SHA1 Message Date
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
nesquena-hermes 3bb4fc8c20 Merge remote-tracking branch 'origin/master' into pr6143-gate 2026-07-16 19:34:40 +00:00
nesquena-hermesandcarlotestor dcdcfb9977 fix(streaming): degrade self-heal SessionDB to None on failed rebuild
Codex gate on #6143 found a SILENT regression the reuse-path fix introduced:
_replace_session_db_in_kwargs early-returned the OLD handle when the rebuild
failed, but master assigned None. If the old handle was already CLOSED, both
credential self-heal callers (streaming.py:9032/10253) then built a fresh agent
around a closed SessionDB -> every persist/search fails with 'NoneType has no
attribute execute' while the chat keeps going. Guard on _session_db_is_open:
keep a live handle (subagents hold it by ref), else degrade to None (clean lazy
reinit, master behaviour). +2 regression tests.

Co-authored-by: carlotestor <carlotestor@users.noreply.github.com>
2026-07-16 19:14:53 +00:00
Ruby Hartono dec041997f fix(models): add defensive row backstop to display-path state.db message read
Memory: get_state_db_session_messages had no SQL LIMIT — it full-scanned the
state.db messages table and fetchall()'d into a Python list. The display path
(GET /api/session) already has a careful since_timestamp optimization that
bounds the common uncompressed tail load, but it deliberately bails (returns
None) on compressed sessions (truncation_watermark/boundary) and msg_before
paging — in those cases the read used to full-scan with no cap. A
pathological/huge state.db could then materialize unbounded rows into memory.

Add an opt-in `limit` parameter to get_state_db_session_messages (applied as a
SQL LIMIT that keeps the NEWEST rows via a DESC subquery re-sorted ASC). It is
a BACKSTOP, not a semantic window: the display window counts visible rows
post-reconciliation, so a true window LIMIT here would corrupt the
sidecar/state.db merge (see _state_db_since_timestamp_for_limited_display,
which deliberately does NOT SQL-LIMIT raw rows for exactly that reason).

Thread a generous _STATE_DB_DISPLAY_ROW_BACKSTOP (50000) from the GET /api/session
display caller only. The full-history model-context callers (reconciliation at
models.py:3437, context reconstruction at :8576, new-turn context at
streaming.py:8443) do NOT pass a limit — their semantics require the full
history, unchanged.

Test: tests/test_state_db_read_backstop.py — no-limit returns full history
oldest-first (model-context contract unchanged); limit=N keeps the newest N
re-sorted oldest-first; limit > row count returns all; limit<=0 clamps to 1
(newest); invalid limit falls back to unbounded; limit composes with
since_timestamp. Existing state.db active-filter + reconciliation suites (54
tests) unchanged.

Contract Routing
- Task type: memory-bound fix (unbounded state.db row materialization on the
  display path's bail cases).
- Touched areas: state.db message reader (api/models.py) + GET /api/session
  display caller (api/routes.py). NOT the model-context callers.
- Relevant public docs: AGENTS.md, CONTRIBUTING.md, docs/CONTRACTS.md,
  docs/rfcs/webui-run-state-consistency-contract.md (Model context layer).
- State layer mutated: none (read-only). The model-context read path is
  UNCHANGED (no limit passed) — Invariant 1 (visible current turns enter model
  context) and the reconciliation contract are preserved. Only the display path
  gets the defensive cap, which does not affect what the agent receives.
- Scope boundaries: this is narrower than a naive "add LIMIT everywhere" —
  _state_db_since_timestamp_for_limited_display's docstring explains why a
  semantic window LIMIT would corrupt the merge; this cap is purely a
  pathological-input safety net above any legitimate session size.

Release note: GET /api/session's state.db read now has a defensive row backstop,
capping memory when the bounded since_timestamp optimization bails on
compressed/paged sessions.

Model Used: builtin:zai-coding-plan/GLM-5.2 (ZCode agent).
2026-07-16 23:40:26 +07:00
Frank SongandPaperclip 6e3543de16 merge master into provider-aware model selection
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-07-16 23:44:01 +08:00