Commit Graph
7891 Commits
Author SHA1 Message Date
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>
2026-08-16 21:20:56 +00:00
nesquena-hermesandnesquena-hermes dc3bf44e57 Release: SQLite source build loads on arm64 (ld.so.conf.d fix for #6900) (#7044) (#7045)
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-08-14 20:53:38 -07:00
nesquena-hermesandnesquena-hermes 6086446018 fix(docker): source-built SQLite must load on arm64 (ld.so.conf.d) — follow-up to #6900 (#7044)
* fix(docker): register /usr/local/lib in ld.so.conf.d so the source-built SQLite loads on arm64

The #6900 SQLite-from-source upgrade passed the amd64 docker-smoke but FAILED the
multi-arch release build on linux/arm64 with 'AssertionError: SQLite 3.46.1 still
vulnerable' — Python's sqlite3 kept loading the base image's /usr/lib multiarch
libsqlite3 (3.46.1) instead of the freshly-compiled /usr/local/lib copy (3.53.0),
because /usr/local/lib is NOT in the default ld.so search path on Debian arm64
(it happened to be picked up on amd64). Register /usr/local/lib via an
ld.so.conf.d entry before ldconfig so the new lib wins on every architecture.

The PR's own build-time version assertion is what surfaced this (correctly), and
it also serves as the verification that the fix works once the arm64 build passes.

Follow-up to #6900 (exp-v0.52.227, whose multi-arch image failed to build/push).

* test-fix: keep wal.html link within 500 chars of ARG (condense arm64 comment)

---------

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-08-14 20:51:42 -07:00
nesquena-hermesandnesquena-hermes 71584418a8 Release: upgrade bundled SQLite to 3.53.0 (WAL-reset fix, secure_delete preserved) (#6900, @qxxaa) (#7043)
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-08-14 20:16:20 -07:00
1aab1e4105 fix: compile SQLite 3.53.0 from source to fix WAL-reset corruption bug (#6900)
* fix: compile SQLite 3.53.0 from source to fix WAL-reset corruption bug

The python:3.12-slim base ships SQLite 3.46.1 (Debian Trixie), which is
vulnerable to the WAL-reset corruption bug discovered March 2026.
Debian has not backported the fix.

Compiles SQLite 3.53.0 from the official amalgamation tarball during the
Docker build. Installs to /usr/local/lib (takes ldconfig priority over
/usr/lib). Build tools (gcc, make, libc6-dev) are purged in the same
layer. A build-time Python assertion fails the image build if the linked
library is still vulnerable.

Version and year are build args for easy bumps.

https://sqlite.org/wal.html#walresetbug

* fix: enable FTS5/FTS4/R-Tree and pin tarball SHA-256

Address review feedback (nesquena-hermes):

1. Add --enable-fts5 --enable-fts4 --enable-rtree to ./configure so the
   compiled SQLite matches the distro package's feature set. Without
   FTS5, state.db session/message full-text search breaks silently.

2. Pin the amalgamation tarball SHA-256 as a build arg and verify with
   sha256sum -c before extraction.

3. Extend the build-time assertion to create and drop an FTS5 virtual
   table, proving the module is available - not just the version number.

4. Add regression tests for checksum verification, FTS5 configure flag,
   and FTS5 build-time vtable assertion.

* docker(sqlite): compile with SQLITE_SECURE_DELETE to preserve deleted-row erasure

Codex gate found a SILENT data-privacy regression: the Debian base image's SQLite is
built with SQLITE_SECURE_DELETE (PRAGMA secure_delete=1, deleted content overwritten),
but compiling 3.53.0 from the amalgamation without the flag drops it to 0 -> deleting a
session removes its rows but leaves transcript bytes recoverable in state.db. Reproduced
end-to-end via api/models.py delete path. Add CPPFLAGS=-DSQLITE_SECURE_DELETE and a
build-time assertion that PRAGMA secure_delete==1 (fails the image build if not).

Co-authored-by: qxxaa <qxxaa@users.noreply.github.com>

---------

Co-authored-by: qxxaa <qxxaa@users.noreply.github.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-08-14 20:14:37 -07:00
nesquena-hermesandnesquena-hermes 7506884d29 Release: bound long-session memory to prevent tab-focus OOM (#7006, @webtecnica) (#7042)
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-08-14 19:44:38 -07:00
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>
2026-08-14 19:42:15 -07:00
nesquena-hermesandnesquena-hermes 95f05c5147 Release: composer auto-dismisses transient Reconnected status pill (#7028, @starship-s) (#7041)
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-08-14 18:34:28 -07:00
starship-sandnesquena-hermes 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>
2026-08-14 18:27:00 -07:00
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>
2026-08-14 18:51:41 +00:00
nesquena-hermesandnesquena-hermes ae075ba188 docs(changelog): boot script no longer fabricates appearance choice on fresh browser (#6808) (#7029)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-14 18:41:06 +00:00
nesquena-hermesandnesquena-hermes dd44aa9830 docs(changelog): ja-locale profile wording consistency (#6762) (#7026)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-14 16:48:28 +00:00
asakuraandnesquena-hermes 155838f2c3 fix(i18n): unify ja locale profile wording (プロフィール → プロファイル) (#6762)
Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
2026-08-14 16:43:19 +00:00
nesquena-hermesandnesquena-hermes 0dc9cc381c docs(changelog): suppress reserved [SILENT] control turns at ingress (#7018) (#7020)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-14 12:41:19 +00:00
6d6560f286 fix(chat): suppress reserved SILENT control turns at ingress (#7018)
Cron agents use the exact final response `[SILENT]` as a delivery-suppression
sentinel. If a wake relay accidentally POSTs that sentinel to `/api/chat/start`
and 8701 restarts while the turn is pending, session repair materializes it as
a visible `{role: user, _recovered: true}` message. The pending value can then
be recovered again on later restarts, creating repeated `[SILENT]` turns.

Treat the exact normalized sentinel as a successful no-op at both server-side
turn entry points: the HTTP `/api/chat/start` handler and
`start_session_turn`. Both checks run before session lookup, runtime barriers,
or pending-state mutation. Matching is deliberately exact and case-sensitive,
so `[silent]`, prose containing `[SILENT]`, and ordinary user text are not
suppressed.

Add regression tests proving both paths return HTTP/status 200 without session
lookup, plus negative cases for non-exact text. The new tests fail 3/3 before
the fix. Targeted and neighbouring chat-start suites pass 21/21.

Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
2026-08-14 12:39:20 +00:00
nesquena-hermesandnesquena-hermes 91356b7c3f docs(changelog): kanban sidebar-eviction fix (#6998) (#7016)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-14 10:41:12 +00:00
9b8a9e05d1 fix(sidebar): prevent kanban runs from hiding CLI sessions (#6998)
* fix: prevent kanban rows from hiding CLI sessions

Keep worker-heavy Kanban history out of the bounded interactive session window and recover it through a separate capped pass. Add regression coverage and update query-pass contract tests.

* docs: describe imported session projection bounds

---------

Co-authored-by: zicochaos <23367003+zicochaos@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
2026-08-14 10:39:36 +00:00
nesquena-hermesandnesquena-hermes b58ea15e31 docs(changelog): translate workspace-sort keys in 14 locales (#6910) (#7014)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-14 05:40:23 +00:00
webtecnicaandnesquena-hermes 3d18a8ca5a i18n: translate workspace-sort keys in 14 non-English locales (#6902) (#6910)
Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
2026-08-13 22:39:25 -07:00
nesquena-hermesandnesquena-hermes 69fb748f73 docs(changelog): streaming session-scope render guard (#6502) (#7012)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-14 05:03:33 +00:00
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>
2026-08-13 22:02:33 -07:00
nesquena-hermesandnesquena-hermes fe8fc067aa docs(changelog): first-response jump scroll-settle fix (#6621) (#7011)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-14 04:18:49 +00:00
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>
2026-08-13 21:17:42 -07:00
nesquena-hermesandnesquena-hermes 5a68684fc7 docs(changelog): 402 exhausted-TTL fast recovery (#6626) (#7010)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-14 03:16:43 +00:00
webtecnicaandnesquena-hermes 7a5e6bc27a fix(providers): reduce pool-exhaustion TTL for HTTP 402 from 1h to 2min (#6626)
* fix(providers): reduce pool-exhaustion TTL for HTTP 402 from 1h to 2min

Mirrors the agent-side fix: when a provider returns HTTP 402 (Payment
Required), the credential pool marks the key as exhausted with a 1-hour
TTL — even though recharging credits restores the key immediately.

Both copies of _entry_exhausted_ttl_seconds() now handle HTTP 402 with
a 2-minute TTL, matching the new EXHAUSTED_TTL_402_SECONDS constant in
agent/credential_pool.py.

* fix(providers): tie HTTP 402 TTL to installed runtime contract (#6626)

* test(providers): behavior-test 402 TTL 119/120s boundary on both helper copies (#6626)

Add executed boundary tests for the HTTP 402 pool-exhaustion TTL:

- Host helper: with the runtime TTL monkeypatched to 120, an exhausted
  entry is still unavailable at 119s and becomes available at 120s, for
  both the integer 402 and the persisted string "402" error codes.
- Embedded _ACCOUNT_USAGE_SUBPROCESS_CODE copy: execute the subprocess
  code under the existing quota-subprocess harness and assert the same
  TTL and 119/120s eligibility boundary against the embedded helper,
  instead of counting source-string occurrences.
- Keep the legacy 3600s fallback and last_error_reset_at precedence
  assertions.

* test(providers): make 402 TTL parity tests self-contained for standalone CI

The parity tests imported agent.credential_pool directly, but the WebUI
test suite runs standalone (Hermes Agent runtime is not installed in CI),
causing ModuleNotFoundError on every shard since 08-02. Simulate the
runtime contract with a fake agent.credential_pool module (same pattern
already used by the embedded-subprocess helper test) so both the 120s
cooldown and the legacy 1h fallback paths are covered.

---------

Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
2026-08-13 20:15:30 -07:00
nesquena-hermesandnesquena-hermes 7c1239dbe7 docs(changelog): stamp v0.52.113 stable section (promotion) (#7009)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-13 19:43:40 -07:00
5b0246de61 Release: preserve intentional transcript shrinks (#6954) (#6997)
* fix(session): preserve intentional transcript shrinks

* fix(session): validate shrink generations

* docs(changelog): stamp #6954 (preserve intentional transcript shrinks)

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-13 20:44:06 +00:00
07118df53c Release: multi-container Docker gateway API forwarding (#6917) (#6995)
* feat(docker): enable gateway API and password forwarding in compose files

- Added API_SERVER_KEY forwarding to enable the gateway API listener in multi-container setups.
- Configured HERMES_WEBUI_PASSWORD to ensure password authentication works as expected.
- Updated documentation to clarify API_SERVER_KEY requirements and forwarding behavior.
- Added regression tests to cover the new functionality and ensure proper configuration.

* build(docker): force LF line endings for shell scripts via .gitattributes

A CRLF shebang (#!/bin/bash\r) breaks locally-built Docker images: the
kernel can't resolve the interpreter /bin/bash\r, so 'docker compose up
-d --build' on Windows with core.autocrlf=true fails with
'exec /hermeswebui_init.bash: no such file or directory'. The repo blobs
and CI/GHCR builds are LF and unaffected; only Windows local builds
baked the CRLF checkout into the image.

* fixup(docker): dedup API_SERVER_HOST after rebase onto #6968 (which already added it)

* docs(changelog): stamp #6917 (multi-container gateway API forwarding)

---------

Co-authored-by: Yang <yang_qwq@qq.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-13 18:21:14 +00:00
0b69346970 Release: provider-filter + memory diagnostics (#6338 #6363) (#6994)
* fix(#6335): filter unconfigured providers from model selector

When a known provider (e.g. openai-api) is listed in config.yaml's
providers section with only metadata (name, display_name) and no route
configuration or credentials, it should not appear in the chat model
selector dropdown.

Previously, any provider recognised as a 'known provider' (present in
_PROVIDER_MODELS or _PROVIDER_DISPLAY) would be admitted to the picker
simply by being listed in the config.yaml providers block — even after
the user removed its API key.  This caused a persistent ghost provider
entry in the dropdown (#6335).

The fix gates known-provider admission on credential evidence: the
provider must already have been detected from a credential source (env
vars, hermes auth, or credential pool) OR carry explicit route
configuration (api/base_url/api_key/key_env) OR be the active provider
with a configured models allowlist.

* fix(#6338): normalize provider aliases in credential check

The credential check compared _canonical (config key like 'x-ai',
'google') directly against detected_providers (alias-resolved values
like 'xai', 'gemini').  Alias-named authenticated providers were
therefore incorrectly treated as unconfigured and removed from the
model selector.

Fix: resolve _canonical through _resolve_provider_alias() before the
membership test, so config keys like 'x-ai' match credential evidence
reported as 'xai'.  Also check the raw canonical for cases where the
config key is itself the alias target.

Refs: #6335, #6338

* fix(#6338): normalize detected_providers through alias resolver

Normalize both _canonical and detected_providers entries through
_resolve_provider_alias before the membership test, so that a WebUI
canonical form in detected_providers (e.g. x-ai added by a prior
loop iteration) also matches the alias-resolved form used in
credential detection (e.g. xai from hermes_cli).

This is belt-and-braces on the existing _resolve_provider_alias(_canonical)
check: both sides now map into the same alias-resolved namespace,
closing any edge case where detected_providers contains a WebUI-canonical
form that happens to differ from the alias target.

* test(ui): catalog-admission regression for credentialed providers (#6338)

* test(ui): pin config path state in catalog-admission regression (#6338)

* fix(system-health): expose runtime memory diagnostics without blocking owner locks (#6351)

* fix(system-health): keep owner contention local to runtime status (#6351)

* docs(changelog): stamp #6338 #6363 (certified batch)

---------

Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>
Co-authored-by: webtecnica <webtecnica@gmail.com>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-13 17:47:46 +00:00
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>
2026-08-13 17:40:54 +00:00
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>
2026-08-13 17:08:40 +00:00
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>
2026-08-13 16:41:32 +00:00
nesquena-hermes 7d0bc4b6a5 Merge pull request #6981 from nesquena/release/stage-6979-1786619874
Release: per-message media preview snapshots (#6979)
2026-08-13 04:23:23 -07:00
nesquena-hermesandhappy5318 d0ba72035e docs(changelog): stamp #6979 per-message media preview snapshots
Co-authored-by: happy5318 <happy5318@users.noreply.github.com>
2026-08-13 11:17:54 +00:00
nesquena-hermes 0f1ae89082 Merge remote-tracking branch 'origin/master' into HEAD 2026-08-13 10:48:34 +00:00
happy5318 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).
2026-08-13 18:02:03 +08:00
happy5318 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.
2026-08-13 16:40:24 +08:00
happy5318 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.
2026-08-13 16:13:03 +08:00
happy5318 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.
2026-08-13 15:48:17 +08:00
nesquena-hermes 0fc0ebb2b7 Merge pull request #6977 from nesquena/release/stage-6968-1786605449
Release: multi-container Docker agent API reachability (#6968)
2026-08-13 00:23:39 -07:00
nesquena-hermesandbragov4ik fa54dbc9a2 docs(changelog): stamp #6968 multi-container Docker API_SERVER_HOST fix
Co-authored-by: bragov4ik <bragov4ik@users.noreply.github.com>
2026-08-13 07:17:29 +00:00
nesquena-hermes 0f6b9e674d Merge remote-tracking branch 'origin/master' into HEAD 2026-08-13 07:07:47 +00:00
nesquena-hermes 8540583d95 Merge pull request #6971 from nesquena/release/stage-6922-1786592110
Release: media ETag revalidation + post-commit error containment (#6922)
2026-08-12 20:40:31 -07:00
nesquena-hermesandhappy5318 b87d0179fc docs(changelog): stamp #6922 media ETag revalidation + post-commit error containment
Co-authored-by: happy5318 <happy5318@users.noreply.github.com>
2026-08-13 03:35:10 +00:00
nesquena-hermes e510219e16 Merge remote-tracking branch 'origin/master' into HEAD 2026-08-13 02:45:27 +00:00
happy5318 8228dbe4da fix: contain ALL post-commit body errors, not just disconnects
The post-commit body-transmission block only caught
_CLIENT_DISCONNECT_ERRORS. A non-disconnect error during body write —
a generic OSError/EIO from f.read(), or a PermissionError — escaped
_serve_file_bytes entirely, and Handler.do_GET (server.py:388-395)
caught it and emitted a second status line after the already-committed
200/206, corrupting the HTTP stream.

Broaden the post-commit except to swallow every Exception (keeping the
disconnect branch for its debug message). Regression tests drive both
the snapshot and streaming paths with generic OSError/EIO and
PermissionError mid-body, asserting the status sequence is exactly
[200] with no trailing 500.
2026-08-13 09:49:34 +08:00
nesquena-hermes f3e0e29815 Merge pull request #6969 from nesquena/release/stage-6966-1786581547
Release: de-dup session title on collision (#6964)
2026-08-12 17:44:22 -07:00
nesquena-hermesandwebtecnica fac3470668 docs(changelog): stamp #6964 title-dedup + add manual-rename-during-retry test
Codex SHOULD-FIX: add a committed test proving the de-dup retry variant never
clobbers a user-source manual rename (retry goes through the same provenance
rank guard). Additive test-only; no behavior change to the contributor's fix.

Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>
2026-08-13 00:39:07 +00:00
nesquena-hermes 636a543853 Merge remote-tracking branch 'origin/master' into HEAD 2026-08-13 00:25:19 +00:00
Kirill Ivanov 821f45dbb6 Fix two containers 2026-08-12 23:23:18 +00:00