Commit Graph
6842 Commits
Author SHA1 Message Date
Frank Song 33859b2704 Merge remote-tracking branch 'origin/master' into paperclip/pr-5732 2026-07-09 09:43:54 +08:00
Frank Song d8b7ac23d6 fix(wakeup): require pool recovery before clearing pause 2026-07-09 09:43:48 +08:00
nesquena-hermes 9765f0e45b Merge pull request #5808 from nesquena/release/stage-5761
Release: refresh OpenCode Go static catalog (#5761)
2026-07-08 18:28:43 -07:00
nesquena-hermes 5f2ec9e1f0 docs(changelog): #5761 OpenCode Go catalog refresh 2026-07-09 01:23:33 +00:00
sbe27 68d406f7ff fix(models): refresh OpenCode Go static catalog 2026-07-09 01:23:08 +00:00
nesquena-hermes f8f60d5fdd Merge pull request #5807 from nesquena/release/stage-5782
Release: back off bg drain loop instead of silent tight-spin (#5782)
2026-07-08 18:22:36 -07:00
nesquena-hermes a1aca345e1 docs(changelog): #5782 drain-loop backoff 2026-07-09 01:18:50 +00:00
ai-ag2026andClaude Opus 4.8 141b49b74c test(bg): fix drain-loop backoff test import so it actually collects
The regression test used a bare `from _wakeup_helpers import install_fake_registry`,
which raises ModuleNotFoundError under the repo test runner (rootdir = repo root,
tests is a package). pytest treats that as a COLLECTION error for the file, not a
failure — so the whole suite stayed green while this file's tests silently never
ran, leaving the backoff fix with zero effective coverage.

Use the package-qualified `from tests._wakeup_helpers import install_fake_registry`,
matching the four sibling bg_task_complete suites. Confirmed the import now
resolves from the repo root (the bare form fails).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 01:18:21 +00:00
ai-ag2026andClaude Opus 4.8 8dde1de4fa fix(bg): back off drain loop instead of silent tight-spin (#2476, #4633)
_drain_loop read process_registry.completion_queue directly under a bare
`except Exception: continue`. A registry missing that attribute raised
AttributeError that was swallowed and retried with no backoff — a
100%-CPU tight loop with no log line. The loop now reads the queue via
getattr(..., None) and backs off on the stop event when it is absent
(mirroring streaming.py), catches queue.Empty explicitly for the normal
idle path, and logs a warning + backs off (_DRAIN_STOP.wait(1.0)) on any
other queue error so a persistent fault can neither spin the CPU nor stay
invisible.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 01:18:21 +00: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
nesquena-hermes 8e00277ce5 Merge pull request #5805 from nesquena/release/stage-5785
Release: frame passkey-login 200 with Content-Length (#5785)
2026-07-08 18:10:07 -07: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
nesquena-hermes 80276850a6 docs(changelog): #5785 passkey-login Content-Length 2026-07-09 01:05:56 +00:00
ai-ag2026andClaude Opus 4.8 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
nesquena-hermes 9dacbe60de Merge pull request #5804 from nesquena/release/stage-5787
Release: pair metering begin_session with end_session teardown (#5787)
2026-07-08 17:55:24 -07:00
nesquena-hermes 2c46e9e251 docs(changelog): #5787 metering session begin/end pairing 2026-07-09 00:51:59 +00: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 ff0e6347cb Merge remote-tracking branch 'origin/master' into paperclip/pr-5732 2026-07-09 08:22:26 +08:00
nesquena-hermes 4fc637d8d0 Merge pull request #5790 from nesquena/feat/update-channels
Release channels: stable / experimental self-updater
2026-07-08 17:12:50 -07:00
nesquena-hermes d4120b47b5 test(updates): update timeout-guard assertions for the channel-threaded apply/force bodies 2026-07-08 22:52:53 +00: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
nesquena-hermes 3730a4a551 Merge pull request #5796 from nesquena/release/stage-5784
Release: evict run-journal writer-lock cache on journal delete (#5784)
2026-07-08 14:23:09 -07:00
Nathan Esquenazi be004baaaf Merge branch 'master' into feat/update-channels 2026-07-08 14:20:48 -07:00
nesquena-hermes 37adf8b7ce docs(changelog): #5784 evict run-journal writer-lock cache on delete 2026-07-08 21:14:04 +00:00
ai-ag2026andClaude Opus 4.8 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
nesquena-hermes 6ab95bffa4 Merge pull request #5794 from nesquena/release/stage-5783
Release: serialize daemon-thread start under a lifecycle lock (#5783)
2026-07-08 14:03:05 -07:00
nesquena-hermes 81a77efc91 docs(changelog): #5783 serialize daemon-thread start under lifecycle lock 2026-07-08 20:58:54 +00:00
ai-ag2026andClaude Opus 4.8 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
nesquena-hermes 460f3eb47d Merge pull request #5793 from nesquena/release/stage-5781
Release: sweep completion-dedup map by delivery lifecycle (#5781)
2026-07-08 13:47:35 -07:00
nesquena-hermes 4f9d63218b docs(changelog): #5781 sweep completion-dedup map by delivery lifecycle 2026-07-08 20:42:35 +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
t 046a0d6bcb lint: drop unused pathlib.Path import in test_update_channels (ruff F401) 2026-07-08 20:28:15 +00:00
nesquena-hermes c008e4443a Merge pull request #5791 from nesquena/release/stage-5780
Release: open state.db read-only for lineage reads + watcher (#5780)
2026-07-08 13:22:57 -07:00
nesquena-hermes 3a8de74758 docs(changelog): #5780 open state.db read-only for lineage reads + watcher 2026-07-08 20:18:25 +00:00
t 43afc498b8 test+style: fix 2 full-suite regressions from the channel edits
- test_api_timeout: assert the updates/check call keeps timeoutMs:60000 in its
  new _checkBody form (functionally identical; still the 60s override)
- panels.js: reword two // comments to drop apostrophes — the smooth-text-fade
  brace-matcher test treats ' in a comment as a string delimiter and lost
  loadSettingsPanel's closing brace (node --check confirms the JS was always valid)
2026-07-08 20:18:10 +00:00
ai-ag2026andClaude Opus 4.8 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 99bd2930cc fix(updates): mobile badge ellipsis needs display:block (Fable r2)
text-overflow:ellipsis is a no-op on the inline-flex badge; display:block in the
<=768px rule makes the ellipsis actually render for a long experimental describe.
2026-07-08 20:06:34 +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 bf5e5a0d3d test(updates): fix channel-signature fallout + add i18n keys to all 14 locales
- test_update_stash_recovery: add merge-base handler for the force-update
  rewind guard probe (origin/master is a descendant of HEAD -> not a rewind)
- test_security_review_fixes: fake_check accepts channel kwarg
- i18n.js: native translations of the 4 update-channel keys for all 14
  non-English locales (locale-coverage tests enforce key presence)
2026-07-08 19:37:44 +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