claw-io: symlinks whose target resolves outside the workspace root were silently dropped; now emitted
with target_outside_workspace=True (display-only — safe_resolve_ws/open_anchored_fd still block
navigation). Dedup-winner over #4573 (stronger tests). Co-authored-by: claw-io <claw-io@users.noreply.github.com>
latipun7: adds id-ID-GadisNeural to the Edge TTS allowlist (Edge TTS is a core builtin -> parity, default-in)
+ picker label + sets speaking-state at start of _playEdgeTtsChunked so the Listen button reflects playback.
Co-authored-by: latipun7 <latipun7@users.noreply.github.com>
Gate findings (Codex 2-CORE / Opus SHIP-WITH-FIXES), both fixed:
1. cross-profile leak — _pre_compression_continuation_session_id scanned all profiles' sessions,
matched only by parent_session_id; now filters children by _profiles_match(child, snapshot)
+ validates returned sid with is_safe_session_id. +cross-profile regression test.
2. restore-state poison — frontend wrote continuationSid to localStorage/URL BEFORE the inner
loadSession proved it loadable; removed the speculative write (success path at the loaded id
handles it). Updated the source-grep test to assert the safe form.
Bounded BFS (range(20)+seen) + recursion guard confirmed safe by both gates.
george-andra: GET /api/session surfaces continuation_session_id for a hidden pre-compression
snapshot; loadSession follows it so a mobile reload mid-compression recovers to the visible
continuation instead of a hidden snapshot (chat looked 'lost'). Resolved 2 conflicts (1615 behind):
sessions.js merged w/ #2971 re-arm guard; routes.py merged w/ _session_source_is_webui branch.
Co-authored-by: george-andra <george-andra@users.noreply.github.com>
franksong2702: preserve the reader's semantic viewport anchor during mid-stream transcript rebuilds
before raw scrollTop fallback; remount virtualized anchors before restore w/ _programmaticScroll
scoped to the remount frame; treat captured userUnpinned/pinned snapshot as authoritative so
distance-inference can't re-pin a reader who scrolled away during a live reply. Browser-only scroll
state. Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
Manual /compress wrote the compressed payload into BOTH s.messages (visible) and s.context_messages
(model-facing) + wiped s.tool_calls -> messages vanished from the window. Fix keeps s.messages +
tool history durable, deep-copies compressed only into s.context_messages, anchors off visible msgs.
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
Codex gate (SILENT): the scene-builder/hydrator _anchor_scene_row_looks_like_final_answer
(messages.js + routes.py) used a loose '>=80 + prefix' rule while the renderer
_anchorSceneProseMatchesFinalAnswer uses '>=80 + ratio>=0.9'. A short intermediate-prose row that
was a prefix of the final answer could be dropped from the persisted scene by the looser builder,
leaving the stricter renderer no row to show -> silent prose loss. Aligned both filters to the
renderer's rule (drop fewer -> preserve more intermediate prose). Matrix already GREEN; this closes
the edge the fixture didn't exercise.
Transparent Stream lost ALL tool/thinking activity on every rehydrate path (settle/switch/hide/blur/
reload) — only final answer survived, because _renderSettledAnchorSceneForMessage early-returned
outside Compact Worklog. Adds a transparent-mode settled renderer (same persisted activity_scene_v1,
event rows) + intermediate-prose preservation (suppress only the final-answer-dup prose). Compact
Worklog unchanged.
Codex re-gate (CORE, reproduced): _reload_dotenv() was guarded, but get_profile_runtime_env() also
copies a profile .env into runtime env, which profile_env_for_background_worker()/streaming apply to
os.environ -> HERMES_WEBUI_ISOLATED_PROFILE=0 in a profile .env still escaped isolation on that path.
Fix: filter _PROTECTED_ENV_KEYS in get_profile_runtime_env's .env loop + add the key to
_BLOCKED_RUNTIME_ENV_KEYS (gateway-parity filter). + runtime-path regression test.
Codex gate (CORE, reproduced): _reload_dotenv() copies a profile's .env into os.environ, so a
contained user could set HERMES_WEBUI_ISOLATED_PROFILE=0 in their own profile .env and silently
escape isolation. Fix: _PROTECTED_ENV_KEYS — _reload_dotenv refuses to override operator/deployment
keys from a profile .env (logs a warning). + regression test proving .env can't disable isolation.
Profile-isolation regression: isolated mode was inferred from HERMES_HOME path shape (*/profiles/<name>),
which the launcher exports for ANY normal named profile -> single-users on a named profile lost profile
switching + saw only 1 profile. Fix: HERMES_WEBUI_ISOLATED_PROFILE explicit opt-in is now the PRIMARY gate.
Codex+Opus ship-gate: a <=64KB but deeply-nested JSON manifest makes json.loads raise
RecursionError, which escaped _read_manifest_urls into the app-shell route -> every page
load 503s. Add RecursionError to the fail-safe except + regression test (verified
red-without/green-with). Co-authored-by: santastabber <santastabber@users.noreply.github.com>
rodboev. Default-off; OFF path byte-identical to master (cache key 'u' + _renderUserFencedBlocks
unchanged when window._renderUserMarkdown falsy). ON routes user text through the same renderMd
SAFE_TAGS/_isSafeUrl sanitizer as assistant msgs. Nathan override-approved (likes the idea, opt-in).
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
The #4411 snapshot test codified the OLD recursive wrapper signature verbatim
(window.X=function(){return X()}), which is the brick the recursion fix removes.
Update lines 829-830 to assert the correct direct-assignment export form + that the
wrapper form is absent — so the test now guards AGAINST the brick instead of for it.
Codex final re-confirm flagged this stale assertion (product code already correct).
The #2715/#2771 brick-guard (test_window_function_collision) capped the window.X=RHS
capture at 32 chars and required a trailing terminator to classify a bare identifier,
so a very long function name (e.g. _renderLiveAnchorActivitySceneSnapshotForStream, 47
chars) was misclassified as 'other' → false positive on the now-correct direct-assignment
exports. Widen capture to 80 chars + recognize an identifier running to end-of-window.
Still flags the real object/number/other bug shapes.
Codex ship-gate (CORE). ui.js:9241 re-exported three top-level functions via
same-name wrappers (window.X = function(){ return X() }). In a classic script a
top-level `function X(){}` IS window.X, so the wrapper reassigns that global to
itself → infinite recursion → RangeError: Maximum call stack size exceeded on
every live worklog render / session reattach / snapshot-restore path (send,
sessions.js:1264/1375/570). Replace with direct assignments. JS-only brick the
pytest suite can't catch (no JS execution) — confirmed by classic-script semantics
+ will verify live in-browser. Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
Mirror the write-side _find_anchor_scene_message ambiguity guard on the read side:
_hydrate_anchor_activity_scenes now counts ref occurrences and skips ref-based
attachment for any ref resolving to >1 assistant message (falls through to the
positional index match), so a duplicate content-digest ref can't double-render the
same worklog scene. Low-severity defense-in-depth on the crown-jewel render surface.
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
Codex ship-gate (CORE): _handle_session_anchor_scene loaded the session via
_get_or_materialize_session (by-id, no profile scoping) and persisted
anchor_activity_scenes without the active-profile visibility check GET /api/session
applies — an authenticated request under profile A could write anchor scenes onto a
profile-B session. Add the _session_visible_to_active_profile guard (404, no write),
mirroring routes.py:~8922. Regression test verified red-without/green-with the guard.
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
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>
Converged both bounce items: enrich-existing-only (continue on webui_list-is-None,
no new providers -> routing side-effect gone, routes.py no longer touched) + respects
per-provider ID prefix convention (@nous:). Co-authored-by: kaishi00 <kaishi00@users.noreply.github.com>
Two existing tests asserted _load_models_cache_from_disk()/get_available_models()
return EXACTLY the saved payload; the loaders now also return aliases reconstructed
from config (empty {} when no config aliases). Update both assertions to expect the
aliases field. Caught by the full suite + Codex re-gate.
Codex ship-gate (SILENT): both disk loaders dropped `aliases` (the save path
never persisted them), so a disk-cache hit — strict cold-path OR the new stale
fallback — served /api/models with empty aliases, silently breaking `/model
<alias>` slash-command resolution (static/commands.js resolves slash aliases
only from /api/models.aliases). Add _model_aliases_from_config() helper and
reconstruct aliases from current config in BOTH loaders (fixes the pre-existing
strict-loader gap too). Adds a regression test.
Co-authored-by: starship-s <starship-s@users.noreply.github.com>
Applies Opus ship-gate findings: (1) the autouse fixture now points
_get_models_cache_path at an isolated temp file so the over-budget static-fallback
test can't read a real ~/.../models_cache.json and become an order-dependent flake;
(2) _load_stale_models_cache_from_disk rejects a cross-schema cache (its groups/badge
shape may be incompatible with the current picker). Adds a cross-schema-rejection test.
Co-authored-by: starship-s <starship-s@users.noreply.github.com>
The legacy /v1/chat/completions approval handler (added for #4549) rendered
the approval card but never populated _STREAM_RUN_IDS, so /api/approval/respond
could not relay the user's choice back to the gateway to resume the parked run
— it fell through to the local approval path and returned {ok: false}. Record
the run_id from the approval payload (no-op when absent), mirroring the runs-API
path. Adds a behavioral regression test that fails on the pre-fix head.
Codex ship-gate finding (CORE). Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Adds a Preferences setting 'Virtualize long transcripts' (default ON, opt-out).
When disabled, _currentMessageVirtualWindow returns a full non-virtualized window
so the entire transcript renders at once (also short-circuits the virtual
re-measure loop). Server-persisted (virtualize_transcript bool, default True),
hot-applies via renderMessages on toggle. i18n in all 13 locales.
Pairs with @rodboev's #4328 stream-end anchor scroll fix (cherry-picked into
this branch) to fully resolve#4325.
Independent review (APPROVED clean) + greptile both flagged one non-blocking nit:
_is_known_model_provider swallowed a transient _is_plugin_model_provider failure
at debug level, so a real plugin-backed provider could briefly vanish from the
picker with no visible log. Promoted to logger.warning(exc_info=True).
Also corrected the endpoint-error-hint comments in ui.js: they cited #4253, which
is actually a cron release (mislabel carried from #4279's body). Comment-only —
no logic change to the browser-verified collapsible-groups code.
Fixes the Photon phantom-providers regression from #4247 (v0.51.461): the
credential-pool detection loop added every auth.json credential_pool key to
detected_providers without checking it names a model provider, so the Photon
iMessage plugin's photon/photon_project/photon_user messaging credentials each
rendered as a phantom provider carrying the full auto-detected model catalog
(251 models on the reporter's machine).
Gated on a new _is_known_model_provider() check (custom:* slug, static
_PROVIDER_DISPLAY/_PROVIDER_MODELS entry, or registered model-provider plugin)
on both the load_pool and raw-dict fallback paths; the group-builder's generic
fallback no longer paints an unrecognized id with the global catalog. Real
pool-keyed providers (copilot, aliased google->gemini) still surface.
Integrates the stronger of two independent same-fix PRs — #4330's implementation
(thorough docstring, try/except-hardened plugin check, preserved #1881 comment,
defensive else branch) + its 8-case test that mocks at the WebUI boundary
(stubs hermes_cli, never imports the bundled agent package that's absent in CI,
exercises both the load_pool and ImportError raw-dict paths) — over #4329's
2-case test. Both rodboev (#4329) and the maintainer agent (#4330) independently
found and fixed the same regression.
Reported by Nic.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Builds on #4279's collapsible-group foundation and extends it into a complete
provider-group UX:
- Groups start COLLAPSED except the group that owns the selected model, so the
picker opens scannable instead of as a long flat list. Search auto-expands
matching groups (and searches hidden overflow models); clearing the search
restores the collapsed-except-selected view.
- 'Show more' restyled as a distinct '+ Show all N models' expander (muted
italic, indented, no model-id line) that reveals overflow IN PLACE — the group
stays open, other groups keep their state, and the view scrolls to the newly
revealed models instead of resetting to the top. Falls back to a full
re-render in minimal/headless DOM. Endpoint-error groups (#4253) and the
expanded group persist correctly across a search round-trip.
- Per-row provider chip suppressed for rows already under their own provider
heading (removes the repetitive 'provider on every row' noise); kept for
hoisted/search rows.
Verified end-to-end on desktop + mobile 390px (collapse/expand, search +
overflow-search + clear-reset + no-results, load-more in-place + stay-open +
scroll-to-new, endpoint-error visibility). #3691 driver tests updated for the
collapsible-group structure + de-noised chip. In-place-path CI coverage tracked
in #4331.
Co-authored-by: akrhin <akrhin@users.noreply.github.com>
Gate follow-ups on the converged head, all fail-closed hardening with no
contradiction to the existing security model:
- _as_posix_path rejects embedded null bytes (returns None).
- _safe_resolve catches ValueError (null-byte .resolve()) -> raw path so the
block gate rejects cleanly.
- _workspace_access_error returns a clean 'Invalid path' validation error on
stat() ValueError instead of an uncaught 500.
- /api/workspaces/add route preflight resolve() now catches
(ValueError, OSError, RuntimeError) -> clean 400.
Reconciliation note: an interim gate suggestion to restore ~user/~root
expansion and block /root was REVERTED — the full suite surfaced
tests/test_batch_fixes.py::TestRootWorkspaceUnblocked, which pins the
deliberate #510/#521 decision that /root is NOT blocked (Hermes runs as root
in many deployments, where /root is the legitimate home, allowed via the home
carve-out). Expanding ~root for a non-root deployment would register root's
home, so the PR's original 'leave ~user literal' behavior is the correct,
safer choice. Kept only the null-byte fixes; added a guard test pinning that
/root stays unblocked.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Normalize both the candidate and terminal.cwd before the remote-POSIX
containment check so `{cwd}/../other/...` lexical escapes are rejected, and
restore the blocked-system-root gate BELOW the home-directory allow-list in
both resolve_trusted_workspace() and validate_workspace_to_add() so valid
systemd-homed paths under /var/home/... stay allowed. Also normalize POSIX
shapes at the source (_as_posix_path) so every downstream block probe is
shape-consistent (/etc/../home/user -> /home/user).
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Sidebar vanished during resilient/slow session loads. Preserve sidebar
fields on resilient loads + add an opt-in api() retry so a slow /api/sessions
shows a retryable error state instead of an empty sidebar.
Maintainer fixes for the 3 review-blocking SILENT regressions:
- Restore read_only / is_read_only / gateway_routing to the sidebar response
allowlist, so read-only (imported CLI + Claude Code) sessions keep their
rename/action-menu/swipe suppression and the gateway model label renders.
(gateway_routing_history intentionally NOT sent — the label reader prefers
the latest gateway_routing, and the full history would bloat each row.)
- Scope-tag the session cache (active profile + all-profiles flag). When a
/api/sessions refresh fails right after a profile switch, the catch path now
clears the stale rows and shows the error state instead of re-rendering the
PRIOR profile's sessions.
Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
Codex gate caught a cross-profile leak: _CREDENTIAL_POOL_CACHE was keyed
by provider id alone, so a custom provider configured under profile A made
profile B report has_key=True from a stale cache hit (then 401 at request).
Key the cache by (active-profile auth-store path, provider id) at all
get/set/evict sites via _credential_pool_profile_tag(). Also make the two
new bare _entry.runtime_api_key reads getattr-safe (OAuth-only entries lack
the attr), matching the guard the author already applied at providers.py.
Adds a cross-profile isolation regression test.
Co-authored-by: akrhin <akrhin@users.noreply.github.com>
Custom/aliased providers with keys in the credential pool (added via
hermes auth add) were reported unconfigured in the WebUI because provider
detection only checked env vars. Add _has_explicit_pool_credentials() that
checks the pool while filtering the ambient gh_cli entry (so Copilot doesn't
falsely show configured on any machine with gh installed), cache-routed to
avoid the cold per-provider load_pool latency, and resolve provider aliases
before load_pool() at every retrieval site so detection and retrieval read
the same pool (glm->zai, github->copilot, etc.). /api/models/live wrapped in
profile scope; getattr-safe runtime_api_key for OAuth-only entries.
Co-authored-by: akrhin <akrhin@users.noreply.github.com>
The /api/transcribe/capability probe's resolve_provider() only knew the
built-in STT providers, so an explicit stt.provider mapping to a Hermes
command-type provider was reported unavailable and the browser fell back
unnecessarily. Add command_provider_available(), delegating to the agent's
_resolve_command_stt_provider_config(provider, cfg) as the single source of
truth, consulted as the final fallthrough after the built-in chain (the
agent rejects built-in names internally, so ordering can't shadow a native
provider). Added a clarifying comment that command providers are
intentionally omitted from the auto-detect tuple to match the agent's
_get_provider() precedence.
Co-authored-by: LLMinem <LLMinem@users.noreply.github.com>
When the agent's role-sequence repair concatenates the prior context-tail
user row with the submitted current turn as <stale>\n\n<current>, that
polluted pair was persisted into both the model context and the visible
transcript. Add a _detect_stale_user_merge detector (requires the literal
\n\n repair boundary + exact tail/current match after sentinel stripping)
and funnel context-merge and display-merge through one
_strip_stale_user_merge_from_messages so a single rule governs persistence.
The strip is scoped to the new-turn candidate slice only — historical rows
in the already-committed previous_context prefix are never rewritten.
Co-authored-by: starship-s <starship-s@users.noreply.github.com>
The backfill inner loop in _merge_display_messages_after_agent_result
re-ran _message_identity (json.dumps) for every future display row on
every outer iteration and used an O(N) list-slice membership test
(in context_keys[_cursor:]), making it O(D²·C). On long tool-call
sessions this pegged a core, held the GIL, and stalled /api/sessions
5-11s — the WebUI appeared hung. Precompute display keys once and keep
a multiset (count-keyed dict) mirror of context_keys[_cursor:] for O(1)
membership → O(D²).
Salvaged from #4314 (the original PR bundled an unrelated, undescribed
HermesWebUIContext feature). Reimplemented the membership mirror as a
count-keyed dict instead of the PR's set: _message_identity returns
DUPLICATE keys for identical-content turns and None for empty rows, so
a plain set diverges from the original list-slice semantics (verified
over 3M adversarial trials); a multiset including None is exact. Added
a differential regression test guarding the equivalence.
Co-authored-by: psanger <psanger@users.noreply.github.com>
start_watcher() ran synchronously in main() at boot; if
_resolve_watcher_target() blocked (locked state.db, slow profile
resolution, I/O stall) the server never bound its port — a startup
brick. Run it on a daemon thread with a 5s join so boot proceeds even
if watcher init stalls; the poll loop already runs on its own daemon
thread so liveness is unchanged.
Co-authored-by: minidarkmimi <minidarkmimi@users.noreply.github.com>
Combined light-UX release NW. Both rebased onto current master (were 96 and
247 behind; net 3-dot diffs applied, byte-identical to PR heads). Both reuse
existing renderers (chat image lightbox / chat CSV table renderer).
Review fix (#4025, Codex): the CSV table preview previously stripped the
workspace Edit affordance .csv had when it fell through to the code preview.
Restored: csv mode is editable, renderCsvPreviewContent caches raw content for
the edit textarea, and saving an edited CSV re-renders the table (not markdown).
Verified the full edit round-trip live + regression test.
Co-authored-by: Rod Boev <rod.boev@gmail.com>
In Transparent Stream mode a live multi-round turn (narrate -> tools ->
narrate -> tools) rendered all assistant prose bunched at the top with
every tool/thinking row clustered below; it self-healed only once the
turn settled.
_syncLiveWorklogReasonsForAnchor() runs on every live segment render
(_flushPendingSegmentRender + the RAF _doRender in messages.js) and
unconditionally built the top-anchored live-worklog rail, mirrored each
round's prose into a wl-reason row there, and hid the real inline
assistant-segment via assistant-segment-worklog-source (display:none).
That prose-folding is the Compact Worklog presentation (#3401) and must
not run in Transparent Stream mode.
Gate the helper on isCompactWorklogMode() so transparent mode keeps prose
as visible, chronologically-placed inline segments interleaved with tool
rows (matching the already-correct settled render). Compact Worklog
folding is unchanged. Verified in an isolated mock-LLM env via an
on-screen visual-position probe (before: textAfterFirstTool=0,
allTextAboveAllTools=true; after: textAfterFirstTool=3,
allTextAboveAllTools=false) and confirmed compact_worklog still folds.
Adds a behavioral regression test asserting the gate is the first
statement in the helper (precedes the rail build), red/green validated.
Absorbs contributor PR #3544 (@rodboev, closes#3340) with two fixes:
1. DETECTION VOCAB (would never fire): the original gated on action names
{save,create,update,upsert}, which don't match the real agent tool enums —
memory.action is add|replace|remove, skill_manage.action is
create|patch|edit|delete|write_file|remove_file. Split into per-tool
predicates with the correct vocabularies: _isMemorySave gates memory on
{add,replace}; _isSkillUpdate gates skill_manage on {create,patch,edit,
write_file}. Deletions excluded so the saved/updated verbs stay accurate;
running/errored excluded.
2. SNAPSHOT/RESTORE PERSISTENCE (Codex catch): classification lived only on the
row._tcData JS property, which does NOT survive the outerHTML/innerHTML
snapshot+restore the live tool-call group uses on session switch/restore —
a restored memory/skill row would be re-counted as a generic tool and the
suffix would silently vanish. buildToolCard now also stamps durable
data-memory-save / data-skill-update attributes, and _syncToolCallGroupSummary
counts them as a fallback when _tcData is absent. Verified live across a real
outerHTML round-trip: label identical before/after.
Replaces the PR's static source assertions with a node-driven behavioral test
(11 cases) covering the real action vocabularies, exclusions, case-insensitivity,
null-arg safety, and the durable-attribute persistence guard.
Co-authored-by: rodboev <rodboev@users.noreply.github.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>
Closes#2771.
v0.51.117 (PR #2766) introduced a top-level function _inflightStateLimits()
in static/ui.js that collided with the window._inflightStateLimits config
object set in static/boot.js. Because top-level function declarations in
classic (non-module) scripts attach to window, boot.js's assignment
overwrote the function reference, and every later _inflightStateLimits()
call threw TypeError. _compactInflightState() runs on every send(), so
no new chat session could be created — v0.51.117 is effectively unusable.
Reported by @jahilldev, with multiple users (@isma3iloiso, @theDanielJLewis,
@JHVenn) confirming the bug or reverting to v0.51.116.
Fix: rename the function to _getInflightStateLimits() — the window-attached
config key stays under its original name (unchanged for any downstream
code that reads it). Updates all 4 call sites in static/ui.js.
Tests:
- Update tests/test_inflight_storage_quota.py — the existing test
asserted 'function _inflightStateLimits()' in UI_JS as a positive
presence check, which certified the bug. Now asserts the renamed
function name is present AND the old colliding name is absent AND
no stale call sites remain.
- Add tests/test_window_function_collision.py — generalized regression
that scans every static JS file for top-level function declarations
whose name also appears as the target of 'window.X = {...}' or
'window.X = <number>'. This is the exact shape that broke #2715
(_pinnedSessionsLimit in v0.51.106) and #2771. Test fails with a
precise diagnostic naming the file and symbol if the bug class
returns. Confirmed test FAILS on current master (unfixed) and PASSES
on this branch.
Verified end-to-end against the live browser before commit:
- typeof window._inflightStateLimits === 'object' (config preserved)
- typeof window._getInflightStateLimits === 'function'
- _getInflightStateLimits() returns the limits object
- saveInflightState() persists to localStorage without throwing
Full pytest suite: 6308 passed, 6 skipped, 3 xpassed, 8 subtests passed.
Opus advisor: SHIP.
Without --force, git fetch origin --tags refuses to overwrite divergent
local tags and returns 'would clobber existing tag', jamming the entire
WebUI update path indefinitely. The WebUI is a release-tracking consumer
that never pushes tags, so it should always defer to whatever the remote
says a release tag points to. Add --force to all three fetch-tag call
sites:
- _check_repo (the 'Check now' button + periodic check)
- apply_force_update (force-reset to remote HEAD)
- apply_update (stash + pull --ff-only)
Tests:
- Updated 3 existing tests in test_updates.py whose fake_git mocks
asserted the exact ['fetch', 'origin', '--tags'] args list.
- Updated 1 existing test in test_update_banner_fixes.py that asserted
the same shape for apply_update.
- Added 4 new regression tests:
- test_check_repo_fetches_tags_with_force
- test_apply_force_update_fetches_tags_with_force
- test_apply_update_fetches_tags_with_force
- test_check_repo_recovers_from_remote_retag (end-to-end,
proves the bare --tags fetch shape is no longer used)
Closes#2756.
Opus advisor review of stage-375 flagged that the protected-bracket set including `<` and `>` caused tables containing comparison operators across adjacent columns to mis-collapse: `| x < 5 | y > 10 |` matched `< ... >` as a bracket pair and stashed the inner pipe, producing one cell instead of two.
Real LLM table output uses angle brackets as comparison operators far more often than as content-grouping pairs, so the safer default is to NOT treat them as a matched pair. Dropped `<` from the opener class and `>` from both closer classes.
Three regression tests added (`TestComparisonOperatorsAcrossColumns` class): `< … >` across columns, `<` alone, `>` alone.
PR #2428's iterative _protectPipes regex introduced two issues we caught during stage assembly:
1. The negated character classes [^)\]}'>] added `'` as a stop character. That breaks cells containing string-literal pipes like `('a'|'b')` (Python type-union examples) — they would still mis-split. Dropped the apostrophe-stop.
2. The literal `}` inside the regex character classes confused the brace-counting extractFunc driver in tests/test_renderer_js_behaviour.py, breaking all 45 existing node-driven renderer tests. Rewrote both brace literals as hex escapes (\\x7b and \\x7d) — semantically identical at the regex-engine level but the JS source carries no bare brace glyph.
Also added tests/test_issue2428_table_pipe_protection.py with 9 regression tests covering single-pipe, multi-pipe-in-brackets, apostrophes-with-pipes, and the KaTeX \$...\$ guard.