05ab4173cfefac6e77280af2aa38cffc4c9dabe0
2882
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6c0f2c765c |
fix(export): resolve session exports against document base URI (#7189)
* fix(export): resolve session exports against document base URI * test(export): cover subpath deep links --------- Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com> |
||
|
|
65c185478c |
fix(ui): suppress false loopback-only warning when public URL is configured (#6573)
* fix(ui): suppress false loopback-only warning when public URL is configured When a user configures status.browser_url (e.g., via a reverse proxy at https://dashboard.example.com), the Dashboard link correctly opens that public URL, but _applyDashboardStatus() still displayed the 'loopback-only' warning because it only checked the browser's hostname. Fix: suppress the warning when status.browser_url is present — the user has explicitly configured a public URL, so the loopback bind is intentional and the warning is a false positive. Closes #6459 * fix(ui): base loopback warning on resolved dashboard target hostname (#6573) * fix(ui): complete loopback classifier for dashboard warning (#6573) --------- Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com> |
||
|
|
eee72f942e |
fix(workspace): new chat inherits the current conversation's workspace (#7180)
* fix(workspace): new chat inherits the current conversation's workspace
`newSession()` resolved the workspace of a new chat as:
switchWs || S._profileDefaultWorkspace || S.session.workspace
`S._profileDefaultWorkspace` is global to the profile, hydrated from
`/api/profile/active`. Preferring it over the current conversation meant a new
chat opened from a MES conversation landed on the profile-global value instead
of MES. With several conversations running in parallel on different workspaces,
the context the user was actually in was systematically lost.
The precedence is worse than it looks, because that "profile default" is not a
stable configured value: `get_profile_default_workspace()` reads
`last_workspace.txt` BEFORE the configured key (api/workspace.py ~419-427), and
for the `default` profile that file is the GLOBAL one. `set_last_workspace()`
rewrites it on every `/api/chat/start` and `/api/session/update` of ANY
conversation. So "profile default wins over the current session" degrades in
practice to "whatever workspace another conversation last used wins over the
conversation I am in" -- observed live: the pointer flipped mid-operation
because a concurrent turn wrote to it.
New precedence:
switchWs || S.session.workspace || S._profileDefaultWorkspace
Preserved invariants:
- the one-shot profile-switch flag still wins first and is still consumed (#823);
- a blank page (no loaded session) still falls back to the profile default,
which remains required for the empty-composer workspace chip (#804/#5169);
- `_profileDefaultWorkspace` is never nulled by `newSession()` (#823).
This deliberately diverges from upstream #4755, which made the profile default
win. That decision assumed a stable configured default; on a shared/global
`last_workspace.txt` it inverts its own intent. The upstream test is rewritten
with the rationale inline so a future rebase does not silently restore the bug,
plus a new test that evaluates the real expression under node and proves the
blank-page fallback still holds.
Pre-existing failures in tests/test_workspace_stale_recovery.py (3) were
verified identical with and without this change.
* fix(workspace): recover stale inherited workspace
---------
Co-authored-by: Anthony Ruiz <ruizanthony@users.noreply.github.com>
|
||
|
|
0b5d2f3698 |
fix(clarify): honor agent.clarify_timeout and support unlimited wait (#7163)
The WebUI clarify bridge resolved the prompt timeout from a local, divergent copy (legacy clarify.timeout, 120s fallback) and treated 0 as invalid, so agent.clarify_timeout: 0 (unlimited) could not be honored and there was no way to make a clarification card wait for the user. Resolve the timeout with the agent core's canonical resolve_clarify_timeout() (clarify.timeout -> agent.clarify_timeout -> 3600, <= 0 = unlimited) against the session's own profile config (the streaming worker thread does not inherit the request's per-profile context), wait with a null deadline when unlimited, and stop the frontend rendering an expired countdown for non-positive timeouts. - api/streaming.py: per-session profile config resolution, canonical resolver with a compatibility fallback, and an extracted _await_clarify_response() helper that polls in 1s slices and honours cancellation. - api/clarify.py: preserve timeout_seconds <= 0 and emit no expires_at. - static/messages.js: _clarifyExpiryMs() returns 0 for non-positive timeouts so the card stays up without a countdown. - tests/test_sprint42.py: resolution matrix, per-session profile divergence, unlimited resolve-later / cancel-exit, and no-expiry payload. Closes #7159 Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com> |
||
|
|
4528279cb2 |
fix: route overlapping custom_providers models by active endpoint (#7107)
* fix: route overlapping custom_providers models by active endpoint When two custom_providers[] entries list the same bare model id (e.g. dogapi and packyapi both advertising 'claude-sonnet-5'), resolve_model_provider scanned them in config write order and returned the first match, silently ignoring the active provider's base_url. A user whose model.base_url pointed at packyapi got routed to dogapi because dogapi appeared earlier in config. Now, when the active provider resolves to a named custom provider (custom:<slug>, including via model.base_url -> named-slug matching), that provider claims models it owns before the ordered scan runs. Falls through to the legacy first-match scan when the active provider is bare 'custom' with no base_url to disambiguate, so existing single-endpoint setups are unaffected. Adds 3 regression tests covering: shared-model active-endpoint wins, provider-unique model still routes by ownership, and bare-custom no-base_url preserves legacy order. * fix: extend active-endpoint routing to aux, handoff, and slug-collision paths Review follow-up: the overlapping-id fix must reach three sibling resolution paths that still routed on the wrong endpoint. 1. Auxiliary slot base_url (config.py set_auxiliary_model): a bare resolve_model_provider(model) ignored the SELECTED aux provider, so a custom:A slot was persisted with the active custom:B endpoint when both listed the model. Look up the selected provider's own custom entry via resolve_custom_provider_connection. (NOT model_with_provider_context here: the @custom:<slug>:model form resolves base_url to None, which would drop the base_url entirely.) 2. Handoff summary (routes.py): read s_obj.model_provider and pass model_with_provider_context(model, model_provider) so a session pinned to custom:A is not rerouted to the active custom:B. base_url is backfilled from the provider's own custom entry downstream. 3. Normalized-name slug collision (config.py resolve_model_provider): two distinct provider names can normalize to the same slug (e.g. "Foo Bar" and "foo-bar"). The active-slug guard now matches the EXACT entry by base_url instead of re-deriving it from the non-unique slug, and fails closed (falls through to the ordered scan) on genuine ambiguity. Adds 4 regression tests: normalized-slug base_url disambiguation, slug-collision fail-closed, aux-slot selected-provider base_url, and session-provider-context handoff routing. * fix: fail closed on custom-provider slug collisions and break aux deadlock resolve_model_provider() returns a provider slug (custom:<slug>) and the credential lookup (resolve_custom_provider_connection) later resolves the API key from the FIRST same-slug entry regardless of base_url. So when two distinct provider names normalize to the same slug (e.g. "Foo Bar" and "foo-bar") and both list the requested model, returning the slug silently pairs one entry's endpoint with another entry's credential. Now raise a clear AmbiguousCustomProviderError instead of guessing, on both the active-slug guard and the ordered-scan fall-through paths. Also: an explicit, unambiguous named custom provider now stays authoritative even when model.base_url is stale/absent and points at neither entry — a stale URL is no longer evidence to demote it to config order. And fix a self-deadlock in set_auxiliary_model(): it held the non-reentrant _cfg_lock while calling resolve_custom_provider_connection() -> get_config() -> reload_config_if_stale(), which re-acquires the same lock. The selected custom provider's base_url is now resolved inline from the config_data already loaded under the lock. Tests: collision cases now assert the ambiguity error; added a stale-URL explicit-provider regression and a watchdog-guarded aux no-deadlock regression (both fail before the fix). Co-authored-by: Cursor <cursoragent@cursor.com> * fix: fail closed on custom-provider slug collisions across all four paths The previous cut only guarded the symmetric bare-model case, and it built collision membership from entries that OWN the requested model. But credential resolution scans ALL same-slug entries and first-matches regardless of ownership, so several slug-only boundaries could still pair one entry's endpoint with another entry's credential — including the asymmetric case where only one colliding entry lists the model. Introduce one shared, pure/lock-safe helper (_custom_provider_slug_key + _unique_custom_provider_entry) that groups ALL named custom_providers entries by a single canonical slug normalization and raises AmbiguousCustomProviderError whenever a consumed slug maps to >=2 entries. Apply it at every slug-only boundary so endpoint and credential can never come from different entries: - bare resolution (resolve_model_provider): all-entry membership for both the active-slug guard and the ordered ownership scan (was owner-only). - provider-qualified @custom:<slug>:model return (the session/send/handoff model_with_provider_context shape), which previously bypassed the check. - credential lookup (resolve_custom_provider_connection), replacing its private first-match-by-slug scan and its duplicate _slugify. - auxiliary persistence (set_auxiliary_model), replacing its own inline first-match-by-slug; the ambiguity now propagates (fails the save) instead of being swallowed, while other best-effort errors stay non-fatal. Still resolves from the in-scope config_data so it remains lock-safe (no deadlock regression). Tests: add fail-closed regressions for the asymmetric A-first-non-owner/B-owner bare case (active-endpoint and bare-custom paths), the model_with_provider_context qualified handoff shape, the credential boundary called directly, and the auxiliary-save ambiguity (asserts nothing is persisted). All five fail before this change and pass after. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: one canonical custom-provider slug identity, point-of-return ambiguity, profile-scoped credentials Deep re-gate follow-up (5 findings): 1. Collision detection now derives from the single slug PRODUCER (_custom_provider_slug_from_name) instead of a looser private key, so producer-level collisions on punctuated/parenthesized names (e.g. 'Foo (Bar)' vs 'foo-bar', both -> custom:foo-bar) are detected. Previously the looser key normalized 'Foo (Bar)' to 'foo-(bar)' and missed the collision, reintroducing the endpoint-A/credential-B bug for those names. 2. The all-entry ambiguity check now runs ONLY at the point a custom:<slug> is about to be returned (via a _finalize() guard covering every custom-return in resolve_model_provider, including the final active-provider fallback), not up front. An unrelated collision on the active slug no longer blocks lanes that resolve to a different provider (@openrouter, a non-colliding @custom:other). 3. streaming.py now resolves endpoint AND credential inside ONE profile scope: the main path extends the scope over runtime-key + custom-provider resolution, and both self-heal retry paths bind the session profile. This stops a detached worker from pairing a named profile's endpoint with the default profile's key. 4. Handoff summary: AmbiguousCustomProviderError now returns HTTP 400 with the actionable rename message instead of degrading to a 200 local fallback the UI treats as success. 5. UI: aux + default model saves surface the server's message and abort (retain dirty state) instead of swallowing it into a generic failure / "settings saved"; the handoff catch shows the 400 rename message verbatim. AmbiguousCustomProviderError gains a .message attribute for verbatim forwarding. Tests: parenthesized-name collision fails closed on all direct paths; unrelated lanes are not blocked by an active collision; custom overrides resolve inside the passed profile scope; handoff returns 400 (not a persisted fallback); JS error-surfacing source guards. Verified the finding #1/#2 cases fail before and pass after (old key misses 'Foo (Bar)'/'foo-bar'; old up-front check over-blocks). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com> |
||
|
|
93814bbb76 |
fix(workspace): snapshot OS drop entries before traversal (#7142)
Co-authored-by: nightcityblade <nightcityblade@gmail.com> Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com> |
||
|
|
4d03578192 |
fix(#6611): make regeneration start atomic (#6677)
* fix(#6611): make regeneration transaction canonical * fix(#6611): close cross-provider regeneration blockers * fix(#6611): harden regeneration transaction boundaries * fix(#6611): bind canonical context and fork lineage * fix(#6611): restore active turn token stamping * test(#6611): make regeneration regressions CI portable * test(#6611): preserve the tracked regeneration error fixture * fix(#6611): preserve extracted session harness contracts * fix(regeneration): preserve concurrent and imported turn ownership (#6611) * fix(regeneration): honor imported turn ownership safely (#6611) * test(regeneration): cover both session lock outcomes (#6611) * test(regeneration): clear unused transaction fixtures (#6611) * fix(#6611): consume public active-turn marker * ci: re-trigger (Playwright install hung ~2.5h on GH runners, warm-cache retry) --------- Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com> Co-authored-by: n <a@n> |
||
|
|
cced4f3509 |
Release — approval-card YOLO handoff fix (#6945) (#7122)
* fix(approvals): handle gateway yolo in webui * fix(approvals): preserve concurrent yolo enables * fix(approvals): honor settled yolo state * fix(approvals): commit yolo after relay * fix(approvals): preserve gateway yolo relay state * fix(approvals): close remaining yolo race windows * fix(approvals): serialize gateway yolo handoff * fix(approvals): linearize yolo disable * fix(approvals): serialize initial yolo relay * fix(approvals): serialize card yolo handoff * fix(yolo): ignore stale card responses * fix(yolo): reject stale session generations * fix(yolo): bind responses to approval owner * fix(yolo): drain pending approvals with exact ownership * fix(approvals): show skip-all busy state * fix: close approval lifecycle races * test: update approval SSE handoff contract * test: include displayed approval owner helpers * fix: linearize yolo transition registration * Release — approval-card YOLO handoff + stale-response ownership fence (#6945) --------- Co-authored-by: dss539 <dss539@users.noreply.github.com> Co-authored-by: n <a@n> |
||
|
|
b1c2bea92e |
Release — scoped extension Configure hook (#7111) (#7116)
* Add scoped extension Configure hook * docs: fail-closed optional chaining in Configure consumer example + old-Core regression The documented consumer used ext?.settings.registerConfigure(...), which throws TypeError on an older E0 Core where settings exists but registerConfigure does not. Use full optional chaining (ext?.settings?.registerConfigure?.()) and add a Node regression proving the consumer survives a Core lacking the capability. Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com> --------- Co-authored-by: Frank Song <franksong2702@gmail.com> Co-authored-by: n <a@n> Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com> |
||
|
|
5e8937981d |
Release — suppress Windows git console windows (#6534) (#7115)
* fix: suppress console windows for git subprocess calls on Windows On Windows, every git.exe spawned by the WebUI (update checks, version detection, workspace git info, rollback inspection, worktree management) creates a visible console window. When Windows Terminal is the default terminal, each short-lived git process opens a new tab. Combined with the 60-second version-skew monitor polling /api/settings and the boot-time update check, this produces a flood of flashing terminal tabs. Root cause: subprocess.run() calls in api/updates.py, api/rollback.py, api/worktrees.py, and api/workspace.py did not pass creationflags=CREATE_NO_WINDOW on Windows. api/workspace_git.py already had the fix (#5692); these four files were missed. Backend fix: - Add _windows_hide_flags() helper to each affected module (mirrors the existing pattern in api/workspace_git.py) - Pass creationflags=_windows_hide_flags() to all git subprocess.run() calls (10 call sites across 4 files) Frontend fix (prevent timeout->retry cascades): - Increase update-check timeout from 30s/60s to 300s (5 min) - Increase /api/settings fetches from 30s to 60s - Increase /api/git-info fetch from 30s to 60s The backend git fetch to GitHub can take 15s per repo; with both WebUI and Agent repos checked sequentially plus other git calls, the total easily exceeds the old 30s default frontend timeout, triggering the api() wrapper auto-retry (3 attempts) and amplifying the git process flood. * [verified] fix: address Windows git subprocess review --------- Co-authored-by: ya1yi2fa3 <ya1yi2fa3@users.noreply.github.com> Co-authored-by: n <a@n> |
||
|
|
509cdb8dfc |
Release — carry explicit_model_pick through /goal (#6705) (#7114)
* fix(session): sync provider change mid-session (#6703) * fix(session): full chat-start parity for /goal explicit-pick (#6703) Addresses review parity details on #6703: - stamp model_explicit_pick_signature after explicit goal resolution (both kickoff paths), matching /api/chat/start #5979, so a first /goal launch after a deliberate custom-provider pick survives a cold streaming catalog instead of reverting to the profile default - consume the one-shot pending session-model marker in cmdGoal, matching the chat/start path (messages.js), so a later /goal with an unchanged dropdown isn't treated as an explicit pick * fix(session): consume pending model-pick marker only after successful kickoff (#6705) * fix(session): node-driven behavioral tests for /goal explicit-pick marker (#6703) * changelog: #6705 — carry explicit_model_pick through /goal * ci: re-trigger tests on fresh runners (prior run's shards hung on GitHub-hosted runners) * ci: re-trigger (playwright browser-install hung on GH runners again; warm cache should restore) --------- Co-authored-by: webtecnica <webtecnica@gmail.com> Co-authored-by: n <a@n> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> |
||
|
|
83e4903274 |
Batch release — double-render fix (#7109) + inline-video preload (#7076) (#7113)
* perf(media): preload visible chat videos * fix(media): ignore detached visibility callbacks * fix(ui): require provable live owner for live-turn preservation (#6948) A completed assistant turn could render twice: when the stream ended (S.activeStreamId cleared) but INFLIGHT[sid] was not yet cleaned, the preserve guard treated bare rendered content (.msg-body presence) as proof of liveness and re-attached the stale live DOM node OVER the rebuilt settled transcript — a second copy of the same message. Data was always clean (state.db, sidecar, /api/session each hold one row); the duplicate existed only in the rendered DOM. Preserve the live turn across the wipe only while there is a provable live owner: an active stream (S.activeStreamId, the #3877 mid-stream case) or explicit live-assistant evidence in the current message projection (S.messages carrying a client-side _live / _activityBurstId / _liveSegmentSeq marker — the reconnect/terminal-projection case). A settled transcript has neither, so the durable settled render wins and appears exactly once. The #5390 blank-turn guard (对话消失) is preserved: a dead empty shell has no live projection either and is still dropped. * changelog: exp batch — #7109 double-render fix + #7076 video preload --------- Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com> Co-authored-by: webtecnica <webtecnica@gmail.com> Co-authored-by: n <a@n> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> |
||
|
|
747b5a6913 |
Batch release exp-v0.52.236 — dotted model labels (#6628) + MCP SDK pin (#6616) (#7103)
* fix(models): normalize dotted Bedrock/Vertex model IDs in labels
Split out of #6607 at reviewer request — it was unrelated to that PR's MEDIA
path handling and needed its own tests.
`us.anthropic.claude-opus-5` carries a cross-region routing prefix plus a vendor
namespace, and `mistral.mistral-large-2407-v1:0` adds a provisioned-revision
suffix. None of it belongs in a human label, so the turn footer rendered
"Us.anthropic.claude Opus 5".
Strip only the two shapes these hosts actually publish, against a CLOSED
provider allow-list:
<region>.<vendor>.<model> us.anthropic.claude-opus-5
<vendor>.<model> mistral.mistral-large-2407-v1:0
A generic "drop leading letters-only dot segments" loop was rejected because it
rewrites arbitrary uncatalogued IDs: `deepseek.v3` rendered as "V3" (vendor name
silently deleted) and `foo.bar.baz` as "BAZ". Dropping a vendor is additionally
gated on the remainder still naming the model, so `deepseek.v3` — where the
vendor IS the name — is left byte-intact.
Backend and frontend are paired: tests/test_dotted_model_label.py drives one
table through both `_get_label_for_model()` and `_stripDottedModelPrefix()` and
fails on divergence, including version dots (`gpt-4.1`, `qwen3.6-35b`),
URI-scheme IDs, and unknown vendors.
The Python half is inlined inside `_get_label_for_model` on purpose: the #3429
harnesses extract that function's source and eval it in isolation, so a
module-level helper NameErrors there. The #3429 JS driver is updated to pull in
the new helper for the same reason.
* fix(models): recognize `global` as a Bedrock routing prefix
Re-review catch, and a real gap: the catalog ships six
`global.anthropic.claude-*` IDs (api/config.py:1901-1909) and the first-party
routing notes use `global.anthropic.claude-…` as the canonical Bedrock shape,
but `global` was missing from the region allow-list. All six therefore kept the
noise this change exists to remove:
global.anthropic.claude-opus-4-7 -> "Global.anthropic.claude Opus 4 7"
Added to the region set in both implementations, which now read identically.
The root cause is two lists that must agree — the region allow-list and the
shipped catalog — so the new guard is catalog-driven rather than another
hardcoded region list: it scrapes every dotted `<head>.<vendor>.<model>` ID out
of api/config.py and asserts none of them keeps a routing/vendor namespace in
its label. A future routing prefix added to the catalog without updating the
region set fails there, instead of shipping mislabeled.
Also pins all six `global.*` IDs plus `us-gov` in the shared strip table (no
suite covered `us-gov` before either), and verified by mutation: dropping
`global` from the Python set fails 3 tests, dropping it from only the JS set
fails 2 — so backend/frontend parity drift is caught, not just a total absence.
Note: the review cited tests/test_provider_prefix_label_normalization.py and
tests/test_ui_model_label_parity.py as the suites to extend; neither exists in
this tree (nothing under tests/ referenced `us-gov` at all), so the cases live
in tests/test_dotted_model_label.py alongside the existing paired coverage.
* fix(models): add missing Bedrock vendors; make the catalog guard actually broad
Two self-review findings (hostile critic pass). Both are the same class as the
`global` gap the reviewer already caught, which means the guard I added for that
gap was too weak to prevent a recurrence.
1. Missing vendors. `luma`, `twelvelabs` and `ibm` are real Bedrock
foundation-model vendors and were absent from the allow-list, so genuine IDs
shipped with the namespace intact:
luma.ray-2 -> "Luma.ray 2"
us.twelvelabs.marengo-embed-2-7 -> "Us.twelvelabs.marengo Embed 2 7"
us.ibm.granite-3-8b -> "Us.ibm.granite 3 8B"
Added those plus `nvidia` and `snowflake` to both implementations.
2. The catalog guard inspected 6 of 75 dotted IDs. Its scrape regex only matched
three-segment `"id": "<region>.<vendor>.<model>"` literals with double quotes,
so the entire two-segment `<vendor>.<model>` shape -- the OTHER documented
shape this PR handles -- was invisible to it. A test that reads 8% of the
corpus while claiming to cover the catalog is worse than no test, because it
reads as proof.
Rewritten to scrape any quoted `id` value regardless of quote style or segment
count (75 dotted IDs now inspected), and to DERIVE the namespace heads from the
production `_regions`/`_vendors` literals instead of retyping them, so a set
that grows without a test update is still covered. Version dots (`qwen3.6-plus`,
`gpt-5.4`) are correctly skipped as non-namespaces.
Also added an explicit per-vendor round-trip test. It asserts no dotted NAMESPACE
survives rather than that the vendor word is absent, because a vendor legitimately
reappears inside some model names (`mistral.mistral-large-2407` -> "Mistral Large
2407") -- my first version of that assertion was wrong for exactly that reason.
Verified by mutation: removing the new vendors fails the round-trip test; deleting
the strip entirely fails 6 tests.
* test(models): drive real getModelLabel() in the parity oracle
The paired test compared _get_label_for_model() against itself, so JS
getModelLabel() never ran and nothing asserted the two sides agreed. It
stayed green when the JS dotted strip was reverted to a no-op AND when the
whole post-strip retry chain was deleted -- blind to both.
Replaced with two tests driven through the real getModelLabel() under Node,
reusing the driver already proven in test_issue3429_uri_scheme_model_label.
Only sinks are stubbed (_dynamicModelLabels empty as it is pre-fetch,
_fmtOllamaLabel identity); every decision function is the shipped source.
The oracle asserts the actual contract -- no routing/vendor namespace leaks
into either label -- rather than string equality of the two labels. Label
divergence is pre-existing and by design: at base
|
||
|
|
e88aff4388 |
Batch release exp-v0.52.234 — O_BINARY (#7077), native-control font (#6871), mobile nav order (#6909) (#7099)
* fix(navigation): preserve mobile action mirror order Reposition stale extension action mirrors during synchronization without moving already-correct elements, preserving native tab order and keyboard focus. Assisted-by: Hermes Agent:gpt-5.6-sol Assisted-by: Codex:gpt-5.6-luna Assisted-by: Claude Code:claude-opus-5 * fix(typography): route native controls through UI font Assisted-by: Hermes Agent:gpt-5.6-sol Assisted-by: Codex:gpt-5.6-luna Assisted-by: Claude Code:claude-opus-5 * fix(windows): open anchored files in binary mode * docs(changelog): note #7077 O_BINARY, #6871 native-control font, #6909 mobile nav order --------- Co-authored-by: starship-s <45587122+starship-s@users.noreply.github.com> Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com> Co-authored-by: n <a@n> |
||
|
|
f60805de01 |
fix(#6867): refresh artifacts after settled-session recovery (#7067)
* fix(#6867): preserve artifact ownership across compression rotation * docs(changelog): note the #6867 artifacts-panel ownership-guard fix --------- Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com> Co-authored-by: n <a@n> |
||
|
|
f1115355a6 |
fix(#6906): keep kanban actions reachable in short windows (#7065)
* fix(#6906): tighten Kanban modal viewport regression * fix(#6906): preserve modal centering fallbacks * docs(changelog): note the #6906 Kanban modal height-cap fix --------- Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com> Co-authored-by: n <a@n> |
||
|
|
8686528b1b |
Release batch A: 6 low-risk gate-passed fixes (experimental) (#7089)
* fix(goal): suppress reserved SILENT sentinel at /api/goal ingress A wake relay POSTing the exact [SILENT] suppression sentinel to /api/goal lets it reach _start_chat_stream_for_session, which persists it as pending_user_message. If 8701 restarts while the turn is pending, session recovery materializes it as a visible _recovered user turn — the same phantom-recovered-turn exposure #7018 closed for chat ingress. Apply the shared _is_silent_control_message() guard immediately after /api/goal session-ID validation and before session lookup or goal-state mutation, returning the same 200 no-op. Matching stays exact and case-sensitive; ordinary kickoff text is unaffected. Add tests mirroring test_silent_control_suppression.py for the goal path (args and text fields, before-lookup suppression, exact-match semantics). Closes #7019 * fix(sessions): keep evicted subagent parents in the import window The sidebar nests a subagent row under its parent only when the parent row is present in the same payload. The visible-window limit was applied as a flat per-row recency slice, so a frozen orchestrator (which stops writing while its leaves keep streaming) lost the recency race against its own leaves and fell outside the window -- promoting those leaves to top-level sidebar rows. Re-add subagent parents that the oversampled candidate set already projected, after the slice. No extra queries, no change to CLI_VISIBLE_SESSION_LIMIT. webui ancestors are deliberately not imported because that sidebar bucket already owns them. Supersedes #7031. * fix(sessions): document and pin the parent-recovery bound (greptile review) Greptile flagged (P1) that a selected subagent child whose parent ranks below the limit * 8 oversample is still promoted to a top-level row. That is real and measured (the parent drops out at candidate #25 of 24), but it is the bound of the design, not a regression: the walk reuses rows the projection already fetched and never issues an extra query. Resolving arbitrarily old ancestors needs an unbounded per-row lookup on the hot sidebar path -- the approach rejected in #7031 -- so the bound is documented and pinned instead. - Docstring: state that `limit` bounds the recency slice, not the row count, so callers must iterate rather than assume len(rows) <= limit; state that recovery is bounded by the oversampled candidate set. - Inline comment: mark the bound at the exact lines Greptile flagged and point at `candidate_limit` as the knob if the window proves too tight. - Tests: cover the candidate-window exhaustion Greptile said was untested -- parent inside the oversample is recovered, parent beyond it stays unresolved -- plus the over-limit return contract and a parent-cycle guard. No behaviour change; 7 tests pass. * fix(share): add _hadAppearance guard to prevent fabricated appearance choice Summary: The inline appearance bootstrap in static/share.html writes hermes-theme and hermes-skin to localStorage unconditionally on every page load, even when the browser had no prior appearance state. This fabricates an explicit user choice on first access via a shared link, making the server-side SETTINGS_DEFAULTS unreachable for deployments that customise the default theme or skin. Root Cause: share.html:9 — the boot IIFE resolves a theme+skin and calls localStorage.setItem() without guarding on whether the user had previously chosen an appearance. The same bug was fixed in index.html by PR #6808 (commit tomtong2015) but share.html was left unchanged. Change: 1. Added _hadAppearance guard before the two localStorage.setItem() calls: var _hadAppearance = localStorage.getItem('hermes-theme') !== null || localStorage.getItem('hermes-skin') !== null; if (_hadAppearance) { setItem('hermes-theme', t); setItem('hermes-skin', s); } 2. The first-paint DOM mutations (classList.add('dark'), dataset.skin) remain outside the guard — only persistence is protected. 3. Synced the skin allowlist with index.html: added neon-soft and neon-paint (zeus and verdigris were already present). Verification: - test_6808_appearance_bootstrap_no_fabricated_choice.py: 11/11 passed covering fresh-browser (no writes), pre-paint fallback, explicit state normalisation, and legacy migration survival. Closes #7030 * fix(tests): pin rebuild budget in issue2513 custom-provider catalog test test (3.13, 4) failed on this PR with: assert "@custom:alpha-proxy:alpha/remote" in alpha_ids E AssertionError: assert '@custom:alpha-proxy:alpha/remote' in {'alpha/sticky'} WARNING api.config:config.py:8355 live provider-catalog rebuild exceeded 4.0s budget - serving fallback, refreshing catalog out-of-band Pre-existing wall-clock flake, not a regression from this PR: this branch touches only api/agent_sessions.py and tests/test_subagent_parent_in_import_ window.py, and both api/config.py and this test file are byte-identical to origin/master. The same shard passed on 3.11 and 3.12. The test never pinned _LIVE_REBUILD_BUDGET_SECONDS, so it raced the global 4s budget in get_available_models(). On a starved runner the cold rebuild overruns, the degraded fallback catalog is served, and the monkeypatched- urlopen model alpha/remote is dropped - leaving only the config-declared sticky model, exactly as CI observed. Force the synchronous (unbounded) rebuild path, matching the existing precedent in tests/test_issue2540_models_endpoint_error.py:20-24. Verified: with the budget forced to 0.001s the unpatched test reproduces the CI assertion verbatim; patched it passes at 0.001s, 0, and default. * fix(#7013): preserve media deny coverage across platforms * fix(docker): keep repository agents out of runtime context * fix(docker): gate image runtime proof behind integration job * test(models): stabilize custom provider catalog regression * Release batch A: 6 low-risk gate-passed fixes (experimental) Batched contributor fixes, each individually Codex-gated during the overnight certifier cycles and re-verified clean-to-ship as a combined stage (Codex SAFE TO SHIP on the combined diff; full suite green except 8 pre-existing approval tests that fail identically on clean origin/master — CI green on same commit). - #7019 (@webtecnica) suppress reserved [SILENT] sentinel at /api/goal - #7031 (@carlotestor) keep evicted subagent parents in the sidebar import window - #7030 (@webtecnica) share.html first-visit appearance guard + skin-id sync - #6853 (@rodboev) exclude repo-root AGENTS.md from the Docker runtime image - #7013 (@webtecnica) test-only: platform-neutral test portability (playwright import guard + media tests served from allowed roots) - test-only (@carlotestor) pin custom-provider catalog rebuild budget (#7054) Co-authored-by: webtecnica <webtecnica@users.noreply.github.com> Co-authored-by: carlotestor <carlotestor@users.noreply.github.com> Co-authored-by: rodboev <rodboev@users.noreply.github.com> --------- Co-authored-by: webtecnica <webtecnica@gmail.com> Co-authored-by: carlotestor <carlotestor@users.noreply.github.com> Co-authored-by: Rod Boev <rod.boev@gmail.com> Co-authored-by: n <a@n> Co-authored-by: webtecnica <webtecnica@users.noreply.github.com> Co-authored-by: rodboev <rodboev@users.noreply.github.com> |
||
|
|
37c59baf68 |
fix(ui): prevent OOM on tab focus for long sessions (#7006)
* fix(ui): prevent OOM on tab focus for long sessions Coordinate visibility-recovery and session-updated refresh paths so they never start concurrent full-transcript loads, window the O(N) pre-render scans to the virtual render window, and bound the render-window expansion. Closes #6999 * fix(ui): full turn context + collision-free cache hash (#7006 review rework) Addresses the maintainer's CHANGES_REQUESTED on PR #7006: - Turn-context maps (_assistantTurnFinalVisibleContentMap / _assistantTurnVisibleContentMap) receive the FULL visWithIdx again. The virtualized render window (head+tail around a gap) cut through assistant runs and merged distinct turns across an omitted user boundary, which could duplicate the final answer in Worklog/Thinking or feed later-turn prose into the reasoning echo-strip. Turn context is input, not output: it must always be complete. - _addBoundedHash now streams the FULL string form through the FNV-1a loop (no length+head+tail clip, no slice copies), and object fields (attachments, tc.args, compression-anchor keys) are walked key-by-key so no integral JSON.stringify() is allocated. Same-length middle-only edits of tool args, attachment metadata, tool snippets, or compression-anchor keys no longer collide in _sessionHtmlCache. Regression tests: - tests/test_issue6999_turn_context_virtual_gap.py: multi-assistant turn with a virtualized gap proves full-context echo-strip suppresses the exact final-answer echo, and documents the gapped head+tail failure. - tests/test_issue2613_render_cache_signature.py: Node harness executing the real _messageRenderCacheSignature() proves same-length middle-only mutations change the signature for every structured field while the old clip scheme collides on the same data. Closes #6999 * fix(ui): coalesce session-updated under refresh guard; insertion-order cache hash Re-gate round 2 for #6999: - session-updated frames arriving while the external-refresh guard is held are latched per SID (max announced count) and drained by the refresh owner's finally with ONE guarded follow-up, instead of being dropped (production does not guarantee a second event). - _hashObjectInto walks keys in insertion order (matching Object.entries render projection) with an explicit per-key index discriminator, so opposite-insertion-order argument objects no longer share a cache signature; recursion depth is now threaded (children increment, not restart). * test(jump-scroll): stub _loadingSessionId + _drainSessionUpdatedPendingCount in extraction harness #7006 adds a reference to the pre-existing module var _loadingSessionId and a new _drainSessionUpdatedPendingCount() helper inside refreshActiveSessionIfExternallyUpdated. The jump-to-answer scroll-settle tests extract that function into a node harness, which didn't declare them -> ReferenceError (19 parametrized failures). Add the missing stubs to the harness preambles (no-op drain, since these tests exercise scroll ownership, not the coalesce path -- real drain behavior is covered by test_issue6999_session_updated_coalesce.py). Test-harness-only; production static/ untouched. Co-authored-by: webtecnica <webtecnica@users.noreply.github.com> --------- Co-authored-by: nesquena-hermes <agent@nesquena-hermes> Co-authored-by: webtecnica <webtecnica@users.noreply.github.com> |
||
|
|
108841ba31 |
fix(composer): dismiss reconnected status (#7028)
Keep reconnect confirmations transient so they stop consuming mobile composer space, while a single owned timer prevents stale notices from clearing newer status text. Assisted-by: Hermes Agent:gpt-5.6-sol Assisted-by: Codex:gpt-5.6-luna Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com> |
||
|
|
1c4ea9c99a |
fix(appearance): don't let the boot script fabricate an explicit them… (#6808)
* fix(appearance): don't let the boot script fabricate an explicit theme choice
`syncSettings` in static/boot.js is careful to let the server win on a first
visit. Its comment says why:
// empty (new-browser) state is indistinguishable from a user who chose
// the defaults. To avoid blocking server→client sync on first visit we
// only let localStorage override the server when it carries an explicit
// user-selectable theme value or a NON-DEFAULT skin.
The inline bootstrap in static/index.html defeats that. It resolves a theme with
`localStorage.getItem('hermes-theme')||'dark'` and then unconditionally writes the
result back:
localStorage.setItem('hermes-theme',theme);localStorage.setItem('hermes-skin',skin);
On a brand-new browser that stores `hermes-theme=dark` before any request is made.
By the time syncSettings runs, `lsHasExplicitTheme` is true, so:
1. the server's `theme`/`skin` in SETTINGS_DEFAULTS (or settings.json) is ignored, and
2. the reconcile branch below POSTs the fabricated value back to /api/settings,
overwriting the stored setting it was supposed to be reading.
The net effect is that the server-side appearance default is unreachable: a fresh
browser always lands on dark and then persists it server-side. Deployments that
set a different default in SETTINGS_DEFAULTS never see it applied.
Fix: only rewrite localStorage when there was already appearance state to
normalise. A fresh visit now leaves localStorage untouched, so `lsHasExplicitTheme`
is false and syncSettings takes the server value exactly as its comment intends.
Deliberately keeps the existing normalisation. The write is not pointless — it
canonicalises legacy names (`solarized` → `dark` + `poseidon`). Gating on "was
there any prior state" rather than per-key preserves that: with `hermes-theme=solarized`
and no `hermes-skin`, the derived skin is still written, so the mapping survives
the next load. A per-key guard would have silently dropped it.
Verified by driving the bootstrap in node against a stubbed localStorage. Behaviour
is byte-identical to before for every input that has prior state — legacy
`solarized`, an explicit `light`+`mono`, and a skin-only `slate` all produce the
same painted result and the same localStorage. The only case that changes is the
empty one, which is the bug:
before {} → localStorage {hermes-theme: dark, hermes-skin: default} server locked out
after {} → localStorage {} server wins
Painting is unchanged throughout: a fresh visit still renders dark, since that is
still the fallback.
* test(appearance): pin absent vs explicit appearance state in the bootstrap
Adds the regression coverage asked for in review. Nothing previously
distinguished "no appearance stored" from "user chose these values":
tests/test_1003_appearance_autosave.py covers picker autosave and
tests/test_bugfix_sweep.py covers guarded storage access, but neither fails if
the bootstrap starts fabricating a choice again.
Executes the REAL bootstrap, lifted out of static/index.html, in node against a
stubbed localStorage and documentElement — the same harness used to verify the
fix, now committed instead of run by hand. Covers all three states the review
called out, plus the pre-paint assertion:
- both keys absent -> no setItem at all, AND the dark class is still applied
(the fix changes persistence only, never the paint)
- either key present -> both values normalised and persisted
- legacy names -> solarized/slate/monokai/nord/oled still migrate, and
the DERIVED skin is still written
That last group is why the guard is on the pair rather than per key: with
`hermes-theme=solarized` and no `hermes-skin`, the write of the derived skin is
the only thing that persists the mapping, so a per-key guard would silently drop
it on the next load.
Verified as a regression test, not a tautology: against the pre-fix
static/index.html, test_fresh_browser_writes_nothing and
test_guard_is_on_the_pair_not_per_key both FAIL, while the other nine still pass
— the unfixed bootstrap does still paint dark and does still persist
migrations, so only the buggy behaviour differs.
One harness note worth keeping: the driver and the extracted script are passed
as FILES (`node <driver> <script>`). Passing the script via `node -e ... --
<script>` shifts process.argv, so the driver eval'd the JSON scenario instead of
the bootstrap — and since `eval('{}')` is a valid empty block rather than a
syntax error, the empty-store case passed while executing nothing. _run() now
asserts the bootstrap produced some effect.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(appearance): keep `var themes=` intact so the skin tests still find the block
CI caught this; review could not. `tests/test_sienna_skin.py` (x2) and
`tests/test_catppuccin_skin.py` locate the inline bootstrap with
init_script_idx = INDEX_HTML.find("var themes=")
end_idx = INDEX_HTML.find("</script>", init_script_idx)
init_block = INDEX_HTML[init_script_idx:end_idx]
Declaring `_hadAppearance` as the first `var` in that chain turned `var themes=`
into `,themes=`, so `find()` returned -1, `init_block` came out EMPTY, and three
assertions failed against the empty string — including "Default theme must remain
'dark'", which read as though this PR had changed the default. It had not: the
default is untouched, the block simply could not be located any more.
Splitting the declaration (`;var themes=` instead of `,themes=`) restores the
literal. Both are `var` in the same function scope, so behaviour is identical —
the regression module still passes unchanged, including the empty-store case
(no writes, dark class still applied) and all five legacy migrations.
Failed on shards 3 and 4 across 3.11/3.12/3.13 while the same shards passed on
master, which is what pinned it to this change rather than flake.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Hermes Agent <hermes@aip.de>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
|
||
|
|
155838f2c3 |
fix(i18n): unify ja locale profile wording (プロフィール → プロファイル) (#6762)
Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com> |
||
|
|
3d18a8ca5a |
i18n: translate workspace-sort keys in 14 non-English locales (#6902) (#6910)
Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com> |
||
|
|
6261b626a7 |
fix(streaming): scope streaming message rendering per-session to prevent cross-session render leak (#6449) (#6502)
When the user switches from a streaming session (A) to another session (B), A's streaming text could render in B's window because: 1. The token handler guards against rendering when the session is not active, but the rAF/setTimeout window between _scheduleRender() and _doRender() can outlive a session switch. 2. _doRender() writes to assistantBody without checking whether the stream's session is still the active frontend pane. Fix: add _isActiveSession() guards to: - _scheduleRender() — drop scheduled renders when the session changed - _doRender() — bail before any DOM write if the session switched - _flushPendingSegmentRender() — defense-in-depth for any future callers These guard at the same level the token/interim_assistant event handlers already do, closing the rAF gap that could leak streamed text into a different session's view. Co-authored-by: webtecnica <contato@webtecnica.com.br> Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com> |
||
|
|
3a95d82531 |
fix(chat): prevent first response jump from snapping back (#6621)
* fix(chat): keep response jump position after session load Cancel pending load-time bottom settling before an explicit response jump takes scroll ownership. Add a regression test covering the first-click race. * fix: gate response jump ownership by destination * fix: keep response jumps programmatic through smooth scroll Native smooth-scroll frames from response jumps could reach the manual scroll listener and claim reader ownership before the final destination was known. - own response-jump scrolling with a generation- and session-scoped lifecycle - reconcile sticky ownership from final 80px tail geometry - preserve current low-delta wheel takeover semantics when integrating with master - cover visible assistant, user-row, and virtualized paths through the production scroll listener * fix(scroll): hold reader off-bottom during jump owner + cancel jump on explicit End (#6621 gate fixes) Codex gate found two defects in the jump-scroll ownership mechanism: - Jump during an active stream snapped back to bottom on the next token: the owner suppressed the scroll listener but left the pre-jump pinned state, so scrollIfPinned() reclaimed the bottom. Now _beginMessageJumpScroll unpins for the ownership window and scrollIfPinned() no-ops while an owner exists. - An explicit End click could be undone by the pending jump reconcile restoring the stale unpinned snapshot. scrollToBottom() now cancels the active jump owner first; _cancelMessageJumpScroll restores the preserved snapshot so a non-reconcile cancel doesn't leak the transient unpinned state. * fix(scroll): drop undefined currentSid global ref in _messageJumpSessionId (#6621 brick-class scope-undef gate) The PR's _messageJumpSessionId() referenced a bare 'currentSid' global that does not exist in ui.js (everywhere else it's a local const from S.session.session_id). test_static_js_scope_undef flagged it brick-class (#3696). The S.session.session_id fallback already IS the canonical accessor; removed the dead first line and updated the harness to drive the session-change case via S.session.session_id. * fix(scroll): round-2 gate fixes for jump-owner state transitions (#6621) Re-gate found three more state-transition edge cases from the temp-unpin window: - wheel-up interrupting an active jump owner after the programmatic latch expires now explicitly establishes the unpinned reader-owned state (was cancelling the jump but leaving pinned -> next token snapped to bottom). - _resetStreamScrollFollow() now cancels the jump owner FIRST, before its pinned-state assignments, so a stream starting mid-jump can't have its pin undone by the snapshot restore -> auto-follow stays enabled. - _finishMessageJumpScroll() flushes a deferred external-session refresh after reconciliation when the terminal state is pinned to the tail, so a refresh deferred during the temp-unpin window isn't stranded. * fix(scroll): make jump-owner guards typeof-safe + widen static test window (#6621) The two _messageJumpScrollOwner guards (scrollIfPinned, scrollToBottom) are pulled into other scroll test harnesses (test_issue6414, test_issue4856) that stub the scroll env without declaring the new global; bare references threw ReferenceError. Guard with typeof (matches the PR's own jumpScrollOwned check). Also widen test_low_delta_wheel_intent_is_tracked_separately's source-slice window 1400->1800 to still contain the (unchanged) _lastMessageWheelIntentMs line after the +9-line wheel-up-during-jump block was inserted. * test(scroll): add streaming-frame-hold + wheel-during-jump regression tests (#6621 Fable finding ii) Fable UX gate flagged that the PR's tests model only the stale load-time settle callback, not the streaming case. Add two node-harness tests: - a streaming render frame (scrollIfPinned) fired inside the jump-owner window must not snap the reader to the bottom (holds at target, 0 bottom-writes). - a gentle wheel-up during the owner window after the programmatic latch stales hands ownership to the reader UNPINNED at their position, never pinned mid-transcript. * fix(scroll): widen jump-owner takeover to downward + touch input (#6621 Fable S3) Fable's re-review (on the fixed code) confirmed the streaming blocker + wheel-up concern resolved, and probe-proved one remaining narrow regression: a gentle wheel-DOWN or touch scroll during the owner window cancelled the owner and restored the pinned snapshot but did NOT re-unpin (the takeover was gated on wheelUp only), so the next streaming token yanked the reader to the bottom. Widen the takeover to any message-pane scroll input during the owner window. Add a wheel-down regression test; widen the #4970 static-slice window to 2000 to still contain the (unchanged) _lastMessageWheelIntentMs line. --------- Co-authored-by: pxxD1998 <214340659+pxxD1998@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> |
||
|
|
f556cf349e |
Release: chat stream scroll-bounce fix (#6257) (#6993)
* fix(stream): preserve fade nodes while prose is live * fix(stream): preserve transparent fade nodes like assistant prose The transparent-activity prose path (transparent_stream mode) kept the pre-PR fade cleanup: _bindTransparentFadeCleanup() in static/ui.js still replaced every animated stream-fade-word span with a fresh text node on animationend, invalidating the native scroll anchor while the row is still growing. Mirror the messages.js fix so both live prose paths share one node-preserving cleanup: remove only the transient is-new class and the --stream-fade-ms inline property, keeping span identity until final settlement rebuilds the persisted DOM. Extend tests/test_smooth_text_fade.py with a sibling regression that extracts the production _bindTransparentFadeCleanup() from ui.js and fires animationend, asserting no replacement, is-new removal, stream-fade-word retention, and timing-override cleanup (plus source-string guards against replaceWith in the cleanup block). The new test is red-before-green against the previous ui.js implementation. * docs(changelog): stamp #6257 (stream fade scroll-bounce fix) --------- Co-authored-by: Anthony Ruiz <ruizanthony@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> |
||
|
|
83b5ccb0fe |
Release: CJK full-width punctuation autolink fix (#6792) (#6991)
* fix(renderer): exclude CJK full-width punctuation from autolink URLs * fix(test): widen autolink block search window for CJK punctuation fix (#6792) The CJK full-width punctuation fix expanded the autolink regex and comment text, pushing target='_blank' and esc(clean) beyond the 600/ 700-char search windows in tests. Widen to 1200 chars. * fix(test): widen autolink window in test_issue470 to match 1200-char block The CJK punctuation fix added comment lines that pushed href="${clean}" beyond the 600-char search window in test_issue470.py::test_autolink_no_esc_on_href, causing the same failure across 3.11/3.12/3.13 shard-4. test_issue342.py was already widened in cc52202f; apply the same 1200-char window here. * fix(test): drive real renderMd() for CJK full-width autolink behaviour Replace the test-owned FIXED_REGEX copy in test_fullwidth_autolink.js with a pytest + node harness that extracts and runs the ACTUAL renderMd() from static/ui.js (same driver pattern as test_renderer_js_behaviour.py). Coverage per review: - every CJK trailing mark (),。;:!?、) through both the outer paragraph pass and the inline pass (list items + blockquotes) - ASCII trailing marks (regression guard) - normal URLs with query params (href keeps raw &, text is escaped) - Markdown links and pre-existing anchors are stashed, never re-wrapped Reverse-verified: reverting the \uFF09 exclusion + CJK strip makes 27 of 38 assertions fail on the real production code. * fix(test): drop stray f-prefix from placeholder-free asserts (F541) * docs(changelog): stamp #6792 (CJK autolink punctuation) --------- Co-authored-by: happy5318 <happy5318@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> |
||
|
|
e7ac9f09fd |
Release: mobile drawer + terminal-backend cache fixes (#6921 #6933 #6671 #6175) (#6990)
* fix: include terminal backend identity in agent cache signature (#5937) (#6175) * fix(mobile): preserve extension panels in drawer sync * fix: close mobile sidebar drawer when opening main-view create forms The add-space/new-profile/new-skill/new-cron forms render in the main view, which on phones is covered by the full-screen sidebar drawer (z-index 200). Opening one from inside the drawer only focused the input (keyboard popped up) while the form stayed hidden underneath. - openWorkspaceCreate / openProfileCreate / openSkillCreate / openCronCreate now close the drawer after rendering the form, mirroring their *_Detail siblings. - move the fromRailClick drawer re-open in switchPanel() into the synchronous section: it used to run after await loadXxxPanel(), racing (and re-opening over) a drawer close issued from inside the drawer during the panel load window. Verified in Playwright (390x844): drawer closes and stays closed after plus-click, form visible with focus on the path input; desktop (1280x800) unchanged; 83 mobile + 26 workspace tests pass. * fix: close mobile sidebar after project quick-create plus The per-project '+' button (_attachProjectQuickCreateButton) created the new project conversation but never closed the mobile sidebar drawer, so on phones the tap looked like a no-op: the session was created, just hidden underneath the full-screen drawer (z-index 200). Close the drawer in the success path after the sidebar repaint, mirroring $('btnNewChat').onclick in boot.js and the #5409 close in _openSidebarSession. Failure path keeps the drawer open so the error toast stays visible for retry. Adds static regression tests pinning the guarded close call and its position after the repaint. * test: pin quick-create drawer close to the success path (try block) Greptile review note: the string-presence and ordering assertions alone would still pass if the guarded closeMobileSidebar call were moved into the later catch branch, silently reintroducing the mobile drawer bug. Add an explicit assertion that the close sits before the success-path catch(err), so a regression that moves it into the failure branch is caught. * docs(changelog): stamp batch1-p1 (#6921 #6933 #6671 #6175) --------- Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com> Co-authored-by: Rodrigo Gomes da Silva <rodrigo.smscom@gmail.com> Co-authored-by: silent-reader-cn <silent-reader-cn@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> |
||
|
|
8841662768 |
fix: close snapshot capture/serve exfiltration holes + prefix-URL corruption (review round 2)
Addresses nesquena-hermes Round-2 review on #6979. All three MUST-FIX items reproduced end-to-end are fixed; three SHOULD-FIX items folded in. MUST-FIX 1 (data exfiltration) — capture/serve deny parity + source binding: - The #3234 state/profile deny model is extracted into a single shared predicate (routes._media_deny_reason) used by BOTH /api/media serving and snapshot capture, so anything the serve path refuses is never captured in the first place (STATE_DIR subdirs, webui_state subdirs, named-profile roots, secret basenames, the snapshot store itself). - Every captured digest now carries a server-owned canonical source-path association (<digest>.src.json sidecar, tmp+rename atomic). /api/media serves a digest only for the exact path it was captured from — replaying a valid digest through a different allowed path falls back to the live file instead of leaking stored bytes. - Snapshot blobs are opened ANCHORED inside the store root (no-follow). - Committed end-to-end regression: capture a denied state file and prove it cannot be served through any path. MUST-FIX 2 (data exfiltration) — configured store root denied regardless of name: - get_snapshot_dir() is resolved and any bare path= target at or below it is rejected BEFORE the allow/serve decision, so a custom HERMES_WEBUI_MEDIA_SNAPSHOT_DIR (e.g. /tmp/custom-store-name) can no longer be read via a plain path= request. MUST-FIX 3 (silent corruption) — prefix-safe URL stamping: - _stampMediaSnapshots now rewrites each COMPLETE path= query value atomically (boundary-aware regex, decode whole value, exact map lookup) instead of substring split/join. One path being a prefix of another (/tmp/a.png vs /tmp/a.png.backup) can no longer corrupt the longer URL or drop its digest. data-path placeholders use the same value-level rewrite with HTML-entity unescaping. Prefix-path regression test committed. SHOULD-FIX folded in: - A recorded digest is FINAL: quota eviction no longer causes a later settle to re-capture the current live bytes and rebind the historical message (immutability restored + no quota-thrash I/O). - is_valid_digest uses whole-string fullmatch (\Z) — a trailing newline can no longer pass the gate. - PDF/HTML fallback, download and open-full-page links now carry snap so they open/download the frozen bytes. Deferred (noted in review thread): emission-time capture (streaming.py) and worklog scene-prose stamping — scene rows do not carry parent-message snapshot metadata; threading it through the scene reconciler is a separate data-pipeline change. Main transcript + transparent ordered segments remain the old/new comparison surface. Tests: +11 backend/frontend regressions (36 total in test_media_message_snapshots.py), incl. the exact Round-2 repros. Reverse-verified: the new tests fail on the round-1 head (8 failed). |
||
|
|
1db353a1f0 |
fix: scope snapshot stamping to main+transparent paths (no scene render changes)
- Revert all worklog/scene render-path changes: the scene render chain is exercised by tests that extract function bodies and eval them in isolation (_renderAnchorSceneRowsIntoWorklog, _anchorSceneNodeForRow, etc.); adding helper references breaks those harnesses with ReferenceError. - Snapshot stamping now covers the main transcript path and transparent ordered segments only — the comparison surface for old/new previews. - Transparent segment stamping is inlined in the msg-body template guarded by typeof m, keeping the source-window string intact for test_issue3397 and keeping the eval'd ordered-block self-contained for test_status_command_card. - PDF/HTML lazy loaders keep the exact publicMediaUrl source string the tests assert; snap is appended to mediaUrl only. |
||
|
|
e5ae65f510 |
fix: preserve transparent-part source window; clean lint in snapshot tests
- Keep `const partBodyHtml=_getCachedRender(partDisplayText,false);` line intact (test_issue3397 asserts the exact source string), apply snapshot stamping on the following line via partBodyHtmlStamped. - Drop unused roots_before variable and unused import in test_media_message_snapshots.py (ruff F841/F401 on new lines). - Update the source-invariant assertion to the two-line stamp shape. |
||
|
|
4eb98889ba |
fix: freeze per-message media previews with content-addressed snapshots
PR #6922 made /api/media revalidate on every use, so overwriting a file in place (same filename) also rewrote every historical chat preview that referenced it — the old/new comparison was lost. At settle time, snapshot each local-file MEDIA: reference into a content-addressed store (<STATE_DIR>/media_snapshots/<sha256>.snap) and stamp the message with _media_snapshots {path: digest}. The frontend appends &snap=<digest> to historical preview URLs; /api/media serves the frozen bytes with immutable caching when the digest is valid, and falls back to the live file otherwise (eviction/never-captured). The existing path allow/deny checks still gate the request — snap only selects which bytes to serve, it never widens access. - api/media_snapshots.py: content-addressed store (sha256 key, dedup, 50MB per-file / 2GB total caps with FIFO eviction), MEDIA ref resolver, capture allow-list mirroring the /api/media roots + #3234 deny list - streaming.py: annotate settled assistant messages (idempotent, best-effort) - routes.py: snap= serving branch + download_name override + deny direct store-path access - models.py: _media_snapshots registered in display-metadata merge keys - ui.js: _stampMediaSnapshots post-cache stamping (main transcript, transparent ordered segments, worklog scene prose) + data-snap threading through the five lazy preview loaders 25 new tests incl. node-executed frontend behavior tests; reverse-verified. |
||
|
|
8673ca7338 | fix(models): preserve hinted custom catalog fallback | ||
|
|
1d8e7fe664 | fix(models): preserve custom provider model identity | ||
|
|
f7887bfc87 |
fix: clear stale compression state on session switch
When switching sessions in loadSession(), the window._compressionUi state was not cleared, causing phantom 'Compressing context' barriers to appear on fresh sessions that never triggered compression. This complements PR #6572 which handles the case where compressed SSE events are lost/delayed, but does not cover the session switch scenario. Root cause: loadSession() tears down approval/clarify/composer state but missed the compression UI state, which is per-session and must not leak. Fix: Clear compression state immediately after hideClarifyCard() in the loadSession() teardown sequence. |
||
|
|
e4c3c728d9 | fix(commands): wait for bundle metadata before skill suggestions | ||
|
|
eea4f03db8 | fix(commands): search skills by keyword in slash autocomplete | ||
|
|
7c5add1903 | fix: settle extension lifecycle before notifying | ||
|
|
c45691141e | Add extension turn lifecycle events | ||
|
|
5564b98b56 | Merge remote-tracking branch 'origin/master' into franksong2702/e0-extension-identity | ||
|
|
8b873c55eb |
Release exp-v0.52.198: Refine selected transcript text in composer (#6410, @rodboev)
Ships PR #6410 with a maintainer UX fix folded in (authorship preserved). Adds a 'Refine' action to the message text-selection toolbar: select text in a chat message -> Refine seeds it into the composer as an editable quoted draft (marker-free, one-way, existing draft + attachments preserved, 44x44 mobile icon buttons, all 15 locales). ChatGPT/Notion-style quote-to-edit flow. Maintainer fix (Fable UX gate): French instruction string now uses the space-before-colon convention (Instruction d'affinage :). Gate: Codex SAFE TO SHIP (marker-free, no XSS, one-way Range, Reply/drafts preserved), Fable SHIP-WITH-UX-FIXES (fix applied), tests 19/19, ESLint clean. Co-authored-by: rodboev <rodboev@users.noreply.github.com> |
||
|
|
2fdc919dcc | Merge branch 'pr6037stage' into release/stage-6037-1786396633 | ||
|
|
66277fbeb8 | fix(kanban): short-landscape overlay scroll + robust mobile-responsive CSS test | ||
|
|
f50a80beb6 | Merge branch 'pr6055stage' into release/stage-6055b-1786393156 | ||
|
|
d92aeecfae |
fix(kanban): keep board-settings field in-viewport on short landscape phones
The default-workdir layout test (#5932) failed on CI at 480x320 for all locales: the board modal is centered in the overlay, and at short landscape heights the capped modal's lower rows render below the fold, so the workdir field's bottom (366) exceeded the 320px viewport. Anchor the overlay to the top at (max-width:640px and max-height:480px) so the capped, internally scrollable modal is reachable from the top, and switch the cap to 100dvh for reliable mobile viewport height. Verified: all 15 layout cases pass. |
||
|
|
6f7fa623cb | Merge branch 'pr-6091-head' into release/stage-6091-1786388870 | ||
|
|
70c77095a7 |
fix(a11y): clear stale aria-expanded when switching panels (#6656)
_syncSidebarAria set aria-expanded on the newly-active rail button without unsetting the previous one, so chat -> kanban -> skills left three buttons all claiming aria-expanded=true. A screen reader announced three open disclosures when only one panel owns the sidebar. Reset every rail button that carries the attribute before setting the active one. |
||
|
|
fa087c53d1 |
fix(cron): mark running cron sessions as active in sidebar (#6734)
* fix(cron): mark running cron sessions as active in sidebar * fix(cron): include cron_running in effective-streaming predicate + test updates (#6734) * fix(cron): longest-prefix-first cron job matching in session list cache (#6728) * fix(cron): require exact run-timestamp suffix in cron_running prefix match Gate re-review (Codex) found the longest-prefix-first match could misattribute: cron_job_prefixes is built ONLY from RUNNING jobs, so when the true longer owner (backup_full) is NOT running, a running shorter job (backup) swallowed a completed cron_backup_full_YYYYMMDD_HHMMSS session (its prefix cron_backup_ still matched), deferring that row's completion/unread transition. Now the text after a matched cron_{jid}_ prefix must be EXACTLY a run timestamp (\d{8}_\d{6}), so a shorter job id can't claim a longer job id's session regardless of which jobs are running. Regression test added (fails without the guard). Co-authored-by: webtecnica <webtecnica@users.noreply.github.com> --------- Co-authored-by: webtecnica <webtecnica@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> |
||
|
|
a662e37f89 |
fix(settings): honor persisted auto-follow across boot failures and partial bodies (#6856)
* fix(settings): honor persisted auto-follow across boot failures and partial bodies (#6819) The Auto-follow new content toggle was silently re-enabled by two paths: - boot.js settings-fetch-failure fallback hardcoded _autoScrollFollow=true, clobbering an explicit OFF for the whole session (post-turn scroll yanks until refresh); - _applySavedSettingsUi assigned body.auto_scroll_follow!==false unconditionally, so a partial settings body without the key evaluated undefined!==false -> true. Fix: mirror the resolved value into profile-namespaced localStorage (JSON map keyed by active profile), make the boot fallback read the mirror instead of hardcoding ON, and only override the runtime flag when a settings body actually owns the key. Fresh users (no mirror) still default to ON. Tests: update test_issue4006 fallback assertion; add test_issue6819_auto_scroll_follow_persistence covering source-shape guards and extracted-helper behavior (fresh default, persisted OFF, profile isolation, partial-body no-clobber, save-path persist). * fix(settings): resolve auto-follow mirror profile from cookie before S.activeProfile init (#6856 review) P1 (greptile-apps review): the profile-namespaced mirror was keyed by S.activeProfile, but the boot settings-fetch-failure fallback runs before boot resolves S.activeProfile, so a non-default profile with a failed fetch read the 'default' mirror entry. Resolve the key from the hermes_profile cookie (set per-client by /api/profile/switch, readable synchronously at page load) first, falling back to S.activeProfile, then 'default'. P2: document the persistence contract in the helper block (storage shape, profile-key resolution order, read semantics, upgrade path). Tests: extend the extracted-helper node test with the P1 scenario (cookie authoritative over stale S.activeProfile, per-profile isolation via cookie, no-cookie fallback) and add source-shape guards for the cookie read. * fix(settings): defer auto-follow mirror read until profile resolves (P1 follow-up) greptile-apps review: the hermes_profile cookie is HttpOnly — document.cookie can never read it, so the cookie-based profile key from 39fed70b always fell back to 'default', re-introducing the wrong-namespace bug for non-default profiles on a failed settings fetch. Correct model: the active profile is only known asynchronously via /api/profile/active. The boot settings-fetch-failure path now sets a safe default (ON) plus a deferred-reapply flag; once S.activeProfile resolves, the mirror is read with the profile-correct namespace and the flag clears. The success path (which already applied+persisted the authoritative value) never touches the flag. Tests updated to the deferred model: source-shape guard asserts the re-apply runs AFTER profile resolution (fallback < profile-resolve < reapply ordering), no document.cookie read in the profile key, and the node behavioral test simulates boot-failure -> profile-resolve -> deferred re-apply. * fix(settings): defer auto-follow mirror write until profile resolves (P1 round 3) greptile-apps review: the SUCCESS boot path called _persistAutoScrollFollow before S.activeProfile initialized, so a non-default profile's successful settings fetch stored its value in the 'default' namespace. A later failed-fetch boot would then find no entry for the real profile and restore auto-follow ON. The success path now defers the mirror write the same way the failure path defers its read: it records the value in _autoScrollFollowDeferredPersist, and the post-profile-resolution block persists it under the correct namespace (then clears the flag). Both deferred branches now run together after S.activeProfile resolves. Tests: success path must not call _persistAutoScrollFollow before profile resolution (source-shape), deferred-persist anchor ordered after profile resolve, and the node behavioral test covers success-boot -> profile-resolve -> deferred write under the resolved profile (and NOT under default). * fix(settings): make auto-follow mirror global to match the backend contract (maintainer review) Maintainer review on #6856: the mirror was profile-keyed, but the backend authority is GLOBAL — SETTINGS_FILE = STATE_DIR / 'settings.json' (api/config.py), and /api/settings calls load_settings() with no profile-specific file. A profile-keyed map leaves a freshly-opened profile with no mirror entry, so a failed settings fetch on that profile wrongly restored ON despite the global OFF — the original failure stayed reproducible. Revert to a single global last-known value ('1'/'0' under one key): - _persistAutoScrollFollow / _readPersistedAutoScrollFollow now use a plain scalar, not a JSON profile map. - Boot success path persists synchronously (no deferred write — the global mirror has no profile dimension). - Boot failure fallback reads synchronously (no deferred re-apply — no profile resolution needed). - Remove _autoScrollFollowProfileKey, _autoScrollFollowDeferredPersist, _autoScrollFollowDeferredReapply entirely. Tests: behavioral node test now asserts the maintainer-requested regression (global OFF survives profile change + failed fetch, single scalar storage); source-shape guards assert the profile map and deferred machinery are gone; boot success persists; partial-body and save-path assertions unchanged. 14/14 pass. * fix(settings): never persist synthesized auto-follow from a failed autosave (Greptile P1) Greptile review on #6856: in _autosaveAppearanceSettings, when the POST fails (saved is falsy), '!saved' synthesizes _autoScrollFollow=true, and the added persist call wrote that synthesized ON into the mirror. A later boot settings-fetch failure would then restore ON from the corrupted mirror, reintroducing post-turn scroll yanks for a user who explicitly disabled auto-follow. Persist ONLY from an explicit boolean in the server response: if(saved && typeof saved.auto_scroll_follow==='boolean' && typeof _persistAutoScrollFollow==='function'){ _persistAutoScrollFollow(saved.auto_scroll_follow); } A failed save or a partial response without the key leaves the mirror untouched (last authoritative value preserved). Tests: source-shape guard asserts the explicit-boolean requirement; the node behavioral test replicates the failed-autosave path (mirror stays OFF) and the successful-autosave path (explicit boolean persists). * fix(settings): sync auto-follow mirror on successful settings-panel GET Gate review (Codex) found a stale-mirror gap in #6856's own contract: a successful loadSettingsPanel() GET applied the authoritative auto_scroll_follow value to window._autoScrollFollow but did not persist the global mirror, so an explicit OFF applied via the panel left a stale ON mirror that a later boot-fetch failure would restore (reopening #6819). Persist from the panel GET too, only when the server sent an explicit boolean (never a synthesized default) — matching the boot + autosave contract. Adds a bite-tested regression. Co-authored-by: sand01chi <sand01chi@users.noreply.github.com> --------- Co-authored-by: sand01chi <sand01chi@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> |
||
|
|
64c2c12df7 |
feat(sidebar): add show_kanban_sessions toggle to hide kanban worker sessions (#6780)
* feat(sidebar): add show_kanban_sessions toggle to hide kanban worker sessions * fix(sidebar): kanban source-filter override + filtered-view cap (cron/webhook parity) Gate re-review (Codex) found the kanban-hide addition missed cron/webhook parity for the source-filter case: - An explicit kanban source_filter now overrides the default hide (threaded source_filter into _dedupe_cli_sidebar_sessions_for_api, normalized with strip().lower() to match the loader), so a deliberate kanban view shows those rows even with the toggle off. Scoped to the matching source only. - A dedicated KANBAN_PROJECT_CHIP_LIMIT bounds a filtered kanban-only view, mirroring CRON/WEBHOOK_PROJECT_CHIP_LIMIT. Deliberately NOT changing the general exclude_sources: the original PR lets kanban load on the source_filter=None path so the toggle-ON case works (the hide is applied by _hide_from_default_sidebar only when the toggle is off). Excluding kanban there would save the 20-row budget but break toggle-on unless a bounded kanban merge is added (cron/webhook don't do that either) — that query-budget optimization is deferred as a separate cross-cutting change. The pinned-survives-hide finding is also out of scope (cron/webhook hide pinned identically, confirmed by Codex). Adds bite-tested regressions. Co-authored-by: vduke <vduke@users.noreply.github.com> --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> |
||
|
|
cabb467b36 |
fix(stream): stop fade-text effect from re-fading the whole message on tool-call rewinds (#6783)
* fix(stream): stop fade-text effect from re-fading whole message on tool-call rewinds When the fade-text effect is enabled, Hermes long tasks visibly blink the entire message on every tool call. Root cause: tool-call XML (<function_calls>) appears inline in the assistant text stream, and the unclosed-tag stripping in _stripXmlToolCalls temporarily rewinds displayText to a strict prefix. That rewind zeroed the fade playout (_streamFadeNextText) and tripped _smdWrite's self-heal, which cleared assistantBody.innerHTML and re-created every word as a fresh is-new span — replaying the fade on all visible text at once. Fix: on a detected rewind, shrink the playout cursor to the common prefix instead of resetting to zero, and mute the fade renderer for that common prefix during any rebuild (both the main smd path and the anchor prose incremental node) so only the post-rewind tail animates. Adds regression tests: playout rewind keeps common prefix visible, rebuild mutes the common prefix, and _smdWrite's self-heal sets the silent-prefix window. * fix(#6783): compute rewind mute prefix in rendered-text space Review feedback (maintainer round on 56c9307f): the silent-prefix budget was computed in SOURCE-text space (charCodeAt over markdown source), but the fade add_text hook only receives RENDERED prose. Markdown delimiters, link destinations and MEDIA token bytes never reach add_text, so the source-space budget over-muted the first genuinely new word after a rewind (e.g. '**alpha** beta' -> '**alpha** gamma' muted gamma). Fix: snapshot the OLD rendered text before the rebuild (assistantBody textContent / anchor .msg-body textContent), then after parser_write compare it with the NEW rendered text and strip is-new only from spans inside the rendered common prefix (_streamFadeMuteRenderedPrefix). - Rendered-space boundary, scoped to the owning node (no stream-global counter that can survive a rebuild without a fade consumer) - No state left behind on reduced-motion / safe-renderer / empty-text / parser-finalization paths: _rewindPrevRendered is a local variable consumed right after the rebuild - Tests: plain-word mute, markdown/MEDIA byte regression (the blocker), per-root scoping (main vs anchor), updated _smdWrite self-heal test Gate: node -c OK; test_smooth_text_fade + test_smd_media_in_stream + test_svg_audio_video_rendering + test_insights -> 83 passed, 18 subtests. * fix(#6783): drop f-prefix from _fade_fake_node JS block (F541) CI lint failure on a6d37b22: tests/test_smooth_text_fade.py:723 F541 f-string without any placeholders. The f-prefix was only used to escape JS braces ({{ }}) — a plain string with literal braces is equivalent and passes the ruff E9+F+B gate. --------- Co-authored-by: silent-reader-cn <silent-reader-cn@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com> |