Skip to content

fix: test harness _extract_js_function handles regex with } in char class (#6890) - #6

Open
webtecnica wants to merge 1123 commits into
mainfrom
fix/6890-media-token-markdown
Open

fix: test harness _extract_js_function handles regex with } in char class (#6890)#6
webtecnica wants to merge 1123 commits into
mainfrom
fix/6890-media-token-markdown

Conversation

@webtecnica

Copy link
Copy Markdown
Owner

Summary

Fixes the test harness _extract_js_function brace counter to handle regex literals with } inside character classes (e.g. /[*_\.,;:!'"\u2018\u2019\u201c\u201d\u2026)]}>]+$/instatic/ui.js:2669`).

Problem

The naive brace counter in _extract_js_function (in tests/test_smd_media_in_stream.py) incorrectly decremented depth when encountering } inside regex character classes, causing function extraction to fail.

Solution

Rewrote _extract_js_function to track context (single/double-quoted strings, template literals, regex literals, line/block comments, and escape sequences) so braces inside literals/comments are ignored for depth counting.

Testing

All 35 tests in tests/test_smd_media_in_stream.py pass, including the 4 new nesquena#6890 tests:

  • test_real_smd_parser_strips_trailing_emphasis_mid_stream
  • test_real_smd_parser_strips_trailing_emphasis_at_stream_end
  • test_real_smd_parser_strips_trailing_comma
  • test_real_smd_parser_strips_trailing_period

Fixes nesquena#6890

nesquena-hermes and others added 30 commits July 31, 2026 11:52
…-gateway-terminal-persistence

fix: preserve gateway terminal error recovery after save failure
…inal-character

fix(streaming): complete interim anchor prose before tools
Move the staged agent source from /tmp/hermes-agent-build (ephemeral
tmpfs) to /app/hermes-agent-src (persistent, same lifecycle as the
venv). The editable install records this path in a .pth file - placing
it alongside the venv ensures both survive container restarts and are
lost together on recreation, eliminating the dangling-link class of
bugs without sentinel files or state machines.

Three states now handled by lifecycle equivalence:
- First boot: stages source, installs editable, writes .deps_installed
- Restart (same container): .deps_installed + staged source both present
- Recreation (new container): both gone, fresh boot re-stages
While a turn is live the server can reconcile that turn's user row into the
transcript returned by /api/session (the sidecar/state.db merge picks it up
from state.db, where the agent writes it immediately) while
pending_user_message is still set. The row is then followed by the same
turn's assistant/tool rows.

_pendingCurrentTailUserMessage() scans the tail backwards and returns null as
soon as it meets a completed assistant, so in that state it reports "the
current turn has no user row". getPendingSessionMessage() then materializes
pending_user_message a second time and the pane renders two identical user
bubbles until the settle render replaces the list wholesale (which is why the
duplicate disappears when the answer lands, and why nothing wrong is ever
persisted).

Scanning past assistant/tool rows on text alone would regress nesquena#3300: a user
who submits the same text twice in a row legitimately gets two identical user
turns. Use pending_started_at as the discriminator instead — the server stamps
the active turn's user row with it, and no earlier turn can share it. Text
equality is still required, so a false match needs identical text AND a
near-identical timestamp. Absent/invalid pending_started_at disables the
fallback and keeps the previous strict-tail behaviour.

Verified: the new midRunReloadDedupe assertion fails on the unpatched ui.js
and passes with the fix; the existing repeat-prompt, compaction-boundary and
strict-tail assertions still pass.
Claude Code sessions listed in the sidebar rendered "Session not available
in web UI." when clicked under a named (non-root) profile.

Root cause: get_claude_code_sessions() scans ~/.claude/projects and stamps
profile=None on every row, because those JSONL transcripts belong to no
Hermes profile. /api/sessions lists them regardless of active profile, but
the GET /api/session detail load ran them through
_session_visible_to_active_profile, whose _profiles_match coerces None ->
'default'. With hermes_profile=feng-family active, the gate 404'd before
_claim_or_synthesize_cli_session ever ran, so the frontend hit its 404
branch and painted the empty-state message. POST /api/session/import_cli
correctly returned 200 with an inline read-only payload (Claude Code
sessions are read_only by design and never get a sidecar), which is why no
file appeared in webui/sessions/ -- Session.save() was never supposed to
run and was not silently failing.

Exempt profile-less claude_code rows from the detail-load profile gate via
_is_profile_agnostic_foreign_session(). Profile-tagged foreign rows keep
the nesquena#5419 409 cross-profile contract.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tings init

Browser race in static/ui.js: _autoScrollFollow can be undefined when
scroll/scrollpin listeners fire before boot.js initializes settings.
Bare global reference (ReferenceError) occurs on the golden path.

Fix: wrap both consumer sites with a typeof guard that defaults to
true (matching the boot.js fallback) when the variable hasn't been set.

Closes nesquena#6606
…esquena#6066)

The frontend re-derived partition rank from the serialized type, so FIFO,
socket, and device entries moved into the regular-file partition on non-default
sorts. Emit workspace_sort_rank from list_dir, consume it in the rank helper,
and keep it out of dir_signature.
The /api/media endpoint served all file types with Cache-Control:
private, max-age=3600. For HTML inline previews (rendered via
loadHtmlInline() → fetch() → iframe srcdoc), this meant the browser
cached the response for 1 hour. When an agent edited and re-rendered
the same HTML file, the inline preview showed stale content — only a
manual full-page refresh revealed the update.

Two changes:
- Backend: serve text/html with no-store instead of max-age=3600
  (images and other media keep the 1-hour cache)
- Frontend: loadHtmlInline() fetch with {cache:'no-store'} for
  belt-and-suspenders defense against intermediate caches

Note: the /api/file/raw path already used no-store for HTML; this
fixes the /api/media path which was missed.
Per review feedback:
- Frontend: assert exact fetch(mediaUrl, {cache:'no-store'}) in
  loadHtmlInline body, not just 'fetch(mediaUrl' prefix (too broad,
  other fetch calls match the prefix)
- Backend: assert Cache-Control: no-store on both attachment and inline
  HTML responses in test_html_media_endpoint_inline_requires_csp_sandbox
- Both assertions fail independently if either the frontend no-store
  option or backend no-store policy is reverted
…oximity

Review round 2: the 1.5s timestamp tolerance over-deduped a legitimate
rapid repeat — two identical-text turns <1.5s apart collapsed into one,
hiding the second turn and copying its attachments onto the earlier row
(the mirror risk of nesquena#6649).

_pendingActiveTurnUserMessage now matches only on unambiguous identity:
- the row carries the server-stamped _active_turn_token (stream_id +
  started_at, per build_active_turn_token), or
- its timestamp equals pending_started_at within a precision-only
  epsilon (1e-6, absorbs float/state.db drift, never a full second).

Anything wider (whole-second truncation, ~1s rapid repeat) returns null
so getPendingSessionMessage() materializes the pending turn — fail
toward the harmless transient duplicate the settle render clears.

Regressions added: two completed identical-text turns ~1s apart → second
pending row returned + first row's attachments untouched; token-identity
row adopted even when its timestamp is outside the epsilon.
webtecnica and others added 29 commits August 13, 2026 22:39
…na#6902) (nesquena#6910)

Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
…a#6910) (nesquena#7014)

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
…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>
…a#7016)

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
…a#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>
…esquena#7018) (nesquena#7020)

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
nesquena#7026)

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
…n fresh browser (nesquena#6808) (nesquena#7029)

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
nesquena#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>
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>
* 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 nesquena#6999

* fix(ui): full turn context + collision-free cache hash (nesquena#7006 review rework)

Addresses the maintainer's CHANGES_REQUESTED on PR nesquena#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 nesquena#6999

* fix(ui): coalesce session-updated under refresh guard; insertion-order cache hash

Re-gate round 2 for nesquena#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

nesquena#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>
nesquena#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>
…te preserved) (nesquena#6900, @qxxaa) (nesquena#7043)

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
…follow-up to nesquena#6900 (nesquena#7044)

* fix(docker): register /usr/local/lib in ld.so.conf.d so the source-built SQLite loads on arm64

The nesquena#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 nesquena#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>
…a#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 nesquena#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 nesquena#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 nesquena#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 nesquena#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 nesquena#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 nesquena#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 nesquena#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(nesquena#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).

- nesquena#7019 (@webtecnica) suppress reserved [SILENT] sentinel at /api/goal
- nesquena#7031 (@carlotestor) keep evicted subagent parents in the sidebar import window
- nesquena#7030 (@webtecnica) share.html first-visit appearance guard + skin-id sync
- nesquena#6853 (@rodboev) exclude repo-root AGENTS.md from the Docker runtime image
- nesquena#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 (nesquena#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>
…xperimental) (nesquena#7090)

* feat: add GLM-5.3 to Z.AI model list

Add glm-5.3 as the newest zai entry in _PROVIDER_MODELS and a matching
zai/glm-5.3 entry in _FALLBACK_MODELS, and bump the Z.AI onboarding
default_model from glm-5.1 to glm-5.3 (Z.ai's current flagship; legacy
GLM-5.2/5.1 requests are routed to GLM-5.3 per docs.z.ai).

Reasoning gating needs no change: _zai_glm_classification() treats
GLM >= 5.2 as effort-ladder capable, so glm-5.3 is already covered and
pinned by tests/test_zai_reasoning_effort_gating.py.

New regression coverage in tests/test_glm_5_3_catalog.py: catalog
presence, newest-first ordering, fallback entry, onboarding default,
full reasoning_effort ladder, and the get_available_models() payload.

* test: restore _cfg_fingerprint in catalog test fixture

Review follow-up (nesquena#7017): the isolation fixture snapshot restored cfg,
_cfg_mtime, and _cfg_path but left _cfg_fingerprint pointing at the
temporary config loaded by the payload test. api/config.py uses that
fingerprint to distinguish in-memory overrides from changed files
(config.py:371), so a stale value could make later same-process tests
skip reloading a changed config. Snapshot and restore it like the rest.

* fix: keep Z.AI onboarding default at glm-5.1 until direct API serves GLM-5.3

Review follow-up (nesquena#7017): GLM-5.3 is live on Z.ai's Coding Plan
endpoint only; the direct api.z.ai endpoint the zai provider uses
still lists the GLM-5.3 API as coming soon. Defaulting new direct-API
users onto glm-5.3 would fail their first message, so the catalog
addition stays (opt-in) and the default stays glm-5.1. Bump the
default in a follow-up once the direct endpoint serves GLM-5.3.

* fix(docker): probe the configured state dir before /workspace for UID/GID (nesquena#7027)

UID/GID auto-detection probed /workspace before the configured
HERMES_WEBUI_STATE_DIR. In a stock single-container image none of the
priority-1 candidates exist, but /workspace does — owned by the image's
build-time 1024:1024. Detection therefore returned the image's own owner,
which carries no information about the host, while the one directory whose
owner *is* the host UID by definition — the state-dir bind mount — was never
probed. With a host-owned state mount and no explicit WANTED_UID the
container remapped to 1024, failed its own state-dir writability check, and
restart-looped.

The log line made this expensive to debug: 1024 is also the fallback default,
so "Auto-detected workspace UID: 1024" read as if detection had found nothing.

- probe ${HERMES_WEBUI_STATE_DIR:-/app/data} first, for both UID and GID
- keep /workspace as a lower-priority signal (unchanged for setups that
  actually bind-mount it), and keep the hermes-home probes from nesquena#668
- stop treating an explicitly supplied 1024 as "unset": the sentinel and a
  valid UID were the same number, so an operator who deliberately ran as 1024
  got it overwritten by detection. The explicit/detected origin is persisted
  next to the value because `su` drops the environment when the script
  re-enters as the runtime user, so the second pass would otherwise see an
  explicit choice as a detected one.

Tests: tests/test_7027_state_dir_uid_probe.py runs the real resolution block
under bash with `stat` stubbed, covering the non-1024 state mount, the
explicit 1024 override (including across the privilege drop), and the
pre-existing /workspace + hermes-home fallback paths. A new state-dir-uid job
in the Docker smoke workflow boots a real container on a host-owned state
mount and gates on /health.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Release batch B: Docker UID fix + GLM-5.3 catalog (experimental)

Two independently gate-passed contributor PRs, rebased fresh onto master and
re-gated as a combined stage (Codex SAFE TO SHIP; full suite green except the 8
pre-existing approval-test failures that fail identically on clean origin/master
— CI green on the same commit; tracked separately for a fix).

- nesquena#7027 (@jorgejiro) probe configured state-dir before /workspace for Docker
  UID/GID; persist explicit marker so a supplied 1024 survives root->su re-entry
  (fixes the single-container restart loop) (nesquena#7034)
- nesquena#7017 (@rh-id) add GLM-5.3 to the Z.AI model list (onboarding default stays glm-5.1)

Co-authored-by: jorgejiro <jorgejiro@users.noreply.github.com>
Co-authored-by: rh-id <rh-id@users.noreply.github.com>

---------

Co-authored-by: Ruby Hartono <58564005+rh-id@users.noreply.github.com>
Co-authored-by: Jorge <jorgejiro@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: n <a@n>
Co-authored-by: jorgejiro <jorgejiro@users.noreply.github.com>
Co-authored-by: rh-id <rh-id@users.noreply.github.com>
…uest_id (nesquena#7093)

submit_gateway_pending_mirror() matches an incoming approval back to its
queued producer entry by object identity (entry.data is approval) then by
approval_id. On the LOCAL backend neither holds: the installed hermes-agent
core delivers the approval to WebUI's notify callback as a COPY
(notify_cb(dict(entry.data))), so identity fails, and a local _ApprovalEntry
stamps a request_id but no approval_id, so the approval_id fallback misses.
The mirror is then created with no token, reconcile keeps the orphan, and
_session_has_pending_approval stays True after the entry is dropped — the
stale-approval-card dead-end (nesquena#4948 local variant): clicking a card whose
approval is gone returns a bare ok:false the UI renders as "Approval response
not accepted." with a stuck card.

Add a request_id fallback (gated on the local not-run_id path) that reunites
the notified copy with its queued entry via the request_id the core stamps
once on the source and preserves through the copy.

The four HTTP tests in test_approval_unblock.py were unfaithful to production:
they built _ApprovalEntry(approval) (stamping request_id onto a copy in
entry.data, not onto the source approval) then submitted the pre-stamp source.
They now submit dict(entry.data), matching what the core actually passes. The
assertions are unchanged; with the fidelity fix but the production change
reverted, the tests fail — so they detect the real regression, not vacuously.

This class is invisible on CI, which never installs the agent core, so both
approval test files skipif-skip entirely and their green never exercised these
paths.

Co-authored-by: n <a@n>
…esquena#7065)

* fix(nesquena#6906): tighten Kanban modal viewport regression

* fix(nesquena#6906): preserve modal centering fallbacks

* docs(changelog): note the nesquena#6906 Kanban modal height-cap fix

---------

Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
Co-authored-by: n <a@n>
…esquena#7067)

* fix(nesquena#6867): preserve artifact ownership across compression rotation

* docs(changelog): note the nesquena#6867 artifacts-panel ownership-guard fix

---------

Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
Co-authored-by: n <a@n>
…l font (nesquena#6871), mobile nav order (nesquena#6909) (nesquena#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 nesquena#7077 O_BINARY, nesquena#6871 native-control font, nesquena#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>
… + GPT-5.6 max reasoning (nesquena#7083) + test order-independence (nesquena#7101) (nesquena#7102)

* fix(tests): make mtime_invalidation and glm_5_3 tests order-independent (nesquena#7100)

* fix: expose max reasoning for GPT-5.6 models

* fix(recovery): do not reattach cancelling runs

ACTIVE_RUNS tracks worker lifecycle, which is deliberately broader than
"a turn a browser may attach to". cancel_stream() keeps the row as
phase="cancelling" while the worker unwinds so a successor turn cannot
start on top of it, but the client has already reached a terminal state
for that stream: its run journal ends in a terminal event.

The recovery lookups treated every same-session ACTIVE_RUNS row as
attachable. An idle session holding a cancelling row therefore received a
recovered server_turn_started on every /api/session/stream subscription:
the client attached, replayed the terminal event, tore the renderer down,
resubscribed, and the server replayed the same frame again. The result is
an endless attach/replay loop that rebuilds the transcript repeatedly.

Separate the two meanings instead of narrowing one call site:

- api/config.py gains active_run_is_attachable() and
  active_run_cancel_is_stale() as the shared predicates.
- active_stream_id_for_session() (browser recovery) returns attachable
  rows only; _session_has_active_turn() (busy check) keeps counting a
  fresh cancellation so a successor cannot overlap the unwinding worker.
- _live_active_stream_id() applies the same rule to the hidden-tab
  status poller, on both the STREAMS and ACTIVE_RUNS paths.
- routes._cancelled_run_is_stale() now delegates to the shared predicate
  rather than keeping a parallel copy of the staleness rule.
- A cancelling row past a bounded unwind window with no live STREAMS
  channel is reclaimed from ACTIVE_RUNS and its stream owner released, so
  a wedged worker cannot suppress background wakeups forever. Age alone
  does not reclaim a row that still owns a live channel.

Staleness anchors on cancelled_at, falling back to started_at, so a
long-running turn cancelled moments ago is never treated as an orphan.

tests/test_cancelling_run_not_attachable.py covers both directions of
each rule. The six behavioral tests fail on the unpatched tree and pass
with the fix; two targeted mutations (forcing the attachability predicate
true, and disabling the staleness reaper) each turn the suite red.

* docs(rfc): document cancellation attach/admission split

Records the runtime contract introduced by the recovery fix in the WebUI
run-state consistency RFC, so the distinction is discoverable instead of
living only in code comments.

- Adds ACTIVE_RUNS to the State Layers table as the worker-lifecycle
  registry, explicitly not the set of runs a browser may attach to.
- Adds invariant 9: lifecycle-busy is not client-attachable. Cancellation
  splits the two meanings, recovery paths must exclude cancelling rows,
  and admission checks must keep counting them.
- Documents the bounded cancellation-unwind window: reclamation needs
  both age and the absence of a live STREAMS channel, and staleness is
  anchored on the cancellation timestamp.
- Extends the review checklist with the admission-vs-attachment question
  and the evidence required when changing a reclamation window.

* docs(changelog): note nesquena#7096 cancelling-run reattach, nesquena#7083 GPT-5.6 max reasoning, nesquena#7101 test order-independence

---------

Co-authored-by: webtecnica <webtecnica@gmail.com>
Co-authored-by: Abdulrahman Elkenany <boudy.elkenany123@gmail.com>
Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
Co-authored-by: n <a@n>
…CP SDK pin (nesquena#6616) (nesquena#7103)

* fix(models): normalize dotted Bedrock/Vertex model IDs in labels

Split out of nesquena#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 nesquena#3429
harnesses extract that function's source and eval it in isolation, so a
module-level helper NameErrors there. The nesquena#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 dd7f6ac, claude-opus-5
(no dot, untouched here) already labels 'claude-opus-5' in JS vs
'Claude Opus 5' in Python, and openai/gpt-4o 'GPT-4o' vs 'GPT 4O'.
getModelLabel() checks _dynamicModelLabels first (ui.js:6991), populated
from the server label (ui.js:3483,3494,3606), so the backend wins once a
catalog loads; the JS formatter is the pre-fetch fallback.

Dropping the JS post-normalization instead would regress the picker to raw
ids before catalog load (us.anthropic.claude-sonnet-4-5 -> 'claude-sonnet-4-5'
instead of 'Sonnet 4.5').

Mutation-checked: JS strip no-op fails 2, retry chain deleted fails 1,
'global' dropped from backend _regions fails 1. Region set derived from
api/config.py source rather than retyped.

No production code changed.

* fix(test): pin mcp SDK to compatible 1.x range with bootstrap guard (nesquena#6602)

Change  to  in requirements-dev.txt and the
mcp_server.py docstring, and add a bootstrap guard in mcp_server.py
that checks Server.list_tools existence at module load time.

The guard fails fast with a clear error message if an incompatible
mcp SDK (2.x) is installed, instead of producing dozens of
secondary errors at test collection/setup. This completes the
mitigation started in PR nesquena#6564 by adding a minimum version floor
and an import-time compatibility check.

* docs(changelog): note nesquena#6628 dotted model labels, nesquena#6616 mcp SDK pin

---------

Co-authored-by: Sam Painter <samfp@amazon.com>
Co-authored-by: webtecnica <webtecnica@gmail.com>
Co-authored-by: n <a@n>
…+ hermes-webui entry point (nesquena#6742) (nesquena#7108)

* fix(goals): delegate profile evaluation to native manager

Use Hermes' context-local home override to run profile-scoped goal operations through the native GoalManager, preserving current judge, wait, and failure semantics while retaining the explicit-DB legacy fallback.

* docs(goals): describe profile ownership boundary

* fix(goals): gate native profile persistence capability

* feat(cli): add packaged hermes-webui CLI entry point (nesquena#6739)

* fix(cli): route hermes-webui entry through bootstrap:main for wheel install (nesquena#6742)

* docs(changelog): note nesquena#6899 profile goal isolation + nesquena#6742 hermes-webui entry point

* test(goals): guard hermes_cli import with importorskip for CI (agent not installed)

nesquena#6899's native-contract tests imported hermes_cli unconditionally, failing
CI with ModuleNotFoundError. Match the repo's established importorskip pattern
so they skip cleanly when the agent isn't installed and run when it is.
Co-authored-by: ticketclosed-wontfix

---------

Co-authored-by: Nick <202622897+ticketclosed-wontfix@users.noreply.github.com>
Co-authored-by: webtecnica <webtecnica@gmail.com>
Co-authored-by: n <a@n>
…lass

The naive brace counter in _extract_js_function broke on regex literals
containing } inside character classes (e.g. /[*_`.,;:!'"\\u2018\\u2019\\u201c\\u201d\\u2026)\\]}\\>]+$/).

Fixed by tracking string/regex/template/comment context so braces inside
literals are ignored for depth counting.
webtecnica added a commit that referenced this pull request Aug 25, 2026
…SE relay (nesquena#6961)

Read/surface half of the maintainer's split for PR nesquena#6961 (child approval
routing, nesquena#6943). The resolve half (#1/#2/#3) stays in a follow-up gated on
the agent contract (agent#82009).

#4 (CORE): scope the child->parent cache by canonical state-db/profile path
and only cache positive lookups, so a miss under one profile can no longer
poison another profile's identical child id, and a late state.db write is
picked up on the next lookup. Adds invalidate_child_parent_cache().

#5 (SILENT): use one aggregate projection (own queue + delegated-child
queues, deduped by stable approval id / gateway mirror token) unconditionally
on all three surface paths — sidebar attention summary, /api/approval/pending,
and the SSE initial snapshot — so a parent-with-1 + child-with-1 now reports
count 2 instead of 1.

#6 (SILENT): publish the aggregate parent head/count to the parent's SSE
subscribers whenever an owned child queue changes (submit_pending,
submit_gateway_pending_mirror, retire_gateway_pending_mirror,
resolve_gateway_pending_local, resolve_child_approval_locked), so a pure-SSE
parent consumer sees child enqueue/resolve without waiting for the 1.5s poll.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.