Tool-call args were truncated to 120 chars at three sites (_truncate_tool_args,
the live _tool_args_snapshot in api/streaming.py, and _toolArgsSnapshot in
static/ui.js). For incidental args that's fine, but content/diff-bearing keys
(command/cmd/script/code/patch/diff/old_string/new_string/content/path/
file_path) feed the rendered tool card AND the diff reconstruction in
recovery-rebuilt sessions, so the cap silently corrupted long commands/paths and
produced broken/empty diffs. Exempt those keys with a much larger cap
(_TOOL_ARG_CONTENT_CAP = _TOOL_RESULT_SNIPPET_MAX = 4000) at every site, keyed by
key name; incidental args keep the 120 cap; storage stays bounded. Part of #4925.
Codex re-gate 2: post-run flush is bypassed on exception (jumps to outer except) + retry
paths, so a buffered reasoning tail was lost before apperror there. Added a guarded
_flush_reasoning_buffer() at the top of the inner finally (guaranteed exit for
normal/exception/retry) so the tail always reaches the live Thinking view before teardown.
+ regression asserting the finally flush.
Co-authored-by: wlknight <wlknight@users.noreply.github.com>
Codex re-gate caught a SILENT gap: the tail-flush only ran on on_reasoning(text=None),
but the agent never calls reasoning_callback(None) — so the last sub-100ms coalesced
buffer was silently lost, and a reasoning→tool/output transition stranded the tail.
Added a shared _flush_reasoning_buffer() helper called at every boundary that closes or
reorders the live reasoning stream: on_token (visible output starting), on_tool (tool
boundary), the defensive on_reasoning(None), and right after agent.run_conversation()
returns (terminal catch-all for a turn that ends on reasoning with no trailing token/tool).
Each flush emits the buffered chunk then clears it (since-last-flush delta semantics, so
the frontend's reasoningText += text stays correct). + regression test for all sites.
Co-authored-by: wlknight <wlknight@users.noreply.github.com>
Module-level _compact_for_echo_compare (strips ALL whitespace, \s+ -> '') so _is_visible_output_echo's
endswith() suffix-match detects interim_assistant prose that only differs from visible token prose by
line-break/paragraph formatting. Hoisted to module scope (importable) + behavior-based test (real
token vs interim text equality). Codex SAFE (exact suffix match, not substring). 103 echo/journal tests.
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
A failed /steer (or busy-mode steer) no longer silently falls back to interrupt+queue
(which cancelled the active stream). _trySteer now returns delivered:bool, restores the
draft + leaves the stream running on failure; send() only clears staged files when
_steerDelivered===true. Backend hunk comment-only (failure paths already returned
{accepted:False}, never touched CANCEL_FLAGS). i18n value-only on pre-existing keys
(locale-parity preserved, 173 locale tests pass). 48 own tests pass.
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: dso2ng <dso2ng@users.noreply.github.com>
In-chat cronjob tool reached cron.jobs whose path constants snapshot at import, so a
chat under a named profile hit the DEFAULT profile's cron dirs. Adds a per-turn
contextvar (_STREAMING_CRON_PROFILE_HOME, reset on every exit) + a tool-call-boundary
wrapper that enters cron_profile_context_for_home() only while the cronjob handler runs
(cron lock, restored immediately — no whole-turn global mutation). Verified: agent
executor's propagate_context_to_thread uses unfiltered contextvars.copy_context() so the
contextvar reaches the sync handler on the ThreadPool worker; reset-token symmetry intact.
Code byte-faithful from PR head c3fd6ab22f8c. 12 own tests pass.
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: roian6 <roian6@users.noreply.github.com>
* stage #4618 (allenliang2022): broaden streaming stale-compressor guard to any model-window mismatch + #4618 regression tests + CHANGELOG
Broadens #3256's default-only live-usage guard: the streaming SSE snapshot now
always resolves the real per-model window via the same helper GET /api/session
hydration uses (_context_length_lookup_inputs_for_model + get_model_context_length)
and corrects whenever it differs from the compressor's cached value, with a
TypeError fallback to the legacy 2-arg form. Fixes 'refresh shows 1M, send reverts
to stale 168k + early auto-compress' on model-switched sessions. Per-stream cache
preserved (one lookup/stream). Code byte-identical to PR head 3beb18e92d73.
Adds 4 source-structure regression tests (RED-proven on master).
Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
* fix#4618 gate findings: profile-scoped config (Codex cross-profile) + #4248 256k acceptance gate (Opus downward-clobber)
Codex SHIP-WITH-FIXES: live-snapshot used ambient get_config() which in the
detached streaming worker resolves the process-global/default profile (#3294) ->
for a non-default profile pinning a different per-model context_length it would
surface the WRONG profile's window. Now resolves via get_config_for_profile_home
on the session's own profile home (mirrors the worker's _cfg resolution).
Opus SHIP-WITH-FIXES: broadened guard aligned resolution w/ hydration but not its
#4248 acceptance gate -> a transient low-confidence 256k metadata probe could
clobber a LARGER cached window mid-stream. Now reuses the exact hydration helper
_should_accept_session_context_length_refresh on both modern + legacy paths.
+ regression tests for both. Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
* fix#4618 Codex re-gate findings: broaden save + SSE-done stale-compressor guards too
Codex re-gate found the broadened live-snapshot guard fixed metering but the two
SIBLING paths still used the old default-only exact-cap test:
- api/streaming.py final session-save: persisted stale other-model window (168k)
to s.context_length -> wrong window on reload.
- api/streaming.py terminal SSE: emitted stale window -> indicator REVERTS
on stream end (messages.js overwrites S.lastUsage) = the exact 'send reverts to
168k' symptom.
Both now resolve the real per-model window via the same hydration helper and honor
the #4248 acceptance gate (no 256k downward-clobber), with legacy 2-arg fallback.
This is the root-cause completion across all 3 paths (live/save/SSE-done).
+ 2 regression tests. Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
* docs(#4618): note model_changed-omission rationale (Opus) + broaden CHANGELOG to all-3-paths
* Release v0.51.561 — Release TT (context-window indicator stays correct after model switch; #4618)
---------
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
Codex+Opus gate caught that the forward-scan-only keep produced [user, _recovered
user, assistant] -> adjacent users -> strict-provider 400 once the anchoring
assistant's prev kept neighbour was a user. Keep a recovered user ONLY when it
separates two assistants (prev_kept==assistant AND next==assistant); mirror in
_api_safe_message_positions. + 3 regression tests.
Opus review note: the substring match in _stale_prefix_matches_prior_user_context
is intentionally loose, but correctness depends on the caller's
suffix-equals-entire-submitted-turn invariant (cleaning can only ever rewrite a
flagged row to the user's actual current turn — never drop legitimate content).
Comment-only; no behavior change.
When the agent's role-sequence repair concatenates the prior context-tail
user row with the submitted current turn as <stale>\n\n<current>, that
polluted pair was persisted into both the model context and the visible
transcript. Add a _detect_stale_user_merge detector (requires the literal
\n\n repair boundary + exact tail/current match after sentinel stripping)
and funnel context-merge and display-merge through one
_strip_stale_user_merge_from_messages so a single rule governs persistence.
The strip is scoped to the new-turn candidate slice only — historical rows
in the already-committed previous_context prefix are never rewritten.
Co-authored-by: starship-s <starship-s@users.noreply.github.com>
The backfill inner loop in _merge_display_messages_after_agent_result
re-ran _message_identity (json.dumps) for every future display row on
every outer iteration and used an O(N) list-slice membership test
(in context_keys[_cursor:]), making it O(D²·C). On long tool-call
sessions this pegged a core, held the GIL, and stalled /api/sessions
5-11s — the WebUI appeared hung. Precompute display keys once and keep
a multiset (count-keyed dict) mirror of context_keys[_cursor:] for O(1)
membership → O(D²).
Salvaged from #4314 (the original PR bundled an unrelated, undescribed
HermesWebUIContext feature). Reimplemented the membership mirror as a
count-keyed dict instead of the PR's set: _message_identity returns
DUPLICATE keys for identical-content turns and None for empty rows, so
a plain set diverges from the original list-slice semantics (verified
over 3M adversarial trials); a multiset including None is exact. Added
a differential regression test guarding the equivalence.
Co-authored-by: psanger <psanger@users.noreply.github.com>
_drain_webui_process_notifications now drops (consumes, never requeues) a
completion whose completed_at is older than a configurable cap
(HERMES_WEBUI_STALE_COMPLETION_MAX_AGE_SECONDS, default 6h; 0 disables).
Fail-open: events without completed_at (legacy agent) are never dropped.
Additive (+192/-0). Closes#4029.
Co-authored-by: allenliang2022
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
* Release PK (v0.51.450): preserve custom provider namespace prefixes in model normalization (#4278)
Token-boundary check in _normalize_provider_id (exact token or prefix
followed strictly by ':' or '/'); explicit aliases for hyphenated
first-party names (google-gemini, google-ai-studio, claude-code); bare-prefix
family detection no longer fires on namespaced model IDs.
Closes#4278. Supersedes #4280 (rodboev) whose '-' boundary re-collapsed
claude-relay->anthropic.
Co-authored-by: b3nw
* gate fix (Codex CORE): add openai-api to _PROVIDER_ALIASES so the :/ token boundary doesn't drop the registered openai-api provider id to '' (would silently repair OpenAI API sessions to the profile default) + 2 regression tests
* gate fix (Opus): gate the streaming send-path bare-prefix loop on '/' not in model so custom namespaces (gemini_cli/, gpt_proxy/, claude-relay/) survive the send path — same #4278 collision Opus found in _apply_profile_provider_context_to_streaming_model; + regression test + CHANGELOG
---------
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Consolidates #4162 + #3944. Aux path: skip the reasoning-disable inject for
routes that 400 on it via _route_rejects_reasoning_extra — hostname-matched
OpenAI / Azure OpenAI / Azure AI Foundry (+ provider aliases openai*/azure*/
azure-foundry/azure-ai*) and OpenRouter Anthropic mandatory-reasoning; keep it
for local/OpenRouter-non-anthropic/other reasoning routes (#4161). Agent path:
rely on the existing agent.reasoning_config={enabled:False} + _build_api_kwargs()
(route-correct per provider profile) rather than re-injecting a generic disable
on top of it; only MiniMax reasoning_split is added (unchanged from master).
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Resolve custom provider API keys from the matched config snapshot and pass them through session hydration plus streaming fallback context-length probes. This prevents authenticated /v1/models endpoints from falling back to the default 256K window and clobbering larger persisted session metadata.
Codex caught a regression: the contributor's _runtime_preferred_base_url preferred
the runtime base_url for ALL non-custom providers, which would clobber an explicit
providers.<id>.base_url override pointing at a different host/port (LM Studio at a
LAN IP, an OpenRouter mirror). Now only prefer the runtime URL when it's the SAME
scheme+host+port as the configured one (the #3895 /v1-dedup case is path-only on
the same host); a genuinely different configured endpoint wins. Added
_same_base_url_endpoint() + 2 regression tests (different-endpoint preserved,
same-endpoint normalization).
Extract _build_partial_message() shared by cancel_stream() and the new
_snapshot_and_append_partial_on_error(); the two provider/exception error paths
in _run_agent_streaming now snapshot accumulated text/reasoning/tool_calls under
STREAMS_LOCK and append a _partial assistant message instead of discarding it.
Closes#3929.
Co-authored-by: b3nw <150195942+b3nw@users.noreply.github.com>
* fix(session): retire stale truncation watermark on new committed turn (#3831)
retry_last / undo_last / the Edit-truncate handler set truncation_watermark
to suppress the *replaced* tail from the append-only state.db merge.
Session.save() deliberately never auto-clears it (#2914), but nothing retired
it when the user then sent a genuinely NEW turn either — so it froze at the old
edit boundary. A frozen watermark then dropped post-watermark state.db rows
whenever the sidecar was later reconstructed empty (recovery/reconcile),
permanently losing the turns sent after the edit (state.db still had them).
Retire a POSITIVE watermark to None once the new user turn is COMMITTED to
session.messages — at the success-merge (3 sites), eager-checkpoint, error/
recovery materialization, and cold-load repair commit points. Not at chat-start:
in deferred mode the new row isn't in messages yet, so a merge in that window
would resurrect the replaced tail (the max-sidecar guard hasn't risen past the
old boundary). Once committed, max_sidecar_timestamp rises past the replaced
tail and the merge suppresses it without the watermark, so retiring is safe.
Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that
must keep blocking all state replay, so the clear is falsy-gated.
Closes#3831
* chore(changelog): clarify watermark-retirement timing to commit-time
Greptile review noted the original phrase "retires the watermark at the
start of a new user turn" was timing-imprecise. The retirement actually
fires when the new turn is durably committed to session.messages —
at the agent-result merge, the eager user-message checkpoint, or the
cold-load recovery commit. Reword for accuracy; semantics unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2)
Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error)
and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save),
which inline the falsy-gated watermark clear instead of calling the tested helper.
The error path is precisely the #3831 failure mode (recovery/reconcile after a
crash), so a dedicated regression test closes that gap. Both assert a positive
watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is
preserved.
---------
Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* stage bg_task trio combined (#2979 superset) on master for deep review
* fix(bg_task): unsubscribe SessionChannel on header-write failure (Codex deep-review catch) + regression test
* test: realign on-subscribe-recovery anchor to subscribe_to_session_channel after leak fix
* CHANGELOG: bg_task trio as v0.51.340 LD (HELD pending independent review)
* bg_task trio: apply 3 independent-review (greptile) fixes
1. start_session_turn now threads the session PROFILE model defaults
(_read_profile_model_config) into the wakeup model-resolve, so a brand-new
session with an empty model falls back to the profile default not global
DEFAULT_MODEL. Updated the white-box spy test signature accordingly.
2. /api/session/stream omits the Connection header (HTTP/1.1 keep-alive
default) to match the #3103 long-lived-SSE pattern.
3. Reaper now prunes _LAST_EMIT_TS for collected sessions so the coalesce
timestamp map can't grow one permanent entry per session forever.
nesquena APPROVED the PR; these are the 3 non-blocking greptile suggestions.
* test: realign _start_session_turn adapter stub lambda to new profile-defaults signature