Commit Graph
481 Commits
Author SHA1 Message Date
Rod Boev 5826d76da0 fix(#6008): publish the authoritative approval head 2026-07-22 12:31:12 -04:00
8586572da5 Release: #6389 stream-teardown leak fix (exp-v0.52.139) (#6416)
* fix(#6389): remove clear_offline_buffer() teardown blocks that cause data loss on late reconnects

Keep the real leak fixes:
- unregister_stream_owner() in both streaming.py and gateway_chat.py
- AGENT_INSTANCES.pop() in gateway_chat.py

The clear_offline_buffer() calls could clear a channel's offline buffer
after a late reconnect already fetched the channel but before it subscribed,
causing the reconnecting client to lose all buffered events (including
stream_end) and hang indefinitely on heartbeats.

STREAMS.pop() already releases registry ownership; GC reclaims the buffer
once the last outstanding channel reference is dropped, correctly preserving
replay for in-flight reconnects.

* docs(changelog): stamp #6389 stream-teardown leak fix

---------

Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: nesquena-hermes <agent@webui>
2026-07-22 04:09:49 +00:00
nesquena-hermes 5ff16c59d4 fix(streaming): catch OverflowError in _terminal_turn_duration so a huge/invalid pending_started_at can't abort terminal-error settlement
Codex gate on #6312 found the extracted helper caught only (TypeError, ValueError)
while the inline code it replaced caught Exception; float(10**400) raises
OverflowError, escaping the helper and aborting terminal-error cleanup before the
error message is persisted / apperror is emitted. Widen _terminal_turn_duration's
float() guard to (TypeError, ValueError, OverflowError) — the exact exceptions
float() can raise. Adds 10**400 to the existing invalid-origin parametrize
(red-before-green verified).
2026-07-21 20:06:17 +00:00
Frank Song 9bde2cf3af fix: reject future terminal turn origins 2026-07-21 15:05:17 +08:00
Frank Song de96c1d384 fix: harden terminal error worklog settlement 2026-07-21 15:05:17 +08:00
48b40d930a Release: isolate Hermes home per streaming turn (#5877, @starship-s) (#6379)
* fix(profiles): isolate Hermes home per streaming turn

Assisted-by: OpenCode:gpt-5.3-codex-spark
Assisted-by: Hermes Agent:gpt-5.6-sol
Assisted-by: Codex:gpt-5.3-codex-spark

* fix(profiles): gate skill isolation by capability

Assisted-by: OpenCode:gpt-5.3-codex-spark
Assisted-by: Hermes Agent:gpt-5.6-sol
Assisted-by: Codex:gpt-5.3-codex-spark

* fix(profiles): harden fallback lock boundaries

Assisted-by: OpenCode:gpt-5.3-codex-spark
Assisted-by: Hermes Agent:gpt-5.6-sol

* test(profiles): adapt streaming isolation harness

Assisted-by: Codex:gpt-5.3-codex-spark
Assisted-by: Hermes Agent:gpt-5.6-sol

* Release: isolate Hermes home per streaming turn (#5877, @starship-s)

---------

Co-authored-by: starship-s <45587122+starship-s@users.noreply.github.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-21 03:48:00 +00:00
302843c89f Release: compact completed image tool-results (#6315, @sjungwon03) (#6371)
* fix(session): compact completed native vision results

* streaming: guard image-part compaction against unhashable part.type (isinstance str) + regression test [maintainer fix on @sjungwon03 #6315]

* docs(changelog): stamp #6315 completed-image tool-result compaction

---------

Co-authored-by: sjungwon03 <sjungwon03@gmail.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-20 20:11:29 +00:00
307b7fce5f Release: terminal-error settlement timing/seal (#6323) + Docker experimental builds (#6329) (#6341)
* fix: preserve timing and seal tool rows on terminal error (#6309)

* fix: publish Docker experimental builds to ghcr (#6298)

- Add exp-v* trigger to release workflow so experimental tags build
  and push Docker images
- Add :experimental floating tag for experimental channel, keeping
  :latest scoped to stable v* tags only
- Mark GitHub Releases from exp-v* tags as pre-releases
- Document available Docker tags (:latest, :experimental, version
  pins) in docs/docker.md

Closes #6298

* docs(changelog): stamp #6323 terminal-error timing/seal + #6329 Docker exp builds

---------

Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-19 22:17:36 +00:00
6ad84d7dd3 fix(wakeup): route async-delegation completions by origin + durable-claim delivery (#6283)
Route async-delegation completions by the immutable origin_ui_session_id (exact origin tab) and deliver them through a durable claim/complete/release lifecycle so they arrive exactly once, restart-safe, on both the background wakeup and next-turn drain paths. Combines #6185 (@carlotestor) + #6159 (@sysophelper-droid); supersedes #6002/#6225.

Co-authored-by: carlotestor <carlotestor@users.noreply.github.com>
Co-authored-by: sysophelper-droid <sysophelper-droid@users.noreply.github.com>
2026-07-19 02:26:55 +00: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
Rod Boev 73305706c6 fix(#6108): bound SessionDB retries to transient sqlite contention 2026-07-17 05:42:00 -04: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 ea3cba3823 Merge remote-tracking branch 'origin/master' into pr6092-gate 2026-07-16 21:05:14 +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
carlotestor 7d2ca8cabb fix: address greptile review on SessionDB adopt helper
- Log failed closes in _adopt_session_db_for_cached_agent with
  logger.debug(..., exc_info=True) instead of bare pass, matching
  _replace_session_db_in_kwargs so a failed close (EMFILE risk) is
  diagnosable.
- Drop the fragile exec/source-slice fallback in the test helper import;
  real import or pytest.skip. Source-level pins still cover reverts
  without importing streaming.
2026-07-16 16:53:42 +02:00
carlotestor 0cee7dba47 fix: stop closing SessionDB under live background subagents
Background subagents (delegate_tool) capture parent_agent._session_db by
reference. The cached-agent reuse path created a fresh SessionDB every
stream request and unconditionally closed the previous handle (PR #1421
EMFILE fix). A server-side wakeup / new turn for the same parent session
could therefore close the shared handle while children were still
flushing messages, producing:

  Session DB append_message failed: 'NoneType' object has no attribute 'execute'

Reproduce (observed in production):
1. Parent WebUI session runs delegate_task with N background subagents
2. Parent turn tears down / idle-hook redelivers a deferred wakeup
3. New stream for the parent adopts a fresh SessionDB and closes the old one
4. Live subagents' append_message hits conn=None

Fix: _adopt_session_db_for_cached_agent reuses a still-open handle (and
closes the unused new one — still EMFILE-safe) and only replaces when
the existing handle is missing or already closed. Same guard applied to
_replace_session_db_in_kwargs (credential self-heal). LRU eviction still
closes at true session boundaries.

Tests updated to pin the adopt helper and the subagent-safe semantics.
2026-07-16 16:38:43 +02:00
pxxD1998 5df6b1fe8a fix: stop stale agent runtime after source updates 2026-07-16 22:28:35 +08:00
chsong 139c76535b fix tool-limit final answer classification 2026-07-15 19:38:25 +00:00
Rod Boev 001d7985bc feat(#6006): instrument send-to-first-token latency 2026-07-13 17:56:44 -04:00
Rod Boev 064ad82ee6 fix(#5940): key pending-turn recovery to exact checkpoints 2026-07-13 03:41:39 +00:00
nesquena-hermes 53a4b35555 harden #5979 round 3 (Codex): signature-based explicit-pick provenance
Codex found a verified lifecycle hole: the bare boolean model_explicitly_picked
was not cleared on /api/session/update (api/routes.py:14438-14460 changes+persists
the model/provider without touching it), so after an update+reload the flag stayed
True and a cold #433 stale leftover (openai/gpt-5.4) was wrongly preserved instead
of stripped to gpt-5.4.

Fix (Codex's prescription): replace the unscoped boolean with a SIGNATURE bound to
the routing context.
- api/models.py: new model_explicit_pick_signature(model, provider) ->
  '<model>\x1f<provider>'; Session.model_explicit_pick_signature persisted field
  (replaces the boolean; in METADATA_FIELDS, restored via kwargs, survives restart).
- api/routes.py /api/chat/start: on a fresh explicit_model_pick, stamp the signature
  of the resolved model+provider.
- api/streaming.py: treat the send as a deliberate pick ONLY when the persisted
  signature equals the signature recomputed from the session's CURRENT persisted
  model+provider. Any model/provider change (chat-start, session-update,
  normalization, provider repair) yields a different signature and auto-invalidates
  the stale pick — no per-mutation-site clearing needed.

So: deliberate pick on unchanged context -> preserve (b3nw); any switch away ->
signature mismatch -> unmarked -> legacy strip (#433 stays correct even after a
session-update+cold-restart). Adds signature match/invalidate/persist tests.
75 focused tests pass (resolver + sprint40 + the 2 streaming-mock suites).
2026-07-13 01:58:20 +00:00
nesquena-hermes 45cb5bd3e6 feat(#5979): persisted explicit-pick provenance for cold custom-proxy resolution
Nathan's call: adopt Codex's complete mechanism instead of the unconditional
cold-preserve. The cold-catalog custom-proxy decision is now gated on whether the
user DELIBERATELY selected the model this session, closing the residual where a
stale #433 leftover would preserve+400 on a cold+no-disk send.

Mechanism:
- resolve_model_provider(model_id, *, explicitly_picked=False): in the custom-arm
  cold branch (no warm provenance, not config-declared), explicitly_picked=True
  preserves the vendor namespace verbatim (deliberate pick, proxy routes on it);
  otherwise the legacy redundant-prefix strip runs (stale cross-provider leftover,
  #433). Warm endpoint-advertised provenance still wins over the flag.
- Session.model_explicitly_picked: new persisted field (default False, restored
  from metadata via **kwargs, listed in METADATA_FIELDS so it survives a restart
  — b3nw's exact cold scenario).
- /api/chat/start stamps it: True on a fresh explicit_model_pick; cleared only
  when the resolved model actually CHANGED without a pick (real switch away);
  preserved across plain follow-up sends of the same model (the onchange marker
  is one-shot, so per-send explicit_model_pick alone would drop the intent).
- streaming.py reads s.model_explicitly_picked and passes explicitly_picked= into
  resolve_model_provider (inside the profile scope).

Independent end-to-end matrix (all pass): ben default cold, ben picked non-default
cold+warm, ben stale cold (strips), #433 warm+cold, #3872 bedrock, #548 zai-org.
67 resolver+sprint40 tests pass incl. new explicit-pick + stale-companion +
persistence round-trip.
2026-07-13 01:43:17 +00:00
nesquena-hermes e3152572aa harden #5979 follow-up per Codex gate: non-blocking disk-only warm + profile scope
Codex gate found two real issues in the send-path warm:

1. Blocking lock wait (CORE): get_available_models(prefer_cache=True) still
   acquires _available_models_cache_lock and can wait up to ~60s (unbounded in
   synchronous rebuild mode) on an in-flight rebuild — unacceptable on the send
   hot path. Rewrote warm_models_catalog_provenance_if_cold as a genuinely
   non-blocking, disk-only publish: try the cache lock NON-BLOCKING (return
   immediately if busy — a concurrent build will publish provenance itself),
   read ONLY the on-disk cache via _load_models_cache_from_disk (no network, no
   rebuild), publish snapshot+fingerprint + _sync_models_cache_provenance().
   Verified: returns in ~3.5ms while the lock is held (was a 60s block).

2. Profile scope (CORE): the streaming worker is a separate thread that does NOT
   inherit the HTTP handler's request-profile TLS. Both the warm and the resolve
   read profile-keyed config (cache path + source fingerprint via
   get_active_profile_name), so a cold send from a NAMED profile could resolve
   against the DEFAULT profile's config and route to the wrong provider/base_url
   (get_available_models reloads config on path mismatch). Wrapped warm + resolve
   in profile_scope_for_detached_worker(_resolved_profile_name) so both see the
   owning session's profile (no-op for default/root).

Tests: rewrote never-live-rebuilds (now asserts warm NEVER calls
get_available_models — reads disk directly) and added a non-blocking regression
(warm returns <2s while the cache lock is held). 65 resolver+sprint40 tests pass;
b3nw end-to-end + non-blocking + disk-warm all verified.
2026-07-13 01:43:17 +00:00
nesquena-hermes 6745827eb5 fix(#5979 follow-up): preserve custom-proxy vendor namespace on cold-catalog sends
The earlier #5979 fix (PR #5980, exp-v0.52.53) resolved the WARM-catalog and
config-declared cases, but b3nw confirmed truncation persists on exp-v0.52.55/56.

Root cause (reproduced on master, confirmed by Codex + Fable):
b3nw's config is provider: custom:llm-proxy, model.default: x-ai/grok-4.5, no
models[] allowlist. His DEFAULT (x-ai/grok-4.5) preserves via config-declaration,
but a model he SELECTS in the picker that isn't the default (x-ai/grok-composer-2.5-fast)
truncates on every COLD send. The send path (api/streaming.py:7857) calls
resolve_model_provider without warming the catalog, and the #1855 chat/start fast
path deliberately skips the catalog when a session already has a persisted
model+provider -- so 'cold at send' is the designed state after any restart /
settings-save / cache-expiry, not a rare race. With provenance None and no config
declaration, the #5980 tri-state fell through to the LEGACY family strip
(if prefix in _PROVIDER_MODELS and _is_first_party_model: return bare), and
grok-composer-2.5-fast had graduated into the x-ai first-party catalog -> stripped.

Fix (two changes, both advisor-verified):
1. Flip the custom-arm cold default from family-strip to PRESERVE-verbatim
   (api/config.py). For an explicit custom / custom:<slug> proxy the strip now
   requires POSITIVE provenance (endpoint advertises only the bare id, #433). A
   wrong strip destroys routing info the user can't recover; a wrong preserve
   fails loudly and self-heals on warm. This also honours _endpoint_advertised_model_ids's
   own documented 'None -> preserve verbatim' contract and removes the data-driven
   flaw where a model graduating into the static catalog silently flips routing.
2. Network-free provenance warm on the send path
   (warm_models_catalog_provenance_if_cold, called in streaming.py before the
   resolve). prefer_cache=True republishes provenance from the durable disk cache
   (no live probe, no _cfg_lock held on the worker thread -> no lock-order
   deadlock), so #433's bare-only-strip stays exact whenever a disk cache exists.

Residual (accepted, strictly better than master): a #433-shaped wrong-preserve
now requires memory cold AND disk cache absent/invalid (first run, post-config-save
window, schema/version bump). It fails loudly and heals on the next /api/models or
session-visit rebuild. Under master b3nw's failure was the mirror image but silent,
recurring every send, and unfixable short of declaring every model in config.

Supersedes rodboev's #5996, which targeted the providers.<slug> arm and does not
cover b3nw's actual custom:llm-proxy config (confirmed by Codex).

Tests: rewrite the cold-fallback test (strip -> preserve), sharpen the
foreign-profile test so trusted-strip vs ignored-preserve genuinely diverge, add
b3nw's exact cold non-declared custom:slug case, and add three warm-helper tests
(publishes-from-disk, noop-when-warm, never-live-rebuilds). 64 resolver+sprint40
tests pass; b3nw end-to-end verified; #433 positive-provenance strip verified.

Thanks @b3nw for the CLI-vs-WebUI comparison that localized it.
2026-07-13 01:43:17 +00:00
Jay Stothard c8bf3a1058 fix(streaming): preserve terminal failure precedence 2026-07-12 22:39:15 +00:00
Jay Stothard fe222d4ba2 fix(streaming): avoid false no-response after final tool answer 2026-07-12 22:39:15 +00:00
b2ae71e01a Release exp-v0.52.48: post-compression context metering (#5872, #4685) (#5977)
* fix(#4685): meter post-compression model context

* Release exp-v0.52.48: meter post-compression model context (#5872, #4685)

---------

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-11 20:41:43 -07:00
nesquena-hermesandnesquena-hermes bcb9b24b58 Release exp-v0.52.47: surface real provider error on failed turn (#5940) (#5971)
* fix(#5940): surface non-retryable provider error instead of no_response

The Agent emits a non-retryable terminal error (e.g. HTTP 400 invalid model / no
credentials) via its lifecycle status_callback, but the WebUI dropped it and the
run result/_last_error were empty for that abort path -> turn completion showed the
misleading no_response 'silent rate limit, try again' message. Capture the emitted
terminal error in _agent_status_callback and seed _last_err with it at completion so
_classify_provider_error reports the real cause (model_not_found / auth) with
actionable guidance. +regression test.

* Release exp-v0.52.47: surface non-retryable provider error instead of no_response (#5940)

---------

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-11 18:43:25 -07:00
Matt Van Horn 14f6103655 fix: teach ephemeral system prompt that redacted credential values are intentional 2026-07-11 00:15:47 -07:00
t d967131056 fix(#3929 ux): gate wakeup-pause disclosure on actual pause + cover both error paths
Codex re-gate caught 2 defects in the prior UX commit:
1. streaming.py:8921 — the disclosure fired for ANY process_wakeup failure (e.g.
   rate-limit) even when no pause was recorded -> falsely claimed 'retries paused'
   while process_wakeup_pause stayed empty. Now gated on the record_* return value.
2. streaming.py:10130 — the raised-exception credential_pool_empty path (exercised
   by the existing agent test) recorded the pause but omitted the disclosure. Added
   the same conditional disclosure there.
Both paths now keep _error_payload['hint'] in sync with the persisted bubble, and
the disclosure appears only when a pause was actually recorded. 69 tests green.
2026-07-10 19:28:06 +00:00
tandfranksong2702 c26bc21194 ux(#3929): disclose wakeup pause in error card + changelog
Fable UX-gate follow-up on #5732: after the first credential-exhausted error,
automatic wakeups are silently suppressed. Disclose it in the wakeup-source
error card ('Automatic retries for this conversation are paused until you send
a message, switch the model/provider, or fix the credentials.') so the silence
reads as intentional, not a stuck agent — Gmail/Dropbox account-needs-attention
convention. Scoped to the process_wakeup + credential_pool_empty seam only.

Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-07-10 19:16:48 +00:00
Frank Song c641a9d37c Keep streaming terminal events after no-pause success 2026-07-10 14:43:41 +08:00
Frank Song a61586bd7f Preserve latest session on wakeup pause clear 2026-07-10 13:20:40 +08:00
Frank Song 8eabf63049 Lock streaming wakeup pause success clear 2026-07-10 12:01:24 +08:00
Frank Song 920b5e8864 Preserve wakeup pause on cancelled stale credential failure 2026-07-10 11:39:31 +08:00
Frank Song 67b001e57c Fix wakeup pause recovery races 2026-07-10 07:08:36 +08:00
Frank Song 4bbce7bc45 Persist restored wakeup pause after streaming cancel 2026-07-10 05:43:44 +08:00
Frank Song d9e45f1d07 Merge remote-tracking branch 'origin/master' into paperclip/pr-5732 2026-07-10 05:27:46 +08:00
f449ff21c6 Release: hard-prune post-compression tool context (#5667) (#5852)
WebUI hard token-cap on protected-tail tool-result payloads in the MODEL CONTEXT
only, applied once after a confirmed compression event, to prevent post-compression
context blowup. Deep-copy content-replace only (no message removal/reorder → no
orphaned tool_result); visible transcript (s.messages) untouched.

Deep-review re-gate: Codex SAFE (pairing-safety + context-only + idempotent verified
by reproduction), 65 own + 992 regression tests green. Crown-jewel streaming path.

Co-authored-by: t <a@b>
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-07-09 13:41:31 -07:00
Frank Song a9ba8d8368 Merge remote-tracking branch 'origin/master' into paperclip/pr-5732 2026-07-10 01:07:49 +08:00
1e275d04ed Release: queue gateway-owned steer fallback (#5823) (#5845)
Steering a live gateway-owned run had no process-local agent in SESSION_AGENT_CACHE,
so it errored with "No agent cached" and dropped the typed text. Now detects the live
gateway ACTIVE_RUNS stream and queues the steer as the next turn (model/provider/profile
snapshotted first), updates the queue badge, clears the draft, shows a toast — without
cancelling the stream. Gateway sibling of the shipped #5597 fallback.

Codex SAFE + Fable SHIP-UX; byte-identical to PR head; 66 steer/busy-input tests pass;
Nathan visual-approved (desktop + mobile screenshots).

Co-authored-by: t <a@b>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-09 10:04:21 -07:00
Frank Song 0cef502329 Merge remote-tracking branch 'origin/master' into paperclip/pr-5732 2026-07-09 14:02:13 +08:00
ai-ag2026andClaude Opus 4.8 c88d89903a Document intentional lock-free GIL-atomic contract for STREAM_* buffers
The per-token hot path mirrors partial text, reasoning, and live tool
calls into STREAM_PARTIAL_TEXT / STREAM_REASONING_TEXT /
STREAM_LIVE_TOOL_CALLS without holding STREAMS_LOCK, while snapshot
readers (_snapshot_and_append_partial_on_error, cancel_stream) read them
under STREAMS_LOCK. This is safe under the CPython GIL for two reasons —
NOT because `+=` is one bytecode (it is not: `d[k] += s` compiles to
load / add / STORE_SUBSCR):

  1. Single writer — only the streaming thread ever writes a given
     stream_id's buffers, so there is no writer/writer race to tear.
  2. Reader/writer atomicity — `+=` builds a complete new immutable str,
     then the final STORE_SUBSCR binding it into the dict is a single
     atomic bytecode, so a concurrent reader's dict.get returns either
     the old or new *complete* string (strings are immutable), never a
     half-built one. list.append / single-key dict writes are atomic too.

So a reader sees a complete-but-possibly-stale value, never a torn one,
and the snapshot is a best-effort partial reconciled by later journal/SSE
events.

Chose documentation over locking: pulling the writes under STREAMS_LOCK
would add per-token contention against readers copying large buffers and
entangle the documented LOCK -> STREAMS_LOCK ordering (deadlock risk) for
only a recoverable staleness window. Make the assumption explicit at the
write sites and the reader instead. No behaviour change.

Rollback: revert this commit (comments only).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 05:50:17 +00:00
Frank Song 9d9d52958e fix(wakeup): restore pause on streaming stop race 2026-07-09 09:15:04 +08:00
Frank Song b21038f015 Merge remote-tracking branch 'origin/master' into paperclip/pr-5732 2026-07-09 09:06:27 +08:00
ai-ag2026andClaude Opus 4.8 4fc51072a2 fix(streaming): pair metering begin_session with end_session teardown (#4633, #2476)
The streaming worker called meter().begin_session(stream_id) but never a
paired end_session. A turn that produced zero output tokens (pre-flight
cancel, or a setup exception before the first token) left a _SessionMeter
in GlobalMeter._sessions forever: get_stats() only prunes sessions with
first_token_ts > 0, so a zero-token session is never reclaimed, and each
leaked entry inflates the `active` count sent over the SSE metering event.

begin_session() and the metering ticker's .start() are now registered as
the first statements inside the worker's outer `try`, and paired with an
idempotent meter().end_session(stream_id, 0) plus a deterministic
_metering_stop.set() in that try's outer `finally` (the same block that
pops STREAMS/CANCEL_FLAGS) — so every exit path tears the session down.
Deferring .start() until after put() is defined also closes a latent
start-before-put ordering window in the ticker closure.

The metering payload is unchanged; end_session only pops the session.

Verification: tests/test_metering_session_lifecycle.py (leak reproduction,
end_session reclaim, idempotency, begin->cancel-before-token->empty) plus a
plain-assert run on python3.11 (conftest gates pytest to 3.11-3.13; system
python is 3.14). py_compile clean.

WARNING streaming-contract: adjusts the streaming worker's begin/end
teardown ordering. Needs RFC/contract review against docs/rfcs/
(session-sse-contract-v1.md) before merge — draft PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 00:51:28 +00:00
Frank Song ef85f5c5cb Merge remote-tracking branch 'origin/master' into paperclip/pr-5732 2026-07-08 04:27:16 +08:00
kosta 0560f2615b fix(state-sync): mirror WebUI API call counts 2026-07-07 19:48:19 +00:00
kosta 059a06b70d fix(state-sync): mirror WebUI cache counters 2026-07-07 19:48:19 +00:00
Frank Song 76478729b8 fix(wakeup): record pauses from turn route lane 2026-07-08 01:57:46 +08:00