Commit graph

2923 commits

Author SHA1 Message Date
luandnh
dba6340661 test: add regression test for pipe inside inline code in renderMd tables 2026-06-25 08:25:58 +00:00
Frank Song
5f17ba55ed Fix gallery-installed extension activation 2026-06-25 07:10:17 +00:00
nesquena-hermes
89d7fae347 fix(#4622): restore full body for CAPPED-PREVIEW settled snippets, not just empty
Codex gate caught that the enrich only restored when the settled snippet was
fully EMPTY — but the backend persists a 4000-char capped preview
(_TOOL_RESULT_SNIPPET_MAX), so long terminal/tool output settles to a truncated
4000-char prefix, not empty (the actual #4622 symptom). Now also restore when
the settled snippet is a bounded preview of the live one (live longer + settled
is a prefix + settled length >= the 4000 cap), without clobbering a genuinely
short non-preview value. Adds capped-preview restore + short-prefix-not-clobbered
tests (proven non-vacuous).
2026-06-25 06:51:33 +00:00
nesquena-hermes
ef3092434f fix(#4622): restore tool-card result body on the settled rebuild path
In the Transparent Stream / Worklog view, terminal tool cards lost their stdout
and patch/edit cards lost their diff AFTER a turn settled (live cards were fine).

Root cause (proven via a node harness driving the real settled chain
_anchorSceneRowsByMessageIndex -> _anchorSceneToolRowFromCall ->
_anchorSceneToolCallFromRow): the settled rebuild builds each tool row from the
persisted messages[].tool_calls (state.db/sidecar), which can carry only a short
preview — or, on a cold/paginated load, nothing — for the result body. The full
output lives on the live S.toolCalls entry at settle time, but the dedup loop in
_anchorSceneRowsByMessageIndex SKIPPED the matching live entry (continue) instead
of restoring the missing body onto the surviving settled row. So buildToolCard
got tc.snippet='' -> empty output region, and the patch/edit diff (detected from
the snippet) never rendered. This also blanks the transparent 'Output' tab and
the 'Full' (detailed-input) tab.

Fix: new _enrichSettledToolRowBodyFromLive() restores snippet/result/output,
command, and args onto a settled row from the matched live call, ONLY when the
settled value is empty (never clobbers a genuinely persisted body). Wired into
the dedup loop: on a tid match that would have been skipped, enrich the existing
pooled row instead of dropping the live data.

The prior #4625 guard only exercised the LIVE buildToolCard path, which is why it
stayed green while the settled path regressed. New
tests/test_issue4622_settled_toolcard_body.py drives the full SETTLED chain and
asserts: terminal stdout restored + full output in data-full + transparent Output
tab non-empty; patch diff body restored + renders as a diff + Full-tab args kept;
genuine persisted body not clobbered; args restored when dropped; no-live-match
leaves the row untouched; no duplicate row after enrich. Verified to FAIL on clean
master (assertions) and pass with the fix. Also registered the new helper in the
and now references the new symbol).

Reusable proof harness saved to ~/WebUI/qa/harnesses/.

Closes #4622.
2026-06-25 06:40:27 +00:00
nesquena-hermes
856aa1d48d fix(#4891): show header in read-only Memory sections + Profiles help view
Gate (Codex) caught two SILENT regressions in the original fix: explicitly
hiding the header in the 'else'/'empty' branches also hid populated read views.
- _setMemoryHeaderButtons('read') now shows the header for ALL read views
  (incl. read-only Project Context / External Notes), gating only the Edit btn.
- _renderProfileConceptHelp() now uses a new 'help' header mode (shows header,
  hides all action buttons) instead of 'empty' (which hid the help title).
Adds tests/test_issue4891_detail_header_visibility.py (proven non-vacuous).

Co-authored-by: luandnh <luandnh@users.noreply.github.com>
2026-06-25 06:20:20 +00:00
nesquena-hermes
f1fee4ec7e test: remove unused 'rules' var (ruff F841 forward-gate) 2026-06-25 05:51:09 +00:00
nesquena-hermes
8e3cce8025 test: class-level regression guard for the #4856 mobile-transcript-collapse family
#4856 (mobile transcript completely unscrollable) survived multiple fixes
because regressions were guarded only by grepping one exact CSS string. This
adds an INVARIANT guard so the whole class can't recur: .messages-inner (base +
every media query) must never become a Y-axis scroll container — neither via an
explicit overflow-y scroll value nor via overflow-x:hidden/scroll/auto (which
the CSS spec coerces overflow-y->auto, collapsing min-height:auto to 0 inside
the flex column). overflow-x:clip is the only allowed horizontal clip. Also
asserts .messages stays the real scroller (overflow-y:auto + min-height:0).
Proven non-vacuous: reintroducing overflow-x:hidden on the mobile rule fails it.
For a true runtime check at a real viewport, the private workspace has
scripts/layout_scroll_probe.py.
2026-06-25 05:47:08 +00:00
nesquena-hermes
3acd802588 fix(#4856): mobile transcript collapses unscrollable — overflow-x:hidden coerces overflow-y:auto
The mobile .messages-inner rule used overflow-x:hidden, which per the CSS
overflow spec coerces overflow-y from visible to auto — turning the inner
into a scroll container. A scroll container's min-height:auto resolves to 0,
so the inner (a flex-shrink:1 item inside the .messages column flexbox)
collapsed to the viewport height instead of growing to its content. The
transcript then overflowed the clipped inner rather than the .messages
scroller, leaving nothing to scroll and pinning scrollTop at 0 — every
open/send/receive stranded mobile users at the top with no way to scroll down.

Fix: overflow-x:clip suppresses horizontal overflow (the original #4583/#4553
intent) WITHOUT coercing overflow-y or creating a scroll container, so
min-height:auto stays min-content and the inner grows to full height.

Verified empirically via headless Chrome mobile emulation (390x844, mobile=true):
scrollableDelta 0 -> 7746; open/send/receive all land at bottom; mid-scroll
position preserved across re-render. Desktop unaffected.

Updated #4553 test (clip satisfies its horizontal-pan goal) + added dedicated
2026-06-25 05:18:32 +00:00
nesquena-hermes
b149a975c6 fix(#4842): update #4385 archive tests to real sidecar files + document stat-gate assumption
Opus gate finding (BLOCKING): the two #4385 archive-reappears regression tests
mocked Session.load_metadata_only to return an archived stub WITHOUT writing a
real sidecar file, and never set SESSION_DIR. The new stat-gate in
_state_projection_sidecar_metadata short-circuits on SESSION_DIR/{sid}.json
before consulting metadata, so those mocks were never reached -> archived=False.

This is NOT a production regression (in prod, load_metadata_only reads exactly
SESSION_DIR/{sid}.json and returns None when absent, so archived metadata only
exists when the file does — in which case the stat() succeeds and behavior is
identical). The divergence existed only at the tests' mock boundary.

Fix: both tests now write a real archived sidecar JSON under a monkeypatched
SESSION_DIR, exercising the actual production read path (strictly better
coverage than mocking load_metadata_only). Also documented the
SESSION_DIR-as-sole-source assumption on the cache function per Opus's
forward-looking note.
2026-06-25 04:56:00 +00:00
nesquena-hermes
a6d8319acb perf(#4842): cache sidebar-projection sidecar reads + hoist per-row I/O on the cron path
GET /api/sessions pinned CPU to 100% and took multiple seconds on profiles with
many cron sessions. The state.db -> sidebar projection did three pieces of
redundant per-row file I/O, in BOTH the visible pass and the higher-capped
(CRON_PROJECT_CHIP_LIMIT=200) cron-only second pass:

1. _state_projection_sidecar_metadata(sid): uncached open() + 64KB prefix read +
   JSON-key scan, once per row.
2. get_last_workspace(): up to two file reads + an is_dir()/remote probe,
   returning the SAME workspace for every row.
3. cron/jobs.json: full read + parse per untitled cron row.

On a cron-heavy install that is hundreds of file reads per build, and because
the enclosing _CLI_SESSIONS_CACHE is keyed on a state.db content fingerprint
that advances on every streamed message row, the whole scan was re-paid on
essentially every 5s poll during a live turn (the recurring 100% CPU / slow
/api/sessions from #4672 -> #4808 -> #4842).

Fix:
- Memoize _state_projection_sidecar_metadata by the sidecar's
  (path, mtime_ns, size, ctime_ns) stat signature in a bounded module cache, so
  a warm build re-stats instead of re-reads. A rename/archive/edit bumps the
  signature and invalidates just that one entry (no staleness). This survives
  the 5s CLI-cache busting during streaming, which is the actual trigger.
  Mirrors the Claude Code parse cache (#4718). Folded into
  clear_cli_sessions_cache() for explicit-reset/test parity.
- Build-scoped memos for cron jobs.json (parse once -> job_id->name map) and
  get_last_workspace() (resolve once), matching the existing _cron_pid() pattern.

Output is byte-identical. Adds tests/test_issue4842_cron_projection_perf.py
(7 tests: warm-rebuild no re-read, stat-change invalidation, dict-copy isolation,
missing-sidecar default, jobs.json once, workspace once, lifecycle clear).
2026-06-25 04:56:00 +00:00
Rod Boev
ee7ff6ed33 fix(extensions): harden gallery against live registry and security review findings 2026-06-25 04:21:50 +00:00
Rod Boev
ddd5c8d936 feat(extensions): add gallery browse, install, and uninstall (#4746) 2026-06-25 04:21:50 +00:00
nesquena-hermes
7b7302bc88 fix(#4853): remove unused dataclass import (ruff F401 forward-gate) 2026-06-25 04:17:05 +00:00
Brett Yelverton
b8cf8dfc4e test(kanban): pin unassigned lane visibility in profile view
Adds three static regression tests:
- profile lanes create an explicit Unassigned lane via _kanbanLaneKey
- hidden-by-filters UX exposes clearKanbanFilters and the right i18n keys
- sidebar meta always shows the unassigned label for tasks without assignee

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 03:54:32 +00:00
Brett Yelverton
8170acafb0 test(kanban): board payload includes unassigned ready tasks
Regression test that verifies the bridge returns unassigned Ready tasks
in the ready column when no assignee filter is active. This confirms the
API side is not the cause of the Dynasty board mobile visibility bug.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 03:54:32 +00:00
Rod Boev
c11049646f fix: use real CSS tokens for recovery bar and update stale test assertion 2026-06-25 03:23:36 +00:00
Rod Boev
764d5b8601 fix(#4749): show steer failure reason and offer retry/dismiss recovery 2026-06-25 03:23:36 +00:00
Rod Boev
c109ed9113 fix(plugins): align activation-state sort with badge precedence (#4713) 2026-06-25 02:57:06 +00:00
Rod Boev
7c1cd99961 fix(#4713): dark-mode disabled badge contrast and active-first plugin sort 2026-06-25 02:57:06 +00:00
nesquena-hermes
d61ca37bd1 fix(#4718): harden parse-cache key with ctime_ns + preserve epoch-0 mtime fallback
Dual-gate (Codex + Opus) findings on the Claude Code transcript parse cache:

1. Cache-key staleness hardening: add st_ctime_ns to the (path, mtime_ns, size,
   max_messages) key so a rare same-size, same-mtime in-place edit can't serve a
   stale parse. Size is already the strong discriminator for append-only JSONL;
   ctime_ns is cheap defense-in-depth (a spurious bump costs one harmless re-parse,
   never a stale read).

2. Timestamp-fallback bug-for-bug fix: the row-builder guard used identity
   (first_ts is None and last_ts is None) but the field assignment uses truthiness
   (first_ts or last_ts or _mtime). They disagreed on a falsy 0.0 timestamp
   (epoch-0/1970 transcripts), leaving created_at/updated_at/last_message_at = None
   instead of the file mtime the pre-cache inline code fell back to. Guard now uses
   the same 'not first_ts and not last_ts' test.

Adds 3 regression tests: epoch-0 mtime fallback, the shared-dict read-only
contract (pins the load-bearing invariant), and same-size+mtime ctime invalidation.
2026-06-25 02:46:00 +00:00
nesquena-hermes
fdb734a6a0 perf(#4718): cache Claude Code transcript parses on the sidebar path
The /api/sessions cold build (which a profile switch triggers) was
dominated by get_claude_code_sessions() re-parsing every Claude Code
JSONL transcript line-by-line on each call — 650-1000ms on a 200-file
/ ~130MB tree, repeated on every switch, every 5s CLI-cache expiry, and
every sidebar poll, because the directory is global but the higher
session cache is keyed per active profile.

Memoize the per-file parse by (path, mtime_ns, size) so a warm build
re-stats files instead of re-parsing them; any genuine edit/append
changes the stat signature and invalidates just that file. Also hoist
the loop-invariant get_last_workspace() out of the Claude Code row loop
(it stats config + probes terminal cwd, ~200 redundant calls per build).

Measured /api/sessions cold-rebuild on a 250-session profile: ~670-880ms
-> ~100-200ms (verified in-browser). Output byte-identical.

Credit @rodboev for the browser-level profiling that isolated the real
bottleneck (the prior pre-warm hypothesis could not move it).

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-25 02:45:59 +00:00
nesquena-hermes
0a6947c2d2 fix(#4754): namespace dismissed approvals by session (gate finding)
Codex CORE: the dismissed-set was keyed by bare approval_id, but gateway/run
sources preserve externally-supplied IDs without session namespacing — so two
sessions carrying the same approval_id collide and dismissing in session A hides
session B's still-pending approval. Fix: _approvalDismissKey(sid, approvalId)
builds a session-namespaced composite key, threaded through _isApprovalDismissed,
_markApprovalDismissed, _unmarkApprovalDismissed + all 5 call sites (showApprovalCard,
dismissApprovalCard, respondApproval, no-pending poll cleanup). Updated 4 stale
assertions to the namespaced signatures + added a functional node-driver
cross-session-collision regression (proven non-vacuous).

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-25 02:15:47 +00:00
Rod Boev
0258599a46 fix(approval): clear pending attention state on dismiss (#4754) 2026-06-25 02:04:51 +00:00
Rod Boev
2764e28b95 fix: remove unused json and re imports 2026-06-25 02:04:51 +00:00
Rod Boev
820362ca14 fix: scope un-dismiss to session-local pending, not global _approvalCurrentId 2026-06-25 02:04:51 +00:00
Rod Boev
2956804a14 fix(#4754): persist approval card dismissal across tab switches and restarts 2026-06-25 02:04:51 +00:00
nesquena-hermes
5296b15956 fix(session): preserve 4096-CHAR prefix semantics in bounded sidecar read (gate finding)
Codex reproduced a SILENT regression: the PR's 4096-BYTE read could miss the
continuation marker the old read_text()[:4096] (4096 CHARS) found. Session.save()
writes compression_anchor_summary (ensure_ascii=False) BEFORE parent_session_id,
so a multi-byte emoji summary puts the marker at char offset ~1230 but byte
offset ~4900 — within the old 4096-char window, past a 4096-byte one → continuation
child no longer detected. Fix: read 16384 bytes (4096 chars worst-case for 4-byte
UTF-8) then slice [:4096] chars, keeping the same needle + bounded I/O. Multibyte
regression test added + proven non-vacuous; updated the read-size assertion to <=16384.

Co-authored-by: starship-s <starship-s@users.noreply.github.com>
2026-06-25 01:49:11 +00:00
Hermes Agent
ddec907a03 test(session): delegate wrapped file context entry 2026-06-25 01:40:22 +00:00
Hermes Agent
dd67f342d7 fix(session): bound continuation fallback sidecar reads 2026-06-25 01:40:22 +00:00
Rod Boev
7873e7ccf0 test(#4875): update live-stream-ux assertion to match widened filter 2026-06-25 01:21:08 +00:00
Rod Boev
36742abcee fix(#4875): widen continuation-prompt filter to cover output-length and tool-call variants 2026-06-25 01:21:08 +00:00
Rod Boev
0668cfa77c fix(#4856): suppress Chromium scroll-anchor re-selection during mobile DOM rebuild 2026-06-25 01:06:09 +00:00
nesquena-hermes
97372c8207 test(#4783): remove un-portable node_modules-prune filesystem test
The buried far-future mtime kept bleeding into the probe value on CI's shard-0
runners despite repeated attempts to make it deterministic. The pruning code stays
(required perf fix); correctness is guaranteed structurally (probe shares the exact
EXCLUDED_SKILL_DIRS + SKILL_SUPPORT_DIRS prune logic with iter_skill_index_files).

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-24 22:07:40 +00:00
nesquena-hermes
87782a9d68 test(#4783): pin legitimate tree mtime so the prune test is filesystem-timing independent
CI shard-0 still flaked: float int(far_future*1e9) rounding left probe ~38ns above
the threshold because the legitimate vendored/ tree's mtime on CI tracked the
buried-tree creation. Now pin skills_dir/skill/SKILL.md/config.yaml to a known
modest mtime AFTER building the buried far-future node_modules subtree, and assert
probe < far_future - 1s. Deterministic across filesystems; still non-vacuous
(fails with prune disabled even when node_modules itself is stamped far-future).

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-24 22:03:15 +00:00
nesquena-hermes
41687073d6 test(#4783): make node_modules-prune test deterministic (CI fix)
The before/after assertion was fragile — creating node_modules between the two
probe calls bumped the (non-pruned) vendored/ ancestor mtime, so 'after==before'
failed on CI's filesystem timing. Rewrote to a timing-independent assertion: a
FAR-FUTURE mtime buried inside the pruned node_modules/ must NOT appear in the
probe value (probe < far_future_ns), which holds even if node_modules itself is
bumped, because the prune removes it from dirnames before the stat loop.

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-24 21:56:26 +00:00
nesquena-hermes
af1649f558 test(#4783): update test_skills_stats_cache to assert the fixed mtime-probe behavior
The pre-existing test asserted the OLD bug: 'add a skill within the 8s TTL ->
still get stale (1,1)'. That stale window is exactly what #4783 fixes — adding a
skill bumps the skills-dir mtime, the per-call probe catches it, and counts now
update immediately (2,2). Updated the assertions to the corrected contract +
added a no-change-serves-cache step. (Test lived in test_profile_skills_stats.py,
not the PR's test_issue4783 file, so the contributor missed it.)

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-24 21:41:15 +00:00
nesquena-hermes
3fbc7d71d3 fix(#4783): prune dependency trees in mtime probe to match compute path (gate finding 4)
Codex round-4 found my followlinks=True change introduced a perf regression: the
probe os.walk lacked the .git/.venv/node_modules/site-packages + skill-support-dir
pruning that iter_skill_index_files applies, so a skill vendoring a dependency
tree made this EVERY-CALL probe walk thousands of irrelevant files — defeating the
cache's whole purpose. The probe now prunes dirnames[:] with the same
EXCLUDED_SKILL_DIRS + SKILL_SUPPORT_DIRS sets as the compute path. Regression test
(change deep inside node_modules/ must not move the probe) added + non-vacuous.

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-24 21:27:22 +00:00
nesquena-hermes
e4e3b46830 fix(#4783): mtime probe follows symlinked skill dirs to match compute path (gate finding 3)
Codex round-3 reproduced: _skill_tree_max_mtime_ns used os.walk(followlinks=False)
while the compute path iter_skill_index_files uses followlinks=True, so an edit to
a symlinked skill's target SKILL.md was invisible to the probe (stale up to TTL).
Added followlinks=True to the probe walk so it descends symlinked skill dirs and
sees target mtime changes. Regression test added + proven non-vacuous.

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-24 21:21:57 +00:00
nesquena-hermes
8a46d88daf fix(#4783): TTL expiry always recomputes (safety net for mtime-preserving changes) (gate finding 2)
Codex re-gate: my prior 'TTL-expired + unchanged-mtime -> refresh TTL, no recompute'
branch could leave counts stale INDEFINITELY for an mtime-PRESERVING out-of-band
change (e.g. a git checkout restoring the old mtime) — worse than master's 8s
recompute. Codex proved it (cached (2,2) stayed (2,2) while a fresh compute returned
(1,2)). Now: within TTL + unchanged mtime serves cached (perf win kept); mtime
changed OR TTL expired both force a full recompute (the TTL is a genuine safety
net). Updated the row-2 test to assert the safety recompute.

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-24 21:16:16 +00:00
nesquena-hermes
7d38b0cd9f fix(#4783): probe skill mtime on every call so out-of-band changes aren't hidden for the full TTL (gate finding)
Codex gate found a SILENT regression: the cache returned on a zero-I/O fast path
while inside the 300s TTL, so _skill_tree_max_mtime_ns never ran during the TTL
window and an out-of-band (CLI/git) skill create/edit/delete was invisible for up
to 5 minutes — WORSE than master's ~8s stale window. Restructured so the cheap
stat-only mtime probe runs on EVERY call (prompt change detection); the TTL now
only bounds the expensive SKILL.md recompute. Unchanged mtime still serves cached
+ refreshes TTL with no recompute (the perf win is preserved). Updated 2 tests
that encoded the flawed zero-I/O design + added a within-TTL-change regression
(proven non-vacuous).

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-24 21:08:19 +00:00
Rod Boev
fdf9adbd02 Catch nested skill deletions in the mtime probe 2026-06-24 20:52:31 +00:00
Rod Boev
9ce8deff5d fix: remove unused importlib and os imports 2026-06-24 20:52:31 +00:00
Rod Boev
970345f74d fix: restore sys.modules after test to prevent api package pollution 2026-06-24 20:52:31 +00:00
Rod Boev
18528a88f0 perf(#4783): two-tier mtime cache for profile skills stats 2026-06-24 20:52:31 +00:00
Rod Boev
2ca65abefa fix: use JSON.parse for bracket-prefixed lines instead of blanket suppression 2026-06-24 20:35:04 +00:00
Rod Boev
4bf659e433 fix: harden test drivers against extractFunc brace-counter overrun 2026-06-24 20:35:04 +00:00
Rod Boev
32d171c1a1 fix: add _toolResultOneLiner to sibling test extractors and pin error-snippet behavior 2026-06-24 20:35:04 +00:00
Rod Boev
29604e5e7b fix: fall back to tc.snippet for cold-loaded tool results 2026-06-24 20:35:04 +00:00
Rod Boev
c19aaae27f fix(#4752): show tool result instead of call args in collapsed card preview 2026-06-24 20:35:04 +00:00
nesquena-hermes
57a78e9284 fix(#4728): clear stale custom:* base_url + default wins without loaded session (gate findings)
Two gate-found CORE/SILENT edges fixed on-branch:
- api/config.py: switching the main default to a different custom:* provider kept
  the OLD provider's base_url, routing New Chat to the wrong endpoint. The
  base_url-clear on provider-change now covers custom:* too (was exempted).
- static/sessions.js: newSession() skipped the configured default when S.session
  was null but the picker held stale state (e.g. after deleting the last session),
  inheriting the stale model. Default now wins over legacy picker regardless of
  loaded-session state (explicit empty-composer override still takes priority).
- Updated 2 stale source-grep precedence assertions + widened a brittle 6000-char
  newSession() slice window to 7000 (assignment block grew; intent unchanged).
- 2 new non-vacuous regression tests (custom base_url clear; precedence).

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-24 20:17:29 +00:00