Two distinct timeout causes, both surfacing as the client's 30s 'Request timed
out' toast with no server-side signal:
A) /api/session/move acquired the per-session agent lock with a bare unbounded
'with _get_session_agent_lock(sid):'. The streaming thread holds that same
lock during checkpoint saves; on slow file I/O (WSL/DrvFs) the move could
block past the client abort. Now acquires with timeout=5 and returns HTTP 503
on contention (lock kept, not dropped, since s.save() still races the writer).
B) /api/projects/delete unlinked every assigned session via get_session()+save()
— O(N) full-messages reserialize. For an actively-streaming session we now
clear project_id on the LIVE CACHED Session object under LOCK (the streaming
thread persists it on its next save — the worker always does a final save at
turn completion) instead of issuing a competing s.save(); falls back to a
direct save when not cached. Non-streaming sessions unchanged.
Also guards the '+ New project and move' shortcut (sessions.js) against the new
503 so it shows a toast instead of an unhandled rejection, keeping the #2551
authoritative refetch in both the success and catch paths.
Adds tests/test_issue3746_session_move_delete_timeout.py (behavioral lock-timeout
test + structural guards for both handlers + the frontend 503 guard). Widened the
#2551 new-project-refetch test's fixed byte-window to a block-scoped search so the
try/catch wrap (which preserves the refetch) doesn't trip a brittle offset assertion.
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
A bare 'custom' provider with a remote base_url is a vendor-routing proxy
(LiteLLM, Bedrock gateway, etc.). A slashed model id like bedrock/opus-4-6 is
intrinsic — the proxy routes on the full string. The prior heuristic stripped
ANY known-provider prefix (bedrock is also a provider name), truncating it to
opus-4-6 and causing 403 'model not allowed'.
Fix: in the custom-base_url branch, strip the prefix only when (a) the configured
provider is a real first-party provider pointed at an OpenAI-compatible proxy
(provider=openai + proxy base_url — the #433 path), OR (b) for a bare 'custom'
provider, the bare id is genuinely a first-party model of that prefix's catalog
(openai/gpt-5.4 -> gpt-5.4, since gpt-5.4 IS an OpenAI model). An intrinsic
routing prefix whose bare id is NOT first-party of that namespace is preserved
(bedrock/opus-4-6 stays whole). Unknown prefixes (zai-org/...) preserved as before.
Both prior behaviors work in tandem with the fix:
- #433 sprint40: bare custom + openai/gpt-5.4 -> gpt-5.4 (redundant, strip)
- #433 model_resolver: provider=openai + google/gemma -> gemma (proxy strip)
- #548: custom + zai-org/GLM-5.1 -> preserved (unknown prefix)
- #1625: custom + loopback base_url -> preserved (local server)
- #3872: custom + bedrock/opus-4-6 -> preserved (intrinsic vendor prefix)
Adds regression tests covering all of the above.
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: haolf000 <haolf000@users.noreply.github.com>
Absorbs contributor PR #3809 (@b3nw), rebased onto fresh master (was ~20 behind,
panels.js conflict resolved by merging the new !isNoAgent skill-tags guard with
the model-select call).
Adds a Model Override dropdown to the Tasks scheduled-jobs create/edit form,
populated from /api/models grouped by provider, persisting model+provider,
clearable to default, disabled in no-agent mode. Surfaces hermes-agent's existing
per-job model override (CLI parity).
greptile P1s (override cleared on fast-save / on API failure) verified
ALREADY-FIXED in PR head; also applied an Opus UX hardening (keep the model
select disabled on a failed /api/models load so the user can't think they
cleared the override). UX approved by Nathan via screenshots.
Pre-merge fixes:
- i18n: the PR added the 3 cron_model_* keys to all locales but left 10 of them
as 'TODO: translate' English stubs (only es was done), tripping
test_zh_hant_locale. Provided real translations for de/zh/zh-Hant/ru/ja/fr/pl/
it/pt/tr.
- test isolation: #3809's new test file shifts pytest-shard composition so
test_issue2863's background-rebuild test ran after a test that leaves the
#3884 _SESSION_INDEX_REBUILD_THREAD globals populated, suppressing the fresh
thread it asserts on. Made that test hermetic (joins+clears the rebuild-thread
globals up front) so it passes regardless of shard run order.
Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: b3nw <b3nw@users.noreply.github.com>
* Release v0.51.344 — Release LH (sidebar fork-lineage grouping #3799/#3884)
Absorbs #3884 (@rodboev): manual forks are kept as sidebar lineage boundaries
so a forked session isn't collapsed under a compression-continuation root,
while enriched child-session rows stay independently visible until the later
attachment pass. Also addresses the greptile TOCTOU flag: the background
index-rebuild thread now pins + re-checks its (SESSION_DIR, SESSION_INDEX_FILE)
target under _SESSION_INDEX_REBUILD_LOCK before writing.
Rebased onto fresh master, content byte-identical to PR head, full-suite +
Codex + Opus gated.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* fix(models): propagate target kwargs in index-rebuild fallback (Opus SHOULD-FIX)
Opus advisor stage-344: the _write_session_index fast-path fallback recursed
with _write_session_index(updates=None) and no kwargs, falling back to the
global SESSION_DIR. Safe today (the only kwargs-caller passes updates=None and
never reaches the fast path) but the invariant was implicit. Propagate the
resolved session_dir/session_index_file so a target-scoped rebuild falls back
to that same target.
---------
Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Phase-1 low-risk batch, each rebased onto fresh master + gated fresh:
- #3883 (@rodboev, #3740): sidebar refreshes a stale message_count:0 index row
from its sidecar when user_message_count>0 + sidecar mtime newer than index,
self-healing the interrupted-stream stale-count case beyond compression lineage.
- #3878 (@rodboev, #3833): manual workspace refresh clears the dir cache and
re-fetches expanded descendants so background-written files become visible.
- #3880 (@koshikai): translate the 11 remaining English strings in the ja locale.
greptile flags evaluated: #3878 P1 relative-path + P2 stale-comment already fixed
in PR head; #3883 P2 missing-snapshot-test already covered by the PR's own
test_all_sessions_refreshes_stale_zero_count_snapshot_row_from_sidecar, P2
double-stat is a bounded cheap micro-opt (FOLD); #3880 'needs screenshots'
rejected (in-place translation of existing keys, no UI shape change).
Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Co-authored-by: koshikai <koshikai@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>
* fix(sidebar): prime idx_messages_session before CLI-session scan (#3887)
The sidebar's CLI-session scan (read_importable_agent_session_rows) orders
candidate sessions by a correlated MAX(timestamp) subquery over messages,
which collapses to a full per-session table scan when the agent's standard
idx_messages_session ON messages(session_id, timestamp) index is absent.
A normally-migrated hermes-agent state.db has it; a db that lost its
migrations (older hermes-agent, or a hand-rebuilt/reimported db) does not,
and /api/sessions then stalls for seconds on every refresh (the 5s cache
TTL never settles -> repeating 'Slow WebUI request still running' warnings).
Prime the index with CREATE INDEX IF NOT EXISTS before the scan: a no-op
when it already exists, a ~20ms self-heal otherwise (measured 13.3s ->
0.009s on a no-index 8k-session db). Best-effort: degrades silently on a
read-only db, a locked db, or a minimal schema without a timestamp column.
Closes#3887
* test(#3887): skip read-only-db test under root (greptile P2)
Root bypasses POSIX permission bits, so chmod 0444 doesn't make the file
read-only for root — the prime would succeed and the test would validate the
wrong path, giving false confidence on root-run CI. Skip under root; the
production handler's except sqlite3.Error: pass covers the read-only/locked/
corrupted/older-schema cases regardless.
---------
Co-authored-by: nesquena-hermes <[email protected]>
* 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
* feat(composer): add saved prompts library with per-profile storage (#2732)
* fix(composer): move saved-prompts popup out of .composer-left to preserve DOM test (#2732)
* fix(composer): correct ARIA roles, add server-side prompt limits (#2732)
* fix(composer): surface save-prompt errors instead of silent success toast (#2732)
* Release v0.51.338 — Release LB (saved prompts library, #3571)
Composer saved-prompts library (@rodboev): bookmark button → popup of saved
prompts; click to insert, save current input, delete. Persists to
$HERMES_HOME/webui/saved_prompts.json with server-side caps (8000 chars / 200).
Maintainer work (per Nathan): conditions were (a) verify it actually works and
(b) hide on mobile. Both met:
- Live-verified load/save/delete all persist through the UI.
- Added mobile-hide (#btnSavedPrompts,.saved-prompts-popup display:none in the
@media max-width:640px + 900px composer blocks). DOM-verified visible at 1280px,
vision-confirmed absent from the composer at 390px.
- Added missing Polish (pl) i18n for the 5 saved_prompts_* keys (PR had en+others
but not pl — failed locale-parity).
- Added tests/test_issue3571_saved_prompts.py (mobile-hide + caps + wiring guards).
Full suite green, ESLint/scope-undef CLEAN, Opus SHIP-safe (auth-gated, CSRF,
XSS-safe, sane caps), Codex SAFE-TO-SHIP.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
---------
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* fix(streaming): make per-token inline-thinking extraction linear (#3633 follow-up)
Codex post-merge perf catch on #3633: _parseStreamState() and
syncInflightAssistantMessage() call _extractInlineThinkingFromContent on the
FULL accumulated assistantText on EVERY streamed token. The #3633 rewrite made
that a full char-by-char walk, so cost was O(n^2) over a stream — a Node harness
measured ~88s (no-tag) / ~103s (leading <think> block) for 2000x100-char tokens,
which would freeze the main thread on long reasoning-model responses.
Two fixes (Python api/streaming.py + JS static/messages.js twin, line-for-line parity):
1. Fast path: if the text contains no complete thinking opener AND (when
streaming) its tail is not a prefix of an opener, return unchanged without the
char walk — two cheap substring scans. Handles the common no-tag case.
2. Bulk-skip plain trailing content: track the next complete opener via
str.find/indexOf (_next_inline_thinking_opener / _nextThinkingOpener); once no
opener remains ahead, append the remainder and stop instead of walking it
(streaming still suppresses a trailing partial-opener prefix). Handles the
leading-block-then-long-answer case.
Result: ~88s/103s → ~0.5s/0.9s (Python), ~0.18s/0.21s (JS). All behavioral cases
(persist + streaming, code-awareness, position-aware unclosed, leading whitespace)
verified unchanged in both twins. Added a per-token streaming perf regression test
and wired _nextThinkingOpener into the node driver harness.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* fix(streaming): perf bulk-skip must respect code context for partial-opener tails
Codex catch on the perf fix: the no-complete-opener bulk-skip suppressed a
trailing partial opener (e.g. '<thi') unconditionally during streaming, but a
partial opener INSIDE inline-backtick / fenced / indented code must stay visible
(master parity). Now, when streaming and the tail is a partial opener, fall
through to the code-aware char walk (bounded — a partial tail is a transient
single token) instead of bulk-skipping; only a PLAIN-text partial opener is
suppressed as a forming block. Added _text_tail_is_partial_opener /
_textTailIsPartialOpener (Python + JS parity) + regression tests for the
inside-code vs plain partial-tail cases.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* docs(changelog): v0.51.336 KZ — inline-thinking streaming perf fix
---------
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Co-authored-by: Hermes Agent <hermes-agent@nesquena-hermes.local>
* fix(streaming): normalize inline thinking extraction across live and persisted turns (#3599)
# Conflicts:
# api/streaming.py
# static/messages.js
# static/ui.js
* fix(streaming): code-aware inline-thinking extraction + position-aware unclosed handling
Codex deep-review caught two regressions in the leading-only -> full-scan
rewrite (both silent data-mangling on the persist/reload path):
1. Code-span unawareness: the scanner only protected triple fences, so a
literal <think> in an inline single-backtick code span or an indented
(>=4-space/tab) code block got silently extracted into reasoning. Added
_inline_thinking_indented_code_at + inline-backtick tracking (Python +
the JS twin _thinkingIndentedCodeAt), so all three code contexts now keep
thinking tags visible.
2. Unclosed-tag truncation: any unmatched open tag moved the trailing prose
into reasoning. Now position-aware — a LEADING unclosed block (cut off
mid-thought) is still reasoning (#3455 intent), but an unclosed tag AFTER
visible content stays visible so literal typed tags don't truncate prose.
Gated partial handling on the previously-unused options.streaming param
(live streaming keeps 'still thinking' behavior; persist/reload does not).
Updated 2 tests that pinned the buggy behavior + added 4 regression tests
(inline-backtick, indented-code, mid-body-unclosed-visible, leading-unclosed-
extracted). Updated the node driver harness to include the new helper.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* fix(streaming): recognize fenced code blocks indented 1-3 spaces
Codex round-3: a fence indented 1-3 spaces is valid Markdown but the fence
detector only matched at column 0, so a literal think tag inside such a fence
(not 4+-space indented code either) was still extracted. Both detectors
(_inline_thinking_fence_marker_at / _thinkingFenceMarkerAt) now walk back over
up to 3 leading spaces to a line start. Added backtick + tilde indented-fence
regression tests.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* fix(streaming): O(n) inline-thinking scan + merge separate reasoning on reload
Round-4 Codex deep-review caught two real issues in my own fixes:
1. PERF (O(n^2)): the indented-code check (_inline_thinking_indented_code_at /
_thinkingIndentedCodeAt) scanned to line boundaries at EVERY character index,
plus the leading check sliced+stripped the whole prefix per unclosed tag. On
long no-newline content this was quadratic (~8.4s @ 200k, called repeatedly
on the streaming path). Replaced with incremental O(1)-per-iteration line
state (_line_is_indented_code / _lineIsIndentedCode evaluated only at line
starts) + a seen_nonspace flag. 200k now extracts in ~55-140ms.
2. RELOAD reasoning-drop: renderMessages() seeded the shared extractor with ''
so a message with BOTH an inline <think> block AND a separate m.reasoning
payload showed only the inline part — the separate payload was dropped
because the !thinkingText worklog resolution was then skipped. Now seeds with
the message's direct reasoning (m.reasoning_content||m.reasoning||...) so the
two MERGE (deduped); separate-only reasoning is preserved without promoting
it into visible prose.
Python + JS twins kept line-for-line parity. Added merge + perf + reload
regression tests; updated the reload structure test and the node driver harness
for the renamed helper.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* fix(streaming): revert reload reasoning-seed; keep O(n) perf fix
Codex round-4 finding #2 (seed renderMessages' inline extractor with
m.reasoning so a separate payload merges) turned out to VIOLATE a deliberate
architectural invariant pinned by test_issue2565 +
test_sprint42: the reload content-extraction path must NOT touch
m.reasoning/m.reasoning_content — reasoning metadata is owned exclusively by
the Worklog Thinking Card path (_worklogReasoningTextFromMessage /
_assistantReasoningPayloadText), never conflated with inline-content
extraction (which would risk promoting provider reasoning into final-answer
prose). Reverted the ui.js seed to the PR's original `thinkingText` arg.
The inline+separate merge is still a genuine extractor capability (exercised
by the live streaming path via liveReasoningText) and is covered by a unit
test, just not invoked from the reload render path by design.
The O(n) perf fix (finding #1) and the code-awareness + position-aware
unclosed handling (rounds 1-3) are all retained.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* fix(streaming): only lstrip extracted content when a leading block was removed
Codex round-5 catch: the extractor unconditionally lstripped the final content
(.lstrip() / .replace(/^\s+/,'')) even when NO thinking block was extracted, so
an assistant reply that legitimately starts with an indented code block or blank
lines lost its leading whitespace on live display, reload, and persistence. This
was a real regression vs master (master returned non-thinking content unchanged).
Now track leading_removed (set only when a LEADING thinking block/prefix is
actually extracted) and lstrip only in that case. Mid-body / no-thinking content
keeps its exact leading whitespace. Python + JS twins kept in parity; added
backend regression tests (indented-first preserved, leading-blank preserved,
leading-think still strips).
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* fix(streaming): reconnect restore prefers raw inflight accumulator
Codex round-6 CORE catch: on reconnect, the single-live-message restore used
(_liveInflightAssistant.content || ''). Because the PR now splits a leading
unclosed <think> into empty content, restoring from the split content dropped
the open tag — so a later </think> token leaked into the visible reply and
corrupted the live accumulator. Restore from
(_fullInflightAssistant || _liveInflightAssistant.content || '') so the raw
open tag survives reconnect and the accumulator stays correct. Added a
reconnect-restore regression test.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* Release v0.51.335 — Release KY (normalize inline thinking extraction, #3633)
Unify inline-thinking (<think>/<|channel>/<|turn|>) extraction across live,
reload, and persisted turns (#3599/#3633, @rodboev). Deep-reviewed: Opus +
6 Codex rounds; maintainer fixes resolved every Codex finding — code-awareness
(inline-backtick/indented/1-3-space fences keep literal tags visible),
position-aware unclosed handling, O(n) line scanning (was O(n^2) on long
content), conditional lstrip (preserve leading whitespace when no leading block
removed), and a reconnect-restore CORE fix (raw accumulator preferred so an open
<think> tag survives reconnect). Python + JS twins in parity. Full suite 8330,
Opus SHIP-SAFE, Codex SAFE-TO-SHIP, ESLint/scope-undef/ruff clean.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
---------
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: Hermes Agent <hermes-agent@nesquena-hermes.local>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Backfill docstrings for api/oauth.py + api/kanban_bridge.py (51 functions, verified accurate to current behavior). Conflicted files dropped for follow-up. Docstring-only, no behavior change. Full suite 8275, CI 11/11. Co-authored-by: camr <camr@users.noreply.github.com>
#3814: O(n) index-count map for /api/sessions. #3815: skip backup-less sidecars on startup recovery. Full suite 8275, Codex SAFE, Opus SHIP, CI 11/11. Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
Phase-3-light. #3790 (@ai-ag2026): expand cold-load transcript window to ~msg_limit renderable rows so tool-heavy sessions don't open showing 1-2 messages. Codex CORE fix: explicit expand_renderable flag (cold-load only; Load-earlier keeps raw cap). Also fixed a recurring CI timing flake (git-parallel test → deterministic Barrier). Full suite 8228, Codex SAFE, Opus SHIP, CI 11/11. Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
Phase-3-LOW backend security fix. #3727 (@rodboev, #1909): enforced CSP now honors the same connect-src as report-only (built from one shared template). Full suite 8205 passed, Codex SAFE (live-verified headers), Opus SHIP. Co-authored-by: rodboev <rodboev@users.noreply.github.com>
list_profiles_api() builds rows from cheap upstream helpers and skips find_alias_for_profile (which reads ~2.4GB of binaries from ~/.local/bin per dropdown open), with a 4s TTL cache + graceful fallback. 4700ms->250ms cold, 0ms cached. Full suite 8183 passed, Codex SAFE, Opus SHIP-IT. Supersedes #3772.
Fix-ourselves pickup of #3774 (@bambalados). _purge_agent_pycache() before os.execv() in _schedule_restart() so the re-exec'd process recompiles freshly-pulled source — fixes AttributeError on first chat after self-update. Full suite 8180 passed, Codex SAFE, Opus SHIP-IT. Co-authored-by: bambalados <bambalados@users.noreply.github.com>
* fix(security): gate /api/onboarding/complete on the local-network check (#3765)
Sibling-path gap surfaced by the #3758 release gate. /api/onboarding/oauth/start,
/setup, and /probe are gated by _onboarding_gate_allows(), but
/api/onboarding/complete was not — it called complete_onboarding() unconditionally
(persists onboarding_completed=True, which hides the first-run wizard). On a
passwordless public bind, an unauthenticated no-Origin POST passes generic CSRF
and could flip the wizard off.
Pre-existing (the endpoint was ungated before #3758 too; #3758 only refactored the
three already-gated siblings). Low severity — it toggles a UI flag, not credentials
or access — but the inconsistency is a real hole, so close it the same way as its
siblings.
- Gate /api/onboarding/complete with _onboarding_gate_allows() → 403 when denied.
- Regression tests: public client (no forwarded headers) → 403 + complete_onboarding
NOT called; loopback client → 200; auth-enabled → 200.
- Mark the legacy _is_local_from_handler mirror in test_onboarding_network.py as a
STALE pre-#3758 contract (it trusts unauthenticated XFF); the authoritative
trust-matrix tests live in test_security_review_fixes.py. Migrating the mirror to
delegate to the real helper is tracked as follow-up test debt, out of scope here.
* docs(changelog): stamp v0.51.308 — Release JX (#3765 onboarding-complete sibling-consistency gate)
---------
Co-authored-by: nesquena-hermes <[email protected]>
* fix(security): ignore spoofable forwarded IPs in onboarding gate + make update-check CSRF-safe (#3758, partial)
Ships the two unambiguous slices of #3758's security review. The two slices with
breakage risk for existing installs — the Docker-default public-bind-requires-auth
gate and removing /tmp from the /api/media allowed roots — are held for separate
review/decision.
Onboarding forwarded-IP spoof hardening (+ release-gate CORE fix):
- The unauthenticated first-run onboarding local-network gate now IGNORES
X-Forwarded-For / X-Real-IP by default (a direct client can spoof them to a
private/loopback address to bypass the gate), trusting them only when
HERMES_WEBUI_TRUST_FORWARDED_FOR=1 is set behind a trusted proxy (rightmost
proxy-appended hop).
- Release-gate (Codex) CORE catch + refinement: when forwarded headers are
present but untrusted, the header is ignored and locality is judged by the raw
socket — but a PRIVATE/LAN raw socket (a separate proxy box that could forward
an arbitrary public client) is no longer treated as local; only a LOOPBACK raw
socket is (genuine same-host; a remote attacker can't forge a 127.0.0.1 TCP
source). This closes the new fail-open the initial refactor introduced (public
client behind a LAN proxy read as local) while preserving genuine same-host
onboarding. LAN-proxy operators must set HERMES_WEBUI_TRUST_FORWARDED_FOR=1.
Regression tests lock the full matrix (spoof-block, LAN-proxy-deny,
loopback-allow, trusted-proxy-rightmost-hop, direct-public-deny).
- Three duplicated inline gate blocks unified into _onboarding_gate_allows /
_onboarding_request_is_local; ONBOARDING_OPEN normalized to canonical truthy
values via _truthy_env.
Update-check CSRF hardening:
- GET /api/updates/check is cache-only (cached_update_status(): no network/git
mutation); forced refresh moves to POST /api/updates/check {force:true}; both
frontend call sites updated and the test_api_timeout contract assertion updated.
- cached_update_status() preserves cached agent info when include_agent re-enabled.
Docker log masking: ENV_OBFUSCATE_PART also masks PASSWORD/SECRET/CREDENTIAL/COOKIE/SESSION.
Held for separate review (NOT in this PR): public-bind-requires-auth startup gate
(server.py + Dockerfile default) and the /api/media /tmp-root removal.
Co-authored-by: fantasticsquirrel <[email protected]>
* docs(changelog): stamp v0.51.307 — Release JW (stage-a3 #3758 partial)
---------
Co-authored-by: nesquena-hermes <[email protected]>
* fix(sessions): resolve branchy compression lineage to the freshest tip (#3751)
Compression parents can have multiple continuation-looking children when a stale
segment is resumed after a newer compressed branch already exists. The previous
projection followed the newest DIRECT child only, so it could hide the deeper
branch with the latest real activity and make a conversation look missing/stale
after compaction or session rotation.
- compression_tip() now walks ALL reachable continuation descendants (DFS) and
selects the freshest importable (messageful) tip by (last_activity, depth).
- read_session_lineage_metadata() expands descendants from the materialized
ancestors via the parent index (scoped, 20-hop cap), pulls per-session message
stats from the messages table, and exposes a canonical _lineage_tip_id so the
WebUI sidebar collapse picks the same tip as the projection.
Backward-compat hardening (two release-gate Codex findings, both fixed + tested):
The new message-stats / tip-scoring code must not raise on older/minimal or
non-standard state.db schemas, which previously collapsed the whole projection:
- messages table with NO `timestamp` column → MAX(timestamp) raised in SQL →
read_session_lineage_metadata returned {} (lost all lineage metadata).
- ISO-8601 TEXT messages.timestamp → float()/raw comparison raised TypeError;
in read_importable_agent_session_rows that propagated through compression_tip
and get_cli_sessions() swallowed it, hiding ALL imported agent rows.
Fixes:
- PRAGMA table_info(messages) detection: require session_id, only SELECT
MAX(timestamp) when present (else NULL + COUNT only); fall back to message_count.
- new _as_score() helper (first numerically-coercible value, else next
candidate e.g. started_at) used at EVERY tip-scoring / sort site in both
compression_tip() and freshest_continuation_tip(), plus the projection sort.
- regression tests: lineage metadata survives REAL/absent/TEXT messages.timestamp,
and read_importable_agent_session_rows survives a TEXT timestamp (no empty hide).
Co-authored-by: ai-ag2026 <[email protected]>
* docs(changelog): stamp v0.51.306 — Release JV (stage-a2 #3751)
---------
Co-authored-by: nesquena-hermes <[email protected]>
* refactor(sessions): add dormant JSON-backed SessionDB adapter (#3720, #3383)
First, lowest-risk slice of the unified-session-db migration: a SessionDB-shaped
adapter over the existing WebUI JSON store, behind a dormant experimental flag
(experimental.unified_session_db, default false). No runtime call site is rewired
— is_unified_session_db_enabled() has no live callers, so persistence behavior is
unchanged until a later migration PR opts in. Includes the adapter, the dormant
config flag + _apply_config_defaults wiring, an architecture doc, and adapter tests.
Also adds docs/architecture/ to the .gitignore docs allowlist (the docs/* rule
excludes subdirectories; the new architecture doc and its presence test would
otherwise be silently dropped on a clean checkout).
Co-authored-by: rodboev <[email protected]>
* docs(changelog): stamp v0.51.305 — Release JU (stage-p2b #3720)
---------
Co-authored-by: nesquena-hermes <[email protected]>
* fix(terminal): reap reparented terminal descendants by process group (#3725, #2577)
Embedded-terminal descendants reparented to the WebUI process could linger as
zombies. The reaper now calls os.waitpid(-terminal_pgid, WNOHANG) scoped to the
terminal's own process group (terminals spawn with start_new_session=True, so
proc.pid == pgid) rather than process-wide waitpid(-1), which would otherwise
reap unrelated WebUI subprocess children and silently coerce their exit codes to
0. Bounded by a 64-iteration limit and lock-guarded. Runs on reader cleanup and
terminal close.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* docs(docker): add opt-in GPU runtime image path (#3721, #3243)
The default image stays CPU-only. A new INSTALL_GPU_LIBS=1 build arg installs
VA-API user-space libraries for users passing through host GPU devices, and
docker_init.bash preserves Docker --group-add supplemental groups (e.g. render/
video for /dev/dri) when dropping privileges to the runtime user. Default
(INSTALL_GPU_LIBS=0) is a no-op. Docs + regression test included.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* docs(changelog): stamp v0.51.304 — Release JT (stage-p2a #3725#3721)
---------
Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* fix(cron): toggle run output rows instead of re-fetching when already open (#3732)
_loadRunContent() only ever expanded, so clicking an already-open cron run row
re-fetched its content pointlessly. It now toggles: an open row collapses (clears
the expansion state + resets the toggle button) and returns early, avoiding the
redundant API call.
Co-authored-by: mysoul12138 <mysoul12138@users.noreply.github.com>
* feat(config): expand ${VAR} references in config.yaml at load time (#3736)
hermes-agent already expands ${ENV_VAR} in config.yaml, but the WebUI's own
loader stored the raw dict, leaving literal ${...} strings. Recursively expand
${VAR} against os.environ on both config load paths (reload_config and
_load_yaml_config_file); unset vars are left untouched (${VAR} preserved).
Co-authored-by: Carry00 <Carry00@users.noreply.github.com>
* fix(security): anchor untracked-file deletes in git_discard (#3702)
git_discard(delete_untracked=True) used raw shutil.rmtree / Path.unlink after a
separate safe_resolve_ws validation, leaving a validation-to-use symlink-swap
window. Route untracked deletes through the anchored helpers (rmtree_anchored /
unlink_anchored) so a swapped path component is rejected at delete time; preserve
the prior missing_ok tolerance for benign concurrent-removal races. Adds
regression coverage for both the symlink-swap block and the concurrent-missing case.
Co-authored-by: Hinotoi-agent <Hinotoi-agent@users.noreply.github.com>
* docs(changelog): stamp v0.51.303 — Release JS (stage-p1a #3732#3736#3702)
---------
Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: mysoul12138 <mysoul12138@users.noreply.github.com>
Co-authored-by: Carry00 <Carry00@users.noreply.github.com>
Co-authored-by: Hinotoi-agent <Hinotoi-agent@users.noreply.github.com>
* fix(ui): stop hidden toast from intercepting clicks on mobile (#3735)
The .toast container kept pointer-events:auto while hidden (opacity:0), so its
fixed padding sat over mobile profile action buttons and ate their clicks. Set
pointer-events:none when hidden; restore auto on .toast.show.
Co-authored-by: timlawrenz <timlawrenz@users.noreply.github.com>
* fix(sessions): rename saves on blur so iOS Safari rename works (#3729)
iOS Safari has no Enter key; the keyboard 'Done' button fires blur, and the old
onblur=cancel discarded the rename. Flip blur to save (Escape still cancels) for
session rename and project create/rename, with a _finishDone guard to prevent a
double-fire between blur and the API callback.
Co-authored-by: reinocheong <reinocheong@users.noreply.github.com>
* perf(session): skip fuzzy dedup matching for giant merge payloads (#3730)
Large tool/log payloads made _matching_visible_duplicate() casefold+regex-tokenize
multi-megabyte contents on every visible key, so /api/session took 10s+ and blocked
/api/sessions for ~19s. Keep loose normalization lazy+cached and skip substring/fuzzy
matching for non-exact payloads >200KB; exact visible-key matches still short-circuit.
Co-authored-by: alvistar <alvistar@users.noreply.github.com>
* docs(changelog): stamp v0.51.302 — Release JR (stage-brick brick/perf hotfixes #3735#3729#3730)
---------
Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: timlawrenz <timlawrenz@users.noreply.github.com>
Co-authored-by: reinocheong <reinocheong@users.noreply.github.com>
Co-authored-by: alvistar <alvistar@users.noreply.github.com>
@rodboev. providers.<name>.models.<model>.context_length overrides (standard provider,
no base_url) were invisible to the session context resolver → wrong window shown/persisted,
could trip auto-compression at the wrong threshold. New _context_length_lookup_inputs_for_model
helper resolves provider config / base_url / custom_providers across route-load, session-save,
and SSE-usage paths; provider-scoped overrides match by provider name and forward as
config_context_length (returned before any base-url-gated probe).
Maintainer pre-merge items both already satisfied in PR head: no-base_url regression test
(test_route_resolver_uses_provider_model_context_length_without_base_url) present; session-save
_cfg_base_url assigned before the helper call (safe-bound, no NameError on TypeError fallback).
Verified api code byte-identical to PR head; 14 context-length tests pass. + CHANGELOG v0.51.300.
Co-authored-by: nesquena-hermes <[email protected]>
* fix(#3718): /api/models/live probes upstream for custom providers with model config (#3719)
@DanielMaly. Config model IDs were added to the ids list before the 'if not ids:' guard,
so a custom provider with a model: field skipped the live /v1/models probe and Settings'
refresh returned only the config entry. Now collects config IDs separately, always probes
for custom providers, merges live (priority) + config (fallback). Includes the maintainer
review follow-ups (CUSTOM_MODELS_ENDPOINT_TIMEOUT_SECONDS constant + behavioral tests).
Captured all 3 logical PR commits' net effect; routes.py + test verified byte-identical
to the PR head. + CHANGELOG v0.51.298.
* test(#3718): remove unused BytesIO import (ruff F401)
* test(#3719): update timeout assertion to CUSTOM_MODELS_ENDPOINT_TIMEOUT_SECONDS
The #3719 maintainer-review commit replaced the hardcoded urlopen timeout=8 with the
CUSTOM_MODELS_ENDPOINT_TIMEOUT_SECONDS constant (5.0). test_named_custom_live_fetch_uses_matching_entry_endpoint
asserted the old literal 8. Reference the constant directly now so the assertion can't
drift again. Not a behavior change — only the live-probe timeout value (8s -> 5s) moved,
URL + auth unchanged.
---------
Co-authored-by: nesquena-hermes <[email protected]>
* fix(terminal): guard embedded terminal on remote backends (#3673)
* fix(terminal): add missing remote-backend locale key
* fix(terminal): add missing remote-backend locale coverage (#3673)
* docs(changelog): v0.51.297 — terminal remote-backend guard (#3711) only
Dropped #3725 (descendant reaper) from this stage: Codex caught a SILENT exit-code
clobber — its process-wide os.waitpid(-1, WNOHANG) can reap a sibling WebUI child that
another subsystem is waiting on, coercing that child's returncode to 0 (failures become
successes). Held for the contributor to scope the reaper to terminal PGIDs
(os.waitpid(-term.proc.pid, WNOHANG)) or a terminal-PGID registry.
---------
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <[email protected]>
* fix: honor explicit model pick, suppress silent revert on cross-family selection (#3737)
When a user changes the model in the composer dropdown and sends,
_resolve_compatible_session_model_state previously had no way to
distinguish an explicit user pick from stale session state. The
profile-aware branch (v0.51.290, PR #3448) and the legacy block
both rewrote bare cross-family models to the profile default, and
the client unconditionally applied effective_model — silently
discarding the user's choice.
Backend: accept explicit_model_pick flag (default False) on
_resolve_compatible_session_model_state. Guard both the
profile-aware branch (routes.py:2024) and the legacy block
(routes.py:2124) to skip cross-provider normalization when set.
_handle_chat_start extracts the flag and passes it through.
Frontend: consult _readPendingSessionModel (sessionStorage, 10-min
window) to detect explicit picks and include the flag. Add a toast
as defense-in-depth when the server still returns effective_model.
Closes#3737
* fix: tighten explicit-pick detection and add regression tests (#3737)
Greptile P2-1: compare model_provider in pending pick detection,
not just model name, to avoid false-positive flag when the
session provider changes between pick and send.
Greptile P2-2: only show the defense-in-depth toast when an
explicit pick was actually overridden — stale-session
normalizations are expected behavior and should be silent.
Add two regression tests for the profile-branch guard:
- explicit_model_pick=True → cross-family model survives
- explicit_model_pick=False → existing normalization preserved
* revert(sidebar): remove manual session status labels (#3570)
The manual per-session status labels (Todo / In Progress / Done) added in
v0.51.284 (#3570) stored state only in browser localStorage keyed by session
id, with no server-side backing — so labels did not persist across browsers
or devices (a user who labeled sessions on one machine saw none after moving
to a laptop). They also rendered as three flat top-level entries in the
session context menu, crowding the root menu.
Per maintainer decision, remove the feature entirely for now. It can be
reintroduced later with proper server-side persistence and a less intrusive
menu treatment.
Removes:
- JS state/cycle helpers + SESSION_MANUAL_STATUS_KEY (static/sessions.js)
- context-menu status entries + sidebar status badge render
- .session-manual-status* CSS (static/style.css)
- session_status_* locale strings across all locales (static/i18n.js)
Full suite: 8084 passed, 0 failed. ESLint runtime gate: clean.
reverts #3570
* fix(#3737): keep explicit-pick marker until send consumes it (Codex catch)
Codex found the explicit_model_pick flag never engaged in the normal flow: boot.js
modelSelect.onchange cleared the pending-pick marker right after /api/session/update,
so by the time send() ran _readPendingSessionModel returned null, _explicitPick was
false, and the server's profile-provider branch still reverted the cross-family pick
(the exact #3737 bug). The flag only worked in the rare race where send beat the
session-update round-trip.
Fix (Codex prescription): do NOT clear the marker in onchange; clear it in send()
immediately after reading a matching pending pick, so it's consumed for that send only.
onchange still RECORDS the pick (_rememberPendingSessionModel) — only the premature
clear is removed.
* test(#3737): lock client clear-timing wiring (onchange records, send consumes)
Static source guards for the Codex clear-timing fix: onchange must record the
pending pick and NOT clear it post-session-update; send() must consume (clear) it
only after reading a matching _explicitPick, and send the flag only when truthy.
Complements the author's resolver-level tests in test_provider_mismatch.py.
* test(#3737): realign refresh-persistence test to the moved pending-pick clear
The Codex clear-timing fix moved the pending-pick clear out of modelSelect.onchange
into send() (consume-on-send). test_model_selection_records_pending_state_before_async_session_update
asserted the OLD onchange-clears behavior (assert _clearPendingSessionModel in body).
Updated to assert the NEW correct behavior (onchange must NOT clear it — it survives to
send). The test's core refresh-survives invariant (marker recorded before the async
session-update; reapplied on load) is unchanged and still passes; only the stale
clear-location assertion is flipped. Not a regression-blessing: the refresh-survives
feature is intact, the marker lifecycle is more correct.
---------
Co-authored-by: John Doe <johndoe@example.com>
Co-authored-by: nesquena-hermes <[email protected]>
* Harden interrupted recovery control filtering
* Redesign live-to-final assistant replies
* Fix live activity anchor test fixture
* Fix CI lint issues for live reply tests
* Strengthen live progress prompt contract
* Recover PR #3401 refresh on origin/master
* Repair live-to-final refresh regressions
* Fix live worklog refresh regressions
* Show live footer timer on initial stream start
* Restore live stream shell after reload
* Preserve per-frame live SSE replay cursors
* Preserve reasoning as Worklog Thinking cards
* Quiet Worklog Thinking card styling
* Align Worklog Thinking card styling
* Scope live Worklog Thinking cards by segment
* Suppress exact duplicate settled Thinking
* Close#3401 merge review test gaps
* fix(#3401): resolve 4 deep-review regressions (inline-think, reconnect-dup, neon skin, busy-gate worklog)
Deep review (Codex diff-vs-master + live-browser drive) of the live-to-final refactor
surfaced 4 regressions vs master that the rewritten suite no longer guarded:
1. Inline <think>…</think>answer reasoning vanished — _assistantReasoningPayloadText
used $-anchored regexes so a leading think block + visible answer extracted nothing
and the Thinking card never rendered. Removed the 3 $ anchors to match the
(non-anchored) display stripper. Live: inline-think thinking-only turn now renders.
2. (CORE) reconnect/reload duplicated the live reply — _rememberRunJournalCursor advanced
a closure-local seq but never wrote INFLIGHT[activeSid].lastRunJournalSeq, so a reload
replayed the journal from after_seq=0 over restored lastAssistantText. Now mirrors the
cursor onto INFLIGHT + schedules a throttled persist.
3. Neon skin silently broke — PR deleted the :root[data-skin="neon"] CSS but left Neon in
the picker. Restored the neon CSS block from master.
4. Settled tool-worklog rebuild gated purely on !S.busy — dropped every prior settled
turn's worklog when renderMessages re-ran during an active stream (switch-back to an
in-progress session). Restored master's !S.busy || (S.toolCalls && S.toolCalls.length).
Live: busy re-render now preserves tool cards (4→4, was 4→0).
Live-verified all 4 + confirmed #3709/#3592 invariants still hold (1 thinking card, none
below footer; distinct siblings preserved). + tests/test_issue3401_deep_review_fixes.py (7).
* test(#3401): realign 3 stale source-shape assertions to the deep-review fixes
Fix commit changed two source literals that existing stage tests scanned for:
- test_live_activity_timeline.py (x2): split anchor 'if(!S.busy){' → the restored
'if(!S.busy || (S.toolCalls&&S.toolCalls.length)){' guard (fix 4).
- test_run_journal_frontend_static.py: 'after_seq=0' not in source — fix 2's comment
contained that literal; rephrased the comment to 'the zero floor (after_seq of 0)'.
Intent of all three assertions unchanged; only the matched string updated. No code
behavior change.
* docs(changelog): v0.51.294 — Release JJ (stage-3401, #3401 live-to-final redesign)
---------
Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: Nathan-Hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: nesquena-hermes <[email protected]>
* fix(#3315): surface compression-exhausted/no-final-answer turns as errors (#3316)
When Hermes Agent exhausts context compression in a long tool-heavy turn, the
streamed result can end on a tool result / assistant(tool_calls) turn with no
final assistant answer. WebUI was finalizing that as a completed response.
Now _session_lacks_final_assistant_answer() + _agent_result_terminal_failure()
classify these as terminal failures and surface an apperror instead. The
compression session-id migration + pre-compression snapshot now run BEFORE the
terminal-failure return (ordering bug from the prior hold) so state stays
consistent when exhaustion fires after the agent rotated session_id.
Co-authored-by: Frank Song <franksong2702@gmail.com>
* docs(changelog): v0.51.292 — Release JH (stage-s4, #3316fixes#3315)
---------
Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <[email protected]>
* fix(#3405): respect profile provider/model in session resolution (#3448)
Profile-bound sessions now resolve their provider/model from the profile
instead of silently falling back to the global active provider — fixes wrong
credentials/billing and silent context truncation. Repairs stale models under
the profile provider (incl. the openai-codex + openai/ slash-model case) while
preserving native slash IDs on openrouter/custom.
Co-authored-by: Rod Boev <rod.boev@gmail.com>
* docs(changelog): v0.51.290 — Release JF (stage-s1, #3448fixes#3405)
---------
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <[email protected]>
* Fix update reload readiness race — poll /health server identity before reload (#3654)
Replaces the raw-uptime comparison (couldn't distinguish a fresh old process
from the restarted one) with a stable server_started_at identity read before
the update POST; reloads only when the identity changes. Both the force-update
and regular apply paths read + pass the baseline. (#874, #3654)
Co-authored-by: Frank Song <franksong2702@gmail.com>
* docs(changelog): v0.51.285 — Release JA (stage-r19)
---------
Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <[email protected]>
* feat(sidebar): add show_cron_sessions toggle to surface cron sessions (#3514, #2841)
Co-authored-by: Rod Boev <rod.boev@gmail.com>
* feat(sidebar): add manual session status labels (#3570)
Co-authored-by: Rod Boev <rod.boev@gmail.com>
* docs(changelog): v0.51.284 — Release IZ (stage-w4)
* fix(settings): persist show_cron_sessions in the explicit Save Settings path too (#3514)
Codex regression-gate follow-up: the autosave path (_preferencesPayloadFromUi)
included show_cron_sessions but the explicit saveSettings() button path read/saved
show_cli_sessions and dropped the cron checkbox — clicking Save Settings silently
omitted it. Read settingsShowCronSessions + add body.show_cron_sessions (gated on
CLI sessions, mirroring autosave).
* fix(settings): gate show_cron_sessions identically in BOTH save paths (#3514)
Codex round-2: my saveSettings() gate exposed that the autosave path
(_preferencesPayloadFromUi) posted the raw cron checkbox state ungated, so
show_cli_sessions=false + show_cron_sessions=true could persist via autosave.
Gate autosave on showCliCb too; update the regression test to assert both
paths gate on settingsShowCliSessions.
---------
Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Adds the Verdigris dark-only appearance skin (emerald/forest-green + bronze-gold),
renamed from the contributor's 'Hermes Agent' to a descriptive material name per
maintainer naming convention. Registered across all 5 skin sites (config allowlist,
boot.js swatch, index.html FOUC map, i18n in 12 locales, scoped CSS palette) + test.
Also fixes the zeus i18n test (zeus is no longer the trailing skin token).
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* feat(sessions): skip adaptive auto-rename for manually-named sessions (#3542, #3230)
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
* docs(changelog): v0.51.276 — Release IR (stage-p3e)
* fix(sessions): clear manual_title lock on /api/session/clear (#3542)
Codex regression-gate follow-up: the clear endpoint reset the title to
Untitled directly, stranding manual_title=True so the reused session never
auto-named again. Route the reset through apply_session_title_rename (which
clears the lock for auto-labels) + add a behavioral and a static-guard test.
---------
Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>