franksong2702's activity_scene_v1 anchor-scene model: live-stream/settled/refresh/re-entry
show the same ordered turn. Rebased clean (no conflicts) onto current master.
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
- Remove the redundant 4th OR clause from _catalog_has_provider() — the
existing C1–C3 already handle compound provider IDs correctly (C2 IS
a normalized-vs-raw comparison; the 4th clause was a strict subset).
- Add && _activeProvider guard to _isCrossProviderPick in messages.js
to prevent spurious True when the profile has no configured default
provider (reviewer's optional hardening suggestion).
- Add test_catalog_has_provider_compound_ids.py verifying that compound
provider IDs like 'custom:glm-free-relay' are correctly recognized by
the existing C1–C3 clauses.
Two-part fix for the model selector jumping back to the profile default
after the first message when a cross-provider model (e.g. a custom relay
like custom:glm-free-relay) is selected.
Backend (api/routes.py):
- _catalog_has_provider() now matches compound provider IDs (e.g.
custom:zenmux-relay) whose normalized form (custom) is a raw
catalog provider_id. Previously the three existing checks missed
normalized-vs-raw comparison for compound hints, causing the catalog
lookup to fail and the fallback branch to silently revert the model
to the profile default on every non-explicit turn.
Frontend (static/messages.js):
- The explicit_model_pick marker was consumed after the first send(),
so subsequent sends lost the signal and the server repaired the
model back to the profile default (#3737 regression).
- Now, when the session has a non-default model from a different
provider than the profile's active provider, every send() sends
explicit_model_pick=true so the server honors the user's choice
across the entire conversation.
- The pending onchange marker is still consumed after the first send
(unchanged behavior); only the explicit_pick inference is broadened.
The /api/profiles handler re-imported _is_isolated_profile_mode locally inside
handle_get, which made the name function-local for the ENTIRE handle_get scope
-> the earlier use at the /api/projects branch (routes.py:8000) hit
UnboundLocalError (F823), failing 6 test shards + lint. It's already a
module-level re-export (routes.py:429), so the local re-import is redundant;
removing it lets every in-function use resolve to the module global. Also drop
an unused 'from pathlib import Path' in the new #4449 state-dir test.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Gate-found (Codex CORE, repro'd): a hardlink at a clean *.md page name can
carry an arbitrary inode (incl. one outside the wiki) through the O_NOFOLLOW +
inode-identity read check — O_NOFOLLOW does not block hardlinks. Reject any
page file with st_nlink > 1 in BOTH the allowlist walk and the read-path
revalidation. Pre-existing on master (the inode recapture predates this PR's
cache); closing it here while on the read path. +1 toothed regression test
(prime cache -> hardlink listed page to outside secret -> assert 404, no leak).
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Gate-found: reaping on started_at age alone could pop a genuinely-live worker
that's mid-teardown past the 180s ceiling (active_stream_id cleared during final
writeback, STREAMS entry still present) — ACTIVE_RUNS is worker-lifecycle truth
for health/background-wakeup/agent-cache consumers. Add a 'not in STREAMS'
guard (snapshot taken sequentially before ACTIVE_RUNS_LOCK, no nesting) and pop
by the real dict key. +1 regression test (toothed). Keeps the anti-permanent-409
age behavior intact.
Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
Codex gate caught that the new load_pool() path runs under the process-default
profile (sibling endpoints /api/providers + /api/models/live already wrap).
Without it a multi-profile client sees+seeds the default profile's pool
(#4247/#4067 profile-isolation class).
Codex root-cause consult confirmed: the CLI-session cache (_CLI_SESSIONS_CACHE,
5s TTL) and session-list cache were keyed on (st_mtime_ns, st_size) of state.db
+ WAL sidecars. Under WAL-mode writes those stamps collide (same mtime-ns bucket
+ WAL frame size after checkpoint), serving a stale cache -> freshly-committed
gateway/CLI session intermittently absent from /api/sessions (the recurring
test_gateway_sync flake AND a real up-to-5s sidebar delay for gateway writes).
PRAGMA data_version is NOT usable here (fresh per-request connection always
reports its own initial value, never advances — verified). Fix = a cheap
content fingerprint (COUNT/MAX(rowid) over sessions+messages) read on a fresh
connection, which DOES advance on every commit and is immune to mtime
granularity. Wired into _sqlite_file_stat_cache_key (models) + the routes-layer
source stamp. + 3 regression tests + corrected the earlier false root-cause
comments.
Codex completeness finding: show_previous_messaging_sessions is a third
visibility setting in the /api/sessions cache key (bypasses messaging dedupe)
with the same stale-cache bug. Added it to the invalidation tuple.
ROOT FIX (product bug): POST /api/settings never invalidated the session-list
cache, so toggling show_cli_sessions/show_cron_sessions could serve a stale
sidebar for up to the cache TTL when the settings-file mtime stamp collided with
a cached entry. The handler now clears the cache directly on those toggles. This
is also the root cause of the recurring test_gateway_sync CI flake (session
absent from /api/sessions right after the visibility toggle).
DEFENSE-IN-DEPTH: cleanup_test_sessions fixture now resets show_cli_sessions to
default BEFORE each test too, not only in teardown.
FLAKE FIX (test infra): bump 31 node-subprocess correctness-harness timeouts from
10s -> 30s across 23 test files. Node spawn + V8 warmup under parallel CI shard
load can exceed 10s, causing TimeoutExpired flakes (e.g. test_1325 on #4445).
Only real node subprocess.run() sites changed — mock signatures, HTTP urlopen,
and deliberate timeout-behavior tests (test_api_timeout, timeout=0/1/2) untouched.
Codex regression gate found that _session_model_identity_matches normalized
only the @provider:model form, not provider/model (slash). A session stored as
'deepseek/deepseek-v4-1m' resolving to bare 'deepseek-v4-1m' was treated as a
model change -> the 256k fallback bypassed the accept-guard and clobbered the
persisted 1M window (the same #4248 bug on slash-qualified IDs). Mirror the
slash-aware split already used by _model_matches_configured_default. Adds a
route-level regression + unit test for the slash shape.
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
A stale sidecar with is_cli_session=true could make an archived
cron/tool/API session resurface in the Sessions tab on refresh. The
session loader now treats explicit cron/tool/api source metadata as
non-CLI even when the sidecar claims is_cli_session, and preserves cron
identity when the archive/materialization fallback creates a session.
Co-authored-by: TomBanksAU <246584738+TomBanksAU@users.noreply.github.com>
Adds WebUI controls to manage the messaging gateway lifecycle without a
terminal. _handle_gateway_lifecycle routes start/stop/restart to the
existing platform-aware `hermes gateway {action}` CLI via a list-argv
subprocess (no shell), scoped to the active profile, with bounded
timeout/error handling and a status-card refresh. Auth + CSRF gated;
profile name charset-validated; stdout/stderr never leaked to the client.
Maintainer hardening (warm-up SHOULD-FIX): added _GATEWAY_ACTION_LOCK, a
server-side single-flight guard. The client disables its button in-flight,
but a scripted authed client could otherwise fire overlapping
start/stop/restart calls and spawn concurrent gateway subprocesses. A
non-blocking acquire now returns 409 on contention (mirrors the self-update
_apply_lock pattern), released in finally. Added a regression test
(toothless-checked: without the lock the 2nd action spawns a subprocess +
returns 200).
Cross-platform: delegates to the mature hermes_cli gateway subcommand
(systemctl/launchctl), pathlib paths, PYTHONUTF8=1 + BROWSER=echo in env.
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Surfaces the Hermes agent's LLM Wiki as a read-only knowledge-base
observability section in Insights (status counts + browsable read-only
pages). Unconfigured shows a clear "Unavailable" empty state.
Strictly read-only, auth-gated. The wiki file-serving path is hardened
against the full traversal/symlink/TOCTOU class (Codex + Opus review, each
vector regression-tested + toothless-checked):
- _llm_wiki_page_files resolves the wiki root once as the trust base;
skips a section whose resolved path escapes the root; requires each *.md
target's RESOLVED path under both real root and real section; rejects
dot-prefixed segments on lexical AND resolved-relative paths (closes
symlink-to-hidden/out-of-tree + symlinked-section escapes). Browse + read
share this allowlist.
- /api/wiki/page rejects abs paths + a real '..' SEGMENT (legit v1..v2.md
still opens); requires the resolved target in the allowlist; reads the
resolved path with O_NOFOLLOW; and fstat-verifies the open fd's
(st_dev, st_ino) against the identity captured at allowlist time — closing
final-component AND parent-directory symlink-swap TOCTOU races. Any
mismatch/vanish -> clean 404, never a 500.
Co-authored-by: Rod Boev <rod.boev@gmail.com>