00afdf7fb05ddec0da532fedf025a20dfa4229ec
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
40a6a7696a |
feat: add GLM-5.3 Flash to Z.AI model list (#7323)
* feat: add GLM-5.3 Flash to Z.AI model list Add glm-5.3-flash next to glm-5.3 in _PROVIDER_MODELS["zai"] and mirror it in _FALLBACK_MODELS, following the existing glm-4.5 / glm-4.5-flash sibling pattern. Reasoning-effort gating needs no change: the >= 5.2 version gate already classifies the id at the full effort tier. Adds a regression suite asserting catalog presence, label, ordering, fallback entry, and end-to-end propagation through get_available_models(). * test: pin glm-5.3-flash sibling adjacency in zai provider catalog Per PR review feedback (#7323): the provider-catalog ordering test now asserts glm-5.3-flash immediately after glm-5.3 and glm-5.2 immediately after the flash sibling, matching the adjacency the fallback test already pins. The base<flash relative check is subsumed; flash<glm-5.1 is kept to pin 5.1 after the whole 5.3 generation. --------- Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com> |
||
|
|
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). |
||
|
|
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). |
||
|
|
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). |
||
|
|
bbda885aa6 |
fix(stream): cap StreamChannel per-subscriber queues (drop-oldest on full)
Memory: each connected tab's subscriber queue was queue.Queue() with no maxsize. A slow/backpressured/backgrounded tab accumulated every coalesced token frame for the WHOLE turn (producer = agent token stream) — unbounded per-tab growth, OOM risk with many tabs x long agentic turns. The offline buffer was already capped (#4633), but once a subscriber attached the live-broadcast path flowed into an unbounded per-subscriber queue. Cap per-subscriber queues at _SUBSCRIBER_QUEUE_MAXSIZE (== _OFFLINE_BUFFER_MAXLEN) and drop the OLDEST frame on queue.Full in both the broadcast (put_nowait) and offline-buffer replay (subscribe_with_snapshot) paths, so a slow tab keeps the most recent tail (older frames stay recoverable via the run journal by last_event_id). Mirrors SessionChannel.emit's drop-on-full contract. The cap intentionally equals the offline buffer bound, not SessionChannel's smaller 16: StreamChannel carries the live token stream and has a #4633 reconnect-replay contract (a resubscribing tab must receive the full retained offline tail), so capping below the buffer would force every flaky-network reconnect through the run journal. Matching the buffer bound preserves that contract while bounding live-broadcast memory to the same worst-case #4633 already accepts. The SSE write deadline (SSE_WRITE_DEADLINE_SECONDS) breaks a stuck socket within ~20s, so the overflow window is short; this also bounds it by frame count. Adds a cumulative subscriber_dropped_events diagnostic counter (ops visibility). Test: tests/test_stream_subscriber_queue_cap.py — bounded queue never exceeds cap, drop-oldest keeps newest tail, draining vs non-draining subscribers, and the #4633 reconnect-replay contract still delivers the full offline tail. Existing offline-buffer/gap-recovery/multitab tests unchanged. Contract Routing - Task type: memory-bound fix (unbounded per-subscriber queue). - Touched areas: live chat SSE broadcast (StreamChannel). - Relevant public docs: AGENTS.md, CONTRIBUTING.md, docs/CONTRACTS.md, docs/rfcs/webui-run-state-consistency-contract.md (Live stream/SSE layer), docs/rfcs/session-sse-contract-v1.md (distinct: that RFC governs the PROPOSED per-session endpoint /api/sessions/{id}/events, NOT StreamChannel, which is the /api/chat/stream live token broadcast; no text in that RFC is changed). - State layer mutated: Live stream / SSE layer — specifically the per-subscriber queue inside StreamChannel (api/config.py). NOT the durable run journal, NOT the transcript, NOT replay. - Invariant preserved: the #4633 reconnect-replay contract — a resubscribing tab must receive the full retained offline tail. The cap is set EQUAL to _OFFLINE_BUFFER_MAXLEN so replay delivery is unchanged; only the previously- unbounded live-broadcast-to-slow-tab growth is bounded. Regression: test_reconnect_replay_still_delivers_full_offline_tail (and the pre-existing test_stream_offline_buffer_cap.py suite) pass unchanged. - Scope boundaries: does not touch replay idempotency (Invariant 5), the turn journal, compression, or sidebar metadata. Slow-tab frames dropped at the cap stay recoverable via the run journal by last_event_id. Release note: StreamChannel per-subscriber SSE queues are now bounded (drop-oldest), capping memory for slow/backgrounded tabs during long turns. Model Used: builtin:zai-coding-plan/GLM-5.2 (ZCode agent). No AI-specific tool use mattered beyond the repo's standard read/edit/test workflow. |
||
|
|
015171eba3 |
fix(models): cap _CLI_SESSIONS_CACHE with LRU eviction (drop-oldest)
Memory: the CLI/cron session projection cache was a plain dict with TTL storage but NO size cap and only lazy, same-key eviction. Each value is a copy.deepcopy() of the full CLI/cron session list (the expensive projection behind #4842/#4672). The cache key folds in a state.db content fingerprint that advances on every streamed message, so the next poll builds a NEW key; the previous key's heavy deepcopy was never looked up again, so its lazy-expiry path never ran. A long-lived process under churn could accumulate orphaned heavy deepcopies until the next structural clear_cli_sessions_cache(). Make it an OrderedDict capped at _CLI_SESSIONS_CACHE_MAX_ENTRIES (8) with drop-oldest on write (while len > MAX: popitem(last=False)) and move_to_end on fresh hits, mirroring the _CLAUDE_CODE_PARSE_CACHE / _SIDECAR_METADATA_CACHE LRU pattern in the same file. TTL remains the primary freshness control; the cap is the backstop the plain dict lacked. clear_cli_sessions_cache() keeps .clear(). Test: tests/test_cli_sessions_cache_cap.py — cache stays at len == MAX after MAX+5 distinct keys (oldest 5 evicted), a fresh hit marks the entry most-recently-used so it survives over colder entries, and the cache is an OrderedDict (the regression: it was an unbounded plain dict). Existing streaming-cache-TTL/cron-session tests unchanged. Contract Routing - Task type: memory-bound fix (unbounded projection cache). - Touched areas: CLI/cron sidebar projection cache (api/models.py _CLI_SESSIONS_CACHE). - Relevant public docs: AGENTS.md, CONTRIBUTING.md, docs/CONTRACTS.md, docs/rfcs/webui-run-state-consistency-contract.md (Sidebar/session metadata layer). - State layer mutated: Sidebar/session metadata layer — specifically the in-memory projection cache that backs the CLI/cron sidebar rows. The cache is a read-derivation of state.db content; it is NOT a source of truth. Dropped entries are rebuilt on the next poll (TTL/invalidation already drive rebuilds). - Invariant preserved — Invariant 4 (Maintenance is not activity): the cache is a derived projection; evicting an entry cannot move a session in the sidebar or change a row's content, because the next poll recomputes the projection from state.db. TTL remains the primary freshness control; the LRU cap only bounds how many STALE orphaned deepcopies linger between invalidations. Regression: test_cli_sessions_cache_cap.py plus the existing streaming-cache-TTL and cron-session tests (which exercise the real invalidate/rebuild path). - Scope boundaries: does not change sidebar ordering, unread state, updated_at, or the projection result. The cap is a memory backstop; correctness still flows from the existing TTL + clear-on-mutation invalidation. Release note: the CLI/cron sidebar projection cache is now LRU-bounded, capping worst-case memory for long-lived processes under heavy streaming churn. Model Used: builtin:zai-coding-plan/GLM-5.2 (ZCode agent). No AI-specific tool use mattered beyond the repo's standard read/edit/test workflow. |