Commit Graph
37 Commits
Author SHA1 Message Date
Anthony Ruiz e3db8f7e01 fix: settle steer on every terminal path and preserve recovery drafts 2026-09-20 17:07:58 +00:00
Anthony Ruiz 1ee4fc1601 fix: resolve durable compressed sessions before WebUI resume 2026-09-20 17:07:58 +00:00
Anthony Ruiz 034e3edef7 fix(streaming): publish initial run and cancellation signal atomically 2026-09-20 17:07:57 +00:00
Anthony Ruiz a98d0c86d0 fix(steer): fence worker registration and final pending drain 2026-09-20 17:07:57 +00:00
Anthony Ruiz 26277f933b fix(steer): revalidate the active-run backend on the cache-only path
The cache-only Steer path only special-cased backend == "gateway" and sent
every other value — including a missing, empty, or unknown backend — to the
cached local agent. Another runtime could own the run while guidance was
enqueued on a stale local agent.

Fail closed instead: the in-process worker now registers its active run with
the explicit local backend tag (WEBUI_LOCAL_CHAT_BACKEND, the same "legacy"
value webui_chat_backend_mode() returns), and cache-only Steer only enqueues
agent.steer() on that tag. Gateway keeps its gateway_steer_queued outcome; a
missing, empty, or foreign backend returns stream_dead without steering.

Extends the positive-ownership regression with backend cases and adds a
worker-side check that the local backend tag is actually registered.
2026-09-20 17:07:57 +00:00
Anthony Ruiz 20c16e9167 fix(steer): require positive ownership on the cache-only path
The cache-only Steer branch (no AGENT_INSTANCES worker) only rejected
*conflicting* ownership, so a missing stream owner or a missing
active-run session_id was treated as non-conflicting and let
agent.steer() run and report accepted without proving the requesting
session owns the run.

Fail closed instead: both the stream owner and the active-run session
must be present and strictly equal to the requesting session (backend
and phase are still revalidated) under the same STREAMS_LOCK ->
ACTIVE_RUNS_LOCK edge; any absent identity returns stream_dead without
calling agent.steer(). No cache eviction, no agent closing, HTTP write
still outside the locks.

Regressions: missing owner, missing/empty run session, both missing,
missing run entry -> stream_dead + steer never called; both present and
equal -> accepted, cache untouched. Legacy happy-path fixtures now
register the ownership they rely on.
2026-09-20 17:07:57 +00:00
Anthony Ruiz 84d66fe45f fix(steer): make cache-only delivery atomic with cancellation
The cache-only compatibility path selected the matching cached agent,
checked stream liveness under STREAMS_LOCK, released the lock, and only
then called agent.steer(). A Stop running in that gap published
phase='cancelling' and detached STREAMS/AGENT_INSTANCES while the
earlier Steer still enqueued into the cached worker, so the HTTP
response reported accepted guidance that cancellation already owned.

Revalidate stream membership, owner, and active-run
session/backend/phase and call agent.steer() under the same
STREAMS_LOCK -> ACTIVE_RUNS_LOCK edge Stop uses; interrupt,
persistence, and HTTP writes stay outside the lock. Cache eviction and
agent closing remain absent.

Extend tests/test_steer_cancel_ordering.py with a cache-only reverse-
order test (AGENT_INSTANCES absent, matching cached worker, decoy cache
entry) gating both unlocked edges around the stream-lock reacquire.
Stop-first must return stream_dead without calling agent.steer(); the
selected cache object is asserted to be the intended worker. Proven
red on the pre-fix code (order ['cancel','steer']) and green after.
2026-09-20 17:07:57 +00:00
Anthony Ruiz 1655113a81 fix: serialize cancellation ownership with active-run steer 2026-09-20 17:07:57 +00:00
Anthony Ruiz 91bd3e03b9 fix(steer): honor Gateway ownership before local cache fallback 2026-09-20 17:07:57 +00:00
Anthony Ruiz b89a3b344c test(steer): isolate the retained cache with monkeypatch 2026-09-20 17:07:57 +00:00
Anthony Ruiz 8869f34da0 test(steer): preserve mismatched entries in cache ownership regression 2026-09-20 17:07:57 +00:00
Anthony Ruiz c6152e4182 fix(steer): target the owned active worker after compression 2026-09-20 17:07:57 +00:00
Anthony Ruiz 8fe3325e12 fix(ui): keep current summary visible next to stale compaction markers
The settled current-summary card was only created when no compaction
marker was loaded at all. A transcript with older (stale) markers plus a
newer `compression_anchor_summary` that no loaded marker references
therefore lost the current summary, which master still rendered.

Decide the fallback from `_latestCompressionReferenceMessage()`
(`referenceMessageRawIdx < 0`) instead of marker presence, and feed that
decision into `_selectCompactionCardPlacements()` before any card node is
built so the fallback is the single preserved-task owner and marker cards
never attach the task list a second time.

Add a render-level regression that executes the real `renderMessages()`
over 220 messages, three stale markers, one preserved task-list message
and a newer summary: the current summary is visible and exactly one task
owner is rendered, in full, virtualized and server-truncated windows.
2026-09-20 03:48:19 +00:00
Anthony Ruiz 44b2a57109 fix(ui): preserve all pre-window compaction cards 2026-09-20 03:48:19 +00:00
Anthony Ruiz fab4968b17 test: remove private compaction fixture path 2026-09-20 03:48:18 +00:00
Anthony Ruiz 3eb06c192c fix(ui): preserve compaction cards across virtual windows 2026-09-20 03:48:18 +00:00
Anthony Ruiz c13819b6b0 test: stub _loadedCompactionMarkerRawIdxs in anchor-ownership node harness
The renderMessages node harness added in origin/master (#7125 lineage)
evals renderMessages in isolation; the new loaded-compaction gate calls
_loadedCompactionMarkerRawIdxs, which must be stubbed like the other
render helpers (_postProcessWithAnchorSuppression, _usedModelTurnChipLabel).
2026-09-20 03:48:18 +00:00
Anthony Ruiz ac83c94637 fix(ui): keep context compactions visible in long transcripts 2026-09-20 03:48:18 +00:00
Anthony Ruiz fc7ae2ddd1 fix(redaction): fail closed on large strings, byte-bound the prefilter cache
Gate found two blocking defects in the new caches:

1. The large-string LRU was keyed on input text only. The installed
   agent.redact registry can extend the secret matcher at runtime via
   register_redaction_patterns() and exposes no policy generation or
   invalidation hook, so a >16k string primed before a registration was
   served with the new secret intact (reproduced against the installed
   registry). Drop the large-string cache: strings above 16,384 chars run
   the redactor uncached again. Regression primes a large string, registers
   a runtime pattern on the real registry and proves the next
   redact_session_data() response is redacted.

2. The prefilter LRU was count-bounded (8192) but its per-entry gate was a
   character cap, which is not a byte bound: 16k four-byte code points is
   ~64 KiB per key, ~512 MiB at capacity, and the workload died with
   MemoryError under 96 MiB of headroom. Gate each entry on
   sys.getsizeof(text) <= 2048 so the retained-key ceiling is a hard
   16 MiB. Regressions cover max-width Unicode, sensitive verdict pairs,
   eviction past the cap, and a low-memory RLIMIT_AS subprocess.
2026-09-17 23:35:54 +00:00
Anthony Ruiz 43394141a8 test(redaction): translate French docstrings and messages to English 2026-09-17 23:21:47 +00:00
Anthony Ruiz d825d2061a test(redaction): neutralize private fixtures 2026-09-17 23:21:46 +00:00
Anthony Ruiz 86fe1a5746 perf(webui): make redaction containers copy-on-write 2026-09-17 23:21:46 +00:00
Anthony Ruiz b28559f62c test(streaming): exercise an echo that really falls outside the search window
The case labelled as an echo further back than the window still ended the
buffer with the suffix, so it matched regardless of the window. Replace it
with a buffer whose whitespace-padded echo spans more than the default
window, and add a dedicated test proving the same echo is rejected with the
default window and found once the window covers it, for both the optimised
scan and the reference oracle.
2026-09-03 09:41:27 +00:00
Anthony Ruiz c6a2c1b7d2 perf(streaming): fold the echo search window once instead of per cut index
`_strip_compact_echo_suffix` probed every candidate cut index and re-folded
the whole remaining tail for each probe, so the cost grew with
`window x suffix` rather than with the data actually inspected. It runs up to
four times per interim assistant message, on the reasoning buffer, the shared
stream mirror and up to two per-message segments.

The practical effect is worst on the longest message of a turn: 297ms for a
200-character message but 5.6s for a 6000-character one, measured on the live
process. That time is spent holding the GIL, so a single long final message
also stalls every other stream served by the same worker. Profiling a live
turn attributed ~5% of total CPU to this one helper.

Fold the window once, then walk backwards across exactly as many non-space
characters as the folded suffix holds. `str.isspace` and the `\s` pattern used
by `_compact_for_echo_compare` agree on all 1114112 Unicode code points, so
the folded view and the walk stay consistent.

Behaviour is unchanged, including the leftmost-cut tie-break on repeated text
and the bounded search window. The new test pins equivalence against a naive
reference oracle over ~2020 cases (exact, partial and absent echoes, mixed
whitespace, unicode, empty and None inputs, repetitions, out-of-window
echoes), asserts the fold no longer runs once per cut index, and caps the
runtime of a long-conclusion call.

Measured: 200 chars 297ms -> 0.149ms, 6000 chars 5645ms -> 0.720ms.
2026-08-27 23:58:18 +00:00
Anthony Ruiz 69e4fa3b98 fix(stream): adopt parser-owned fade row instead of recloning it
After rewind/cache-miss the keyed visible row stayed distinct from the
persistent parser candidate, so every later growth frame deep-cloned the
parser DOM into the old row. That cut off the previous tail word's
~620ms fade and broke word-node identity.

Promote the parser-owned candidate into the existing row's DOM position
once, mute the already-rendered prefix on the real parser body, and
return the adopted node so later keyed renders hit existing === node.

Regression: two post-transition growth frames keep the first new tail
span as the same .is-new node until an explicit animationend.
2026-08-27 23:58:14 +00:00
Anthony Ruiz 9388c6495b fix(stream): keep parser DOM as fade-row authority after rebuild
Cursorless markdown rebuilds cloned the live parser tree, then later
source growth appended raw bytes through _appendTransparentFadeText.
Incomplete emphasis (Hello ** / Hello **world**) became literal
**world** while the candidate already had <strong>world</strong>.

Parser-owned candidates now keep reconciling from the parsed DOM.
Pending and MEDIA tails stay on the parser. Rendered-prefix mute is
unchanged.
2026-08-27 23:58:14 +00:00
Anthony Ruiz 259631e46b fix(stream): resume cursor excludes parser pending tail on cursorless markdown rebuild
Greptile P1 follow-up on the fade-row rebuild: when a cursorless live
markdown row adopts the incremental node's cloned DOM, that DOM lags the
source text by the streaming parser's held-back tail (pending/text
buffers). Keeping the full source-length resume cursor made the next
reconciliation append nothing, so the pending characters stayed missing
from the live row until settlement.

Read the held-back length from the parser bound on the candidate body
(_smdBindParserIdentity) and trim the source-space cursor by exactly
that length, so the cursor matches the adopted content and the next
reconciliation re-appends the pending tail.

Regression: rebuild while the parser holds an unrendered tail, then
reconcile again — the pending text must appear in the live row before
settlement.
2026-08-27 23:58:14 +00:00
Anthony Ruiz 34f10d2545 fix(stream): mute rendered prefix and keep parsed markdown on fade-row rebuild
Review follow-ups for #7082:

- Maintainer should-fix: the no-cursor rebuild branch of
  _refreshTransparentFadeProseRow cleared the body and re-wrapped every
  word as .is-new, so the whole visible row dipped to opacity 0 and
  faded back (~620ms), and wholesale node replacement invited the
  one-time scroll-anchor bounce (#6257). Now the rendered text is
  snapshotted before the clear and the messages.js
  _streamFadeMuteRenderedPrefix idiom is re-applied after the rebuild,
  so only genuinely-new tail words animate. The helper is exported on
  window (__streamFadeMuteRenderedPrefix) the same way
  __anchorProseIncrementalNode is.

- Greptile P1: the same branch passed raw source text to
  _appendTransparentFadeText, flattening live markdown rows to literal
  syntax (links/emphasis/headings lost until settlement). The rebuild
  now adopts a deep clone of the candidate's parsed .msg-body (the
  incremental streaming-markdown output) when one exists, so the parsed
  DOM survives; the plain-text rebuild remains only as fallback for
  candidates without a parsed body. The resume cursor stays in source
  space when rendered text diverges from source (markdown), and shrinks
  to the rendered prefix for plain prose so the parser's held-back
  character is re-appended on the next delta instead of dropped.

Regression tests: rebuild must not re-animate the already-rendered
prefix (tail-only .is-new), and rebuild must keep parsed <a>/<strong>
DOM with no literal markdown in the visible text. The two existing
tests of this PR are unchanged and still pass.
2026-08-27 23:58:14 +00:00
Anthony Ruiz 59d2f295c8 fix(stream): stop tearing live progress words across block boundaries
Transparent-stream progress rows are built incrementally by
`_anchorProseIncrementalNode`, which streams source text into the vendored
streaming-markdown parser. That parser always holds the most recently written
character in its pending buffer, so a live row's rendered `.msg-body` text
trails its source text by one character.

`_refreshTransparentFadeProseRow` resumed appending from the
`data-stream-fade-text` cursor but fell back to `body.textContent` when the
attribute was absent — exactly the case for incrementally built rows, which
never set it. That fallback mixed two coordinate spaces (rendered text vs
source text), so the computed delta started one character early, in the middle
of a word. The delta was then appended as a sibling of the parser's open `<p>`,
and since `<p>` is a block box the tail rendered on its own line: a single word
split across two lines ("Ces deu" / "x fichiers").

Two independent fixes:

1. Resume strictly from the source-space cursor. When no cursor exists we
   cannot know how much source is already rendered, so rebuild the body
   instead of guessing a delta from rendered text.
2. Append into the trailing block element rather than the `.msg-body` root, so
   a continuation of the current sentence stays on the same line even if a
   cursor is ever stale.

Regression tests drive the real vendored parser to build a live row exactly as
production does, assert the parser really does lag its source, and fail if any
word is torn across a block boundary. The existing reconcile harness gains the
new helper dependency.
2026-08-27 23:58:14 +00:00
Anthony Ruiz 127a57e0c5 fix(sessions): fully consume PWA launch action 2026-07-29 21:50:54 +00:00
Anthony Ruiz cd04f5fcc2 fix(sessions): preserve active chat across hard refresh 2026-07-29 21:50:54 +00:00
Anthony Ruiz 87a9e25e41 fix(csv): keep media downloads visible in previews 2026-07-08 19:51:38 +00:00
Anthony Ruiz 5901ed68e1 fix(steer): scope upload UX to owner session 2026-07-05 16:02:54 +00:00
Anthony Ruiz b1a200f166 fix(composer): suppress stale draft restore after send 2026-07-04 08:06:33 +00:00
Anthony Ruiz 20260e2c29 fix(webui): keep handled clarify 404s out of warn logs 2026-07-01 17:17:11 +00:00
Anthony Ruiz 76ae2b795c test(webui): make live-transition guard anchors explicit 2026-07-01 15:50:24 +00:00
ruizanthony 0c13a17231 fix(webui): stop live streaming message flicker
Opt live assistant streaming nodes out of the global theme color/background transitions so token-by-token markdown updates do not flash or fade on light themes.

Adds a targeted regression test in test_smooth_text_fade.py while keeping the opt-in smooth text fade feature intact.
2026-07-01 07:38:44 +00:00