Skip to content

feat(chat): hide [[SILENT]] assistant turns (stacked on #6420) - #1

Open
ruizanthony wants to merge 11 commits into
feat/configurable-background-wakeupsfrom
feat/silent-wakeup-turn-filtering
Open

ruizanthony wants to merge 11 commits into
feat/configurable-background-wakeupsfrom
feat/silent-wakeup-turn-filtering

Conversation

@ruizanthony

Copy link
Copy Markdown
Owner

What

Stacked on nesquena#6420 — will be retargeted to upstream main after nesquena#6420 merges.

Each background-process wakeup (notify_on_complete) starts a full agent turn whose reply renders as a normal message. With several processes finishing in sequence, the intermediate acknowledgements pollute the thread between the user's messages and the one conclusion that matters (measured on real sessions: 85 wakeups → 85 visible intermediate replies across 40 recent sessions).

Contract (render-only — persisted data never mutated)

  • An assistant reply whose trimmed content is exactly [[SILENT]] is not rendered.
  • When a process_wakeup turn ends with the sentinel, the whole turn collapses (wakeup row + every assistant reply, including tool-carrying ones).
  • Hidden rows remain true turn boundaries (generalized _hasHiddenProcessWakeupBoundaryBefore; feat(wakeup): configure visibility and route compressed origins nesquena/hermes-webui#6420 semantics preserved when no sentinel is involved) — Compact Worklog finals, question mapping, regenerate ownership unaffected.
  • Live streaming: the live bubble hides as soon as the accumulated text is a prefix of the sentinel (data-silent-pending + CSS), driven from the per-token/interim chokepoint syncInflightAssistantMessage(); the exact sentinel never flashes at stream end.
  • Safety net: a reply that does not use the exact sentinel (or embeds it in real text) stays fully visible. If the agent never emits the token, behaviour is byte-identical to today.

No new setting: the filter is unconditional and agent-agnostic; the agent opts in per turn by answering exactly [[SILENT]].

Tests

  • New tests/test_silent_wakeup_turns.py: 11 Node-harness cases (full collapse, tool-carrying turns, concluding turns visible, mixed wakeups, lone sentinels, embedded sentinel, whitespace trimming, boundary/finals semantics, live prefix suppression, CSS+wiring presence).
  • tests/test_process_wakeup_rendering.py: harness updated to eval the new helpers (same pattern as other extract-based drivers).
  • Local: 117 passed across the wakeup/virtualization suites; node --check clean; ESLint runtime gate clean; scope_undef_gate clean.

Out of scope

Telegram/gateway surfaces (separate PR if wanted).

…ntract

11 Node-harness cases covering: full silent-turn collapse (plain and with
tool calls), concluding turns staying visible, mixed consecutive wakeups,
lone sentinel replies, sentinel embedded in real text, whitespace-trimmed
sentinel, hidden turns remaining true turn boundaries (finals/compact-worklog
semantics), live-stream prefix suppression semantics, and the CSS/token
wiring contract.
Background wakeups (notify_on_complete) each trigger a full agent turn whose
reply renders as a normal message. When several processes finish in sequence,
the intermediate acknowledgements pollute the thread between the user's
messages and the one conclusion that matters.

Contract (render-only, persisted data never mutated):
- an assistant reply whose trimmed content is exactly [[SILENT]] is hidden;
- when a process_wakeup turn ends with the sentinel, the whole turn
  (wakeup row + assistant replies incl. tool-carrying ones) collapses;
- hidden rows remain true turn boundaries via the generalized
  _hasHiddenProcessWakeupBoundaryBefore (preserves nesquena#6420 semantics when no
  sentinel is involved);
- while streaming, the live bubble hides as soon as the accumulated text is
  a prefix of the sentinel (data-silent-pending + CSS), driven from the
  per-token/interim chokepoint syncInflightAssistantMessage();
- safety net: a reply that does not use the exact sentinel stays visible.

No new setting: the filter is unconditional and agent-agnostic; the agent
opts in per-turn by answering exactly [[SILENT]].
@ruizanthony
ruizanthony force-pushed the feat/configurable-background-wakeups branch 7 times, most recently from 5454b41 to a0bdad2 Compare August 15, 2026 01:13
@ruizanthony
ruizanthony force-pushed the feat/configurable-background-wakeups branch 3 times, most recently from f7ca618 to 6898518 Compare August 24, 2026 23:57
@ruizanthony
ruizanthony force-pushed the feat/configurable-background-wakeups branch from 6898518 to 77a8927 Compare August 29, 2026 02:21
ruizanthony added a commit that referenced this pull request Aug 29, 2026
Replay repair runs on every session LOAD, not just on save, and it dominates
cold-load cost. It re-serializes every assistant message to canonical JSON and
SHA-256s it, for the session AND for every compaction ancestor walked by the
lineage — so a chat with 4 ancestors pays it five times per tab refresh.

A py-spy profile taken under 6 concurrent loads on a live deployment put
`_canonical_message_digest` as the #1 self-time frame. Because the work happens
under the GIL it does not overlap between tabs: one load took 0.55s while six
concurrent loads took 10.83s for the slowest (19.6x).

The work is also almost always for nothing. Sampling the 60 most recent
sidecars, repair changed nothing in 60 of 60 cases — 11.8s of pure waste. A
file only needs repairing once; a clean file stays clean until it is rewritten.

Memoize the NEGATIVE verdict only, keyed by the sha256 of the exact file bytes
(already computed by the primary load path for revision tracking). A hit means
"these exact bytes were proven to need no repair" and skips the pipeline. Any
write changes the digest and therefore misses the cache.

Measured on 6 real sidecars, same machine, warm:

  single load (median)          136ms -> 46ms   (2.9x)
  single load (6 sessions)      875ms -> 302ms  (2.9x)
  6 concurrent loads (wall)    1054ms -> 279ms  (3.8x)
  6 concurrent loads (slowest) 1053ms -> 277ms  (3.8x)

Safety. Skipping repair is equivalent to running it because the collapse
helpers never mutate their input: they build new lists and return
(result, changed), returning the input untouched when changed is False. A test
pins that property, so a future helper that starts mutating in place fails the
suite instead of silently serving unrepaired sessions. A positive verdict is
deliberately not cached: a file needing repair gets rewritten, so caching it
would key on bytes that no longer exist. The cache is bounded (512 entries,
LRU) and a missing digest forces the full pipeline.
ruizanthony added a commit that referenced this pull request Aug 30, 2026
Replay repair runs on every session LOAD, not just on save, and it dominates
cold-load cost. It re-serializes every assistant message to canonical JSON and
SHA-256s it, for the session AND for every compaction ancestor walked by the
lineage — so a chat with 4 ancestors pays it five times per tab refresh.

A py-spy profile taken under 6 concurrent loads on a live deployment put
`_canonical_message_digest` as the #1 self-time frame. Because the work happens
under the GIL it does not overlap between tabs: one load took 0.55s while six
concurrent loads took 10.83s for the slowest (19.6x).

The work is also almost always for nothing. Sampling the 60 most recent
sidecars, repair changed nothing in 60 of 60 cases — 11.8s of pure waste. A
file only needs repairing once; a clean file stays clean until it is rewritten.

Memoize the NEGATIVE verdict only, keyed by the sha256 of the exact file bytes
(already computed by the primary load path for revision tracking). A hit means
"these exact bytes were proven to need no repair" and skips the pipeline. Any
write changes the digest and therefore misses the cache.

Measured on 6 real sidecars, same machine, warm:

  single load (median)          136ms -> 46ms   (2.9x)
  single load (6 sessions)      875ms -> 302ms  (2.9x)
  6 concurrent loads (wall)    1054ms -> 279ms  (3.8x)
  6 concurrent loads (slowest) 1053ms -> 277ms  (3.8x)

Safety. Skipping repair is equivalent to running it because the collapse
helpers never mutate their input: they build new lists and return
(result, changed), returning the input untouched when changed is False. A test
pins that property, so a future helper that starts mutating in place fails the
suite instead of silently serving unrepaired sessions. A positive verdict is
deliberately not cached: a file needing repair gets rewritten, so caching it
would key on bytes that no longer exist. The cache is bounded (512 entries,
LRU) and a missing digest forces the full pipeline.
ruizanthony added a commit that referenced this pull request Sep 2, 2026
Replay repair runs on every session LOAD, not just on save, and it dominates
cold-load cost. It re-serializes every assistant message to canonical JSON and
SHA-256s it, for the session AND for every compaction ancestor walked by the
lineage — so a chat with 4 ancestors pays it five times per tab refresh.

A py-spy profile taken under 6 concurrent loads on a live deployment put
`_canonical_message_digest` as the #1 self-time frame. Because the work happens
under the GIL it does not overlap between tabs: one load took 0.55s while six
concurrent loads took 10.83s for the slowest (19.6x).

The work is also almost always for nothing. Sampling the 60 most recent
sidecars, repair changed nothing in 60 of 60 cases — 11.8s of pure waste. A
file only needs repairing once; a clean file stays clean until it is rewritten.

Memoize the NEGATIVE verdict only, keyed by the sha256 of the exact file bytes
(already computed by the primary load path for revision tracking). A hit means
"these exact bytes were proven to need no repair" and skips the pipeline. Any
write changes the digest and therefore misses the cache.

Measured on 6 real sidecars, same machine, warm:

  single load (median)          136ms -> 46ms   (2.9x)
  single load (6 sessions)      875ms -> 302ms  (2.9x)
  6 concurrent loads (wall)    1054ms -> 279ms  (3.8x)
  6 concurrent loads (slowest) 1053ms -> 277ms  (3.8x)

Safety. Skipping repair is equivalent to running it because the collapse
helpers never mutate their input: they build new lists and return
(result, changed), returning the input untouched when changed is False. A test
pins that property, so a future helper that starts mutating in place fails the
suite instead of silently serving unrepaired sessions. A positive verdict is
deliberately not cached: a file needing repair gets rewritten, so caching it
would key on bytes that no longer exist. The cache is bounded (512 entries,
LRU) and a missing digest forces the full pipeline.
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.

1 participant