Commit graph

2314 commits

Author SHA1 Message Date
Frank Song
d8b7ac23d6 fix(wakeup): require pool recovery before clearing pause 2026-07-09 09:43:48 +08:00
Frank Song
a5974d0ae9 Merge remote-tracking branch 'origin/master' into paperclip/pr-5732 2026-07-09 09:15:09 +08: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
Frank Song
59c868fe86 fix(wakeup): preserve pause on gateway stop race 2026-07-09 09:06:19 +08:00
ai-ag2026
b10116bffc Frame passkey-login 200 response with Content-Length
The passkey-login success handler wrote its 200 JSON body without a
Content-Length header, unlike the password-login and logout handlers. Under
HTTP/1.1 keep-alive that response is unframed, so the browser's fetch().json()
waits for connection close and appears to hang until it times out.

Encode the body once and send Content-Length before end_headers(), mirroring
the /api/auth/login block. Header order is unchanged so set_auth_cookie's
Set-Cookie still precedes end_headers().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 01:05:28 +00:00
ai-ag2026
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
ff0e6347cb Merge remote-tracking branch 'origin/master' into paperclip/pr-5732 2026-07-09 08:22:26 +08:00
nesquena-hermes
236a2c8032 fix(updates): thread offered channel through apply/force to close the debounce race
Codex gate CORE: /api/updates/apply + /force ignored the request-body channel
and re-read the saved setting, so a channel switch whose debounced autosave
hadn't landed could apply the OLD channel. The banner now sends the channel the
CHECK reported for each target; the endpoints validate it against the enum and
thread it into apply_update/apply_force_update (None → saved-setting fallback,
preserving prior behavior). Agent stays channel-neutral server-side.
2026-07-08 22:42:54 +00:00
g
912c626195 Merge remote-tracking branch 'origin/master' into pr-5790-live 2026-07-08 22:26:22 +00:00
Frank Song
75409c511c fix(wakeup): probe empty-provider recovery changes 2026-07-09 06:07:23 +08:00
Frank Song
91141da397 Merge remote-tracking branch 'origin/master' into paperclip/pr-5732 2026-07-09 05:34:24 +08:00
Nathan Esquenazi
be004baaaf
Merge branch 'master' into feat/update-channels 2026-07-08 14:20:48 -07:00
ai-ag2026
c5b15850e5 Fix run-journal writer-lock cache leak on session delete
`_lock_for` caches one threading.Lock per (dir, file, pid) in the module-global
`_WRITER_LOCKS`, but nothing evicted those entries: `delete_run_journal` rmtree'd
the on-disk `_run_journal/{sid}/` directory yet left the cached lock objects
behind, so a long-lived gateway leaked one entry per deleted run forever.

Delete now evicts every cached lock whose parent directory matches the removed
session (pid-independent) under `_WRITER_LOCKS_GUARD`, leaving unrelated
sessions' locks intact.

Refs #4633, #2097.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 21:13:32 +00:00
Frank Song
cb4248bc25 Merge remote-tracking branch 'origin/master' into paperclip/pr-5732 2026-07-09 05:03:24 +08:00
ai-ag2026
d505b45545 fix(bg): serialize daemon-thread start under a lifecycle lock
start_drain_thread and start_session_channel_reaper checked is_alive()
and then created + started the daemon thread without holding a lock, a
check-then-act race: two concurrent callers could both see "not alive"
and each spawn a thread. The loser's thread was never stored in the
module global and ran forever, un-joinable by the matching stop_*. Both
check-then-start sequences now run under a dedicated
_THREAD_LIFECYCLE_LOCK (kept separate from the purpose-bound
SESSION_CHANNELS_LOCK / _EMIT_COALESCE_LOCK), so exactly one thread is
ever created.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 20:58:18 +00:00
ai-ag2026
15470a9a35 fix(bg): sweep completion-dedup map by delivery lifecycle, not channel (#4633)
BG_TASK_COMPLETE_EVENTS_SEEN gained a session_id -> set[process_id] entry the
first time a bg task completed for a session and was never deleted anywhere, so
it grew unbounded for the server lifetime.

The entry is created in _process_one for EVERY completion, whether or not any
SSE channel/tab exists, so pruning it only when the SessionChannel is reaped
would miss the dominant headless case (task fires, tab closed or never opened —
no channel to collect). Instead the reaper now sweeps the map by DELIVERY: once
a completion is drained (its session_id removed from PENDING_BG_TASK_COMPLETIONS)
the short _move_to_finished dedup window is closed and the entry is swept, every
tick, regardless of any channel. The registry's per-process_id
_completion_consumed gate remains the primary idempotency backstop, so sweeping a
delivered session's set can never resurrect an already-delivered completion.

Session deletion also prunes the entry (new forget_bg_task_completion_dedup),
covering a session deleted while a completion is still pending (which the
delivery-gated sweep deliberately retains).

Refs #4633
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 20:41:59 +00:00
Frank Song
75b1571204 Merge remote-tracking branch 'origin/master' into paperclip/pr-5732 2026-07-09 04:34:10 +08:00
ai-ag2026
b967d89c3a fix(sessions): open state.db read-only for lineage reads + watcher projection
The session-listing path already opens the live agent state.db read-only
(file:...?mode=ro) so a write-capable handle doesn't add checkpoint/lock
surface while the agent streams into the same WAL DB (#5455). Three pure-read
projections were missed and still opened a read-WRITE connection:

  - read_session_lineage_report
  - read_session_lineage_metadata
  - the gateway-watcher fingerprint projection (a 5s poll)

Route all three through a shared open_state_db_readonly() helper that mirrors
the listing path: read-only file: URI with a writable fallback that warns. The
self-heal write path (missing idx_messages_session) is intentionally left
writable. No behavior change beyond open mode; reads perform zero writes.

Refs #5455

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 20:17:45 +00:00
t
0a56f44f5e fix(updates): Fable UX gate — channel re-check race + copy + mobile badge
MUST-FIX: /api/updates/check now accepts an explicit 'channel' in the POST body;
the Settings dropdown passes the just-picked value to checkUpdatesNow(channel) so
the immediate re-check can't race the debounced autosave PUT and answer for the
previous channel (verified live: body channel wins over stale saved setting).

SHOULD-FIX:
- copy: 'soaked' -> 'soak-tested'; append 'Applies to WebUI updates only.' to the
  channel helper text (all 15 locales, native translations)
- mobile: .settings-version-badge gets max-width/overflow/ellipsis in the <=768px
  query so the '· Experimental' suffix can't overflow a 320px viewport

(#5 badge-suffix i18n key deferred — non-blocking fast-follow per Fable, matches
the existing English-only version badge on master.)
2026-07-08 20:01:39 +00:00
t
0168bd1aed fix(updates): apply_clear_lock preserves the configured channel
Codex round-2 gate: apply_clear_lock re-entered _apply_update_inner(target)
without the channel, so an experimental WebUI lock-recovery retry silently fell
back to stable. Now passes _read_update_channel() through. + regression test.
2026-07-08 19:53:32 +00:00
t
162e145c30 fix(updates): channel is WebUI-only — never leak it to the Agent repo check/apply
Codex gate CORE fix: check_for_updates passed the user's WebUI channel into the
Agent repo resolution, so on 'experimental' the Agent ignored its v* tags and
fell back to origin/master. The Agent is a separate project (plain v* tags,
tracks master past tags) and must ALWAYS use DEFAULT_UPDATE_CHANNEL. Forced in
all 3 sites: check_for_updates agent leg + _apply_update_inner + apply_force_update
(target=='agent' -> channel=DEFAULT_UPDATE_CHANNEL). Added a real-git regression
proving Agent release/apply resolution is identical under both WebUI channels.
2026-07-08 19:49:36 +00:00
t
2cf43527a3 feat(updates): stable/experimental release channels
Add an update_channel setting (stable|experimental) that selects which git-tag
stream the self-updater tracks on the single linear master line:

  stable       -> 'v*'      promoted, soaked releases (default; unchanged glob)
  experimental -> 'exp-v*'  every release batch (opt-in testers)

A channel is only a tag glob — no branches, no divergence — so every ff-only
guarantee (#2653/#2846/#3140) is preserved. Channel governs the WebUI repo only;
the Agent repo keeps its historical branch fall-through.

Correctness (advisor-reviewed, Codex + Fable):
- describe uses --match <glob> so a commit tagged both v* and exp-v* resolves
  to the channel-correct tag
- stable never falls through to origin/master when HEAD contains the latest
  stable tag (the firehose-suppression that makes channels work)
- apply_force_update refuses to reset --hard onto an ancestor ref (rewind guard)
- update cache + in-progress guard keyed by (channel, include_agent)
- channel display badge is a SEPARATE field; WEBUI_VERSION stays channel-neutral
  (asset cache-busting / SW cache / stale-client skew all do exact-string equality)

Settings UX: channel dropdown with risk-forward copy + the ff-only asymmetry
helper ('switching back to Stable keeps your current version until Stable catches
up'), channel chip on the version badge, i18n keys, re-check on switch.

Tests: new tests/test_update_channels.py (12 real-git-fixture tests) + existing
update suite updated for the channel-aware signatures.
2026-07-08 19:25:27 +00:00
Frank Song
0419fb0640 fix(wakeup): prove credential recovery before clearing pause 2026-07-09 00:39:27 +08:00
Frank Song
9b352e3acf Merge remote-tracking branch 'origin/master' into paperclip/pr-5732 2026-07-09 00:28:30 +08:00
Frank Song
08e9c2898e fix(wakeup): revalidate paused credentials on canonical profile lane 2026-07-09 00:27:57 +08:00
Rod Boev
f4172f73b0 fix(mcp): preserve config overrides for profile reads (#5619) 2026-07-08 16:18:52 +00:00
Rod Boev
3c8d9f851e fix(mcp): scope list config reads to active profile (#5619) 2026-07-08 16:18:52 +00:00
Frank Song
68e4770c2b fix(wakeup): revalidate recovered credential pauses 2026-07-08 08:56:05 +08: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
Frank Song
5764df66f3 fix(wakeup): share process pause lane resolution 2026-07-07 17:00:36 +08:00
Frank Song
1ab9c47e9b fix(wakeup): canonicalize paused route model lanes 2026-07-07 15:09:30 +08:00
Frank Song
09862a3f9a fix(wakeup): stabilize credential pause revalidation 2026-07-07 14:08:25 +08:00
Frank Song
2c5035d6e7 fix(gateway): recheck cancel before wakeup pause clear 2026-07-07 13:21:36 +08:00
Frank Song
3acd588621 fix(gateway): preserve wakeup pause on late cancel 2026-07-07 13:15:54 +08:00
Frank Song
a61381ee7e fix(wakeup): key stale pause to failed lane 2026-07-07 13:08:27 +08:00
Frank Song
46e788797e Merge remote-tracking branch 'origin/master' into paperclip/pr-5732 2026-07-07 13:01:42 +08:00
Swan-S
ccd62f9b9d fix(streaming): drop empty tool_calls arrays before API send (#5737)
Strict providers (DeepSeek v4, newer OpenAI) reject an assistant message
carrying tool_calls: [] with HTTP 400. _sanitize_messages_for_api already
strips orphaned tool results, but not an assistant message that literally
stores an empty tool_calls list.

Drop the empty key on ALL THREE sanitized-dict sites that must agree:
_sanitize_messages_for_api, _api_safe_message_positions, and the
_safe_projection inside _restore_reasoning_metadata (so a tool_calls: [] row
projects identically on both sides of the metadata-carry-forward alignment and
doesn't lose its reasoning/id/timestamp every turn — Fable gate follow-up,
folded in).

Added mutation-checked regression tests: empty tool_calls dropped + populated
chain preserved (both sanitizer paths), and metadata carry-forward survives a
tool_calls: [] row.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-07-07 04:43:46 +00:00
Frank Song
1424917c1d fix(streaming): keep checkpoint guard comment adjacent 2026-07-07 12:42:22 +08:00
Frank Song
8e2a56e563 fix(streaming): keep checkpoint guard adjacent to try 2026-07-07 12:40:07 +08:00
Frank Song
b25e99c1b4 fix(wakeup): revalidate credential pause on refresh 2026-07-07 12:34:41 +08:00
Frank Song
477f567a19 fix(wakeup): preserve pause for stale credential streams 2026-07-07 12:29:32 +08:00
Frank Song
524353b62d fix(wakeup): keep pause on late cancel 2026-07-07 12:09:16 +08:00
Frank Song
cfd1169126 fix(wakeup): preserve pause through cancelled turns 2026-07-07 11:55:48 +08:00
Frank Song
bcb411db53 fix(wakeup): pause credential-exhausted process wakeups 2026-07-07 11:45:01 +08:00
Daniel Harcek
407451977e fix: auto-switch profiles for session links (#5419)
A valid session:// deep link to a session owned by a DIFFERENT Hermes
profile used to look identical to a deleted session (404 -> frontend
self-heals away). Now GET /api/session returns a structured 409
session_profile_mismatch envelope (error/code/session_id/profile ONLY,
no transcript) ONLY when the owning profile is KNOWN, and loadSession()
catches it, switches to the owning profile, and retries once. Truly
missing/deleted or legacy None-profile sessions keep the 404 self-heal.

Gate fixes applied (Codex + Fable):
- Codex CORE: added a post-await stale-load guard after
  _switchProfileForSessionLoad so a navigation during the switch can't
  hijack the UI back to the old session.
- Fable Finding 1: only emit 409 when _session_profile is truthy;
  a None-profile (missing/legacy) session under a non-default active
  profile now keeps 404 instead of a useless profile=null 409 (which
  skipped self-heal + spun the SSE reconnect against a dead sid). Both
  detail branches. + 2 regression tests.
- Fable Finding 2: _switchProfileForSessionLoad now clears the sidebar
  skeleton + re-renders from cache on switch-POST failure (mirrors the
  #4671 canonical-switch catch), then rethrows, so a failed switch can't
  strand the sidebar on the skeleton.

Reconciled tests/test_issue1611_session_profile_filtering.py (4 tests) to
the 409 contract while preserving the no-leak boundary assertion.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-07-07 02:35:02 +00:00
Jinho Seok
f106fcd1f5 fix(workspace-git): hide git subprocess console windows on Windows (#5692)
Add creationflags to _run_git() and _config_names_for_scope() so git
child processes don't accumulate hidden cmd.exe/conhost windows on Windows.

Uses a local module-level _windows_hide_flags() helper (mirrors the
api/updates.py pattern: CREATE_NO_WINDOW on win32, 0 elsewhere) rather than
importing from the OPTIONAL hermes_cli package — a standalone/agent-less
WebUI (and the repo's own CI runners, which don't pip-install hermes-agent)
must keep full workspace-git functionality. windows_hide_flags() is a safe
no-op (0) on non-Windows. Scoped to workspace git commands.

Adds mutation-checked tests asserting the kwarg is wired on both sites.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-07-07 02:07:39 +00:00