Commit Graph
455 Commits
Author SHA1 Message Date
silent-reader-cn 946f7dbc2b fix(media): preserve scanner prose ownership 2026-07-13 07:41:13 +00:00
silent-reader-cn e13a75b7b1 test(media): cover fade MEDIA streaming splits 2026-07-13 07:41:13 +00:00
silent-reader-cn 3b19660235 fix(media): handle fade MEDIA prefix splits 2026-07-13 07:41:13 +00:00
silent-reader-cn a17370ad4a fix(media): harden streamed MEDIA rendering 2026-07-13 07:41:13 +00:00
silent-reader-cn 76940e821d fix: flush smd media tail on stream end 2026-07-13 07:41:13 +00:00
silent-reader-cn 547c218ddf fix: clear fallback smd media tail 2026-07-13 07:41:13 +00:00
silent-reader-cn ca3a1ac04d fix: accept extensionless streaming media urls 2026-07-13 07:41:13 +00:00
silent-reader-cn 13b7080bf0 fix: align smd media boundary extensions 2026-07-13 07:41:13 +00:00
silent-reader-cn 5359b70c6a fix: harden smd media split-tail streaming 2026-07-13 07:41:12 +00:00
silent-reader-cn d2a696d09a fix(webui): resolve Greptile #3 — WeakMap string key crash + unused _smdMediaTailClear
Greptile re-review (3/5) flagged two issues:

1. parserFor fallback ('__default__') string key → WeakMap.set throws
   TypeError (WeakMap accepts only object keys). Fixed by introducing a
   module-level sentinel object __SMD_PARSER_FALLBACK = {} and using it
   in both parserFor definitions instead of the string literal.

2. _smdMediaTailClear was defined but never called despite the comment
   claiming it was. Fixed by adding calls in:
   - _smdEndParser → clears the ongoing live-stream parser's tail
   - _clearAnchorProseIncrementalNode → clears each anchor-scene
     parser's tail before the cache is dropped

All 222 tests remain green.
2026-07-13 07:41:12 +00:00
silent-reader-cn 0b14ab54e3 fix(webui): split prose/MEDIA paths in streaming interceptor + per-parser tail buffer
Code-level fixes for the two issues Greptile + owner review surfaced:

1. SAFETY (Greptile #1 / owner security regression): the original
   _smdMediaAwareAddText concatenated prose slices + MEDIA HTML into
   one string and handed it to DOMParser. Prose containing HTML
   metacharacters (e.g. <img onerror=...>) would be parsed as live
   elements. Fixed by routing each prose run through baseAddText
   (which calls createTextNode) and only feeding each single MEDIA
   token's trusted _inlineMediaHtmlForRef output through DOMParser.

2. CROSS-CHUNK SPLITS (Greptile #2): smd may deliver a MEDIA: token
   across two flushes (e.g. "MEDIA:C:\...\" then "foo.png"). The
   original implementation would treat each half as separate text
   because neither half terminated cleanly. Fixed with a per-parser
   tail buffer (WeakMap keyed by parser instance) that holds trailing
   bytes ending in "MEDIA:" or MEDIA:<partial-ref> until the next call.

   The tail-buffer boundary detection was also improved: the previous
   `/[\s\)\]]/.test(rest)` incorrectly rejected "before MEDIA:" (the
   whitespace is before MEDIA, not after a completed ref). Replaced
   with `rest.endsWith('MEDIA:')` and `/MEDIA:...$/.exec(rest)`.

Architecture: _smdAppendMediaNode is a new single-token DOM insertion
helper that creates a DocumentFragment from one _inlineMediaHtmlForRef
output. _smdMediaTailSet/_smdMediaTailClear manage the per-parser
buffer. Both _safeSmdRenderer and _streamFadeRenderer now route through
the updated interceptor with tail map and parser identity.

Coverage: test_smd_media_in_stream.py tests the architectural invariants
(helper exists, both renderers delegate, recursion guard, entity-safety,
cross-chunk tail). 222 tests pass across all renderer/behaviour suites.
2026-07-13 07:41:12 +00:00
silent-reader-cn 6950540ed5 fix(webui): render MEDIA: tokens inline during smd streaming
The full renderMd() pipeline maps MEDIA:<path-or-url> tokens to inline
images / video / audio / SVG / PDF / etc. via stash + restore passes.
The streaming smd renderer used by the live assistant body never went
through that pass, so any MEDIA token emitted mid-turn showed up as the
raw path string in the chat window and only became a real <img> once
the turn settled and the full render fired.

Fix:
- Extract the URL/media → HTML mapping in renderMd() into a shared
  helper _inlineMediaHtmlForRef(ref, sessionId) so the streaming path
  can reuse the same DOM the settled message will eventually render.
- Add _smdMediaAwareAddText in messages.js that wraps smd's add_text
  to detect MEDIA tokens in any incoming text chunk and splice the
  resulting <img>/<video>/<a>/<div data-path=...> node directly into
  the smd parent via a DOMParser-created fragment. Plain prose still
  delegates to the renderer it intercepted (so the fade renderer keeps
  its word-by-word animation, the safe renderer keeps its O(n) plain
  text-node insertion).
- Route _safeSmdRenderer.add_text and _streamFadeRenderer.add_text
  through the interceptor. Each fast-paths the no-MEDIA case to the
  original baseAddText so we can't recurse into our own wrapper.

Coverage:
- New test_smd_media_in_stream.py locks in the architectural invariants
  (helper exists, both renderers delegate to it, recursion guard).
- test_renderer_js_behaviour.py driver now provides a minimal
  _inlineMediaHtmlForRef stand-in so renderMd can be invoked standalone
  under node without the rest of ui.js in scope (matches the patterns
  renderMd already relied on for _IMAGE_EXTS / _SVG_EXTS / _AUDIO_EXTS).
- test_svg_audio_video_rendering.py static-count assertions relax from
  ">=2" to ">=1" since audio/video/SVG markup now lives in a single
  shared function rather than being copy-pasted inside renderMd's
  stash-restore closure.
2026-07-13 07:41:12 +00:00
kertisalex 90ee9484e0 fix: also block fork on pending user message during streaming
Extends the live-message guard to reject both _live assistant rows and
_pending user rows that haven't been committed server-side yet. Adds a
belt-and-suspenders check: when S.busy is true, the last message in
S.messages is also blocked regardless of flags, closing a timing window
where _live might be momentarily absent on the streaming row.

The user's optimistically-pushed message (messages.js:1603) now carries
_pending:true so forkFromMessage can distinguish it from committed
messages.
2026-07-13 02:25:30 +00:00
nesquena-hermesandnesquena-hermes 7d94b1c890 Release exp-v0.52.52: keep transcript pinned across composer auto-resize (#5962, #5514) (#5983)
* fix(chat): keep transcript pinned across composer auto-resize (#5514)

Reported by @JeffArcLogic.

autoResize() sets the textarea to height:'auto' before measuring scrollHeight
and restoring the height. The transient collapse grows the flex:1 #messages
viewport and the browser clamps a bottom-anchored scrollTop down by the collapse
delta, stranding the reader a few px above the bottom on every keystroke while
the composer is multi-row (delta proportional to composer height) — and the
clamp's async scroll event sticky-unpins the reader, dead-ending the #5516
grow-path re-pin and stream auto-follow. The prior #5516 fix only re-pinned on
NET growth, so a steady-state keystroke was missed.

Fix: snapshot #messages.scrollTop before the height round-trip and restore it
after, undoing the transient clamp within the same synchronous task. Protects
pinned AND near-bottom scrolled-up readers, takes no _programmaticScroll latch,
inert to iOS toolbar reflows. The grow-gated re-pin + ResizeObserver remain for
genuine net growth.

* test(#5514): make near-bottom-unpinned case non-vacuous (Codex gate nit)

The companion unpinned case used scrollTop=7900, below the transient clamp max
(7903), so it never actually clamped and passed on master too. Move it to 7990
(inside the clamp zone: 7903 < 7990 < settled-bottom 8023) so it genuinely
exercises the transient-clamp yank the fix prevents.

* Release exp-v0.52.52: keep transcript pinned across composer auto-resize (#5962, #5514)

---------

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-11 22:37:23 -07:00
b2ae71e01a Release exp-v0.52.48: post-compression context metering (#5872, #4685) (#5977)
* fix(#4685): meter post-compression model context

* Release exp-v0.52.48: meter post-compression model context (#5872, #4685)

---------

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-11 20:41:43 -07:00
akay64 834ff49816 fix: reset context selection counter on clear and session switch 2026-07-11 11:34:26 +04:00
nesquena-hermes 5d2740cf56 fix(send): clearPending:false + preserve file objects (gate round 2)
Re-gate found 2 issues in the prior fix: (1) [CORE] uploadPendingFiles still
defaulted clearPending=true and re-cleared S.pendingFiles after the await,
wiping any attachment staged during the upload window — pass clearPending:false
since we already detached the snapshot. (2) [SILENT] converting File objects to
name strings broke draft-restore signature matching ({value:name} vs canonical
{name,path,size,type}) — preserve the file objects with [..._submittedFiles].
2026-07-11 04:13:53 +00:00
nesquena-hermes d6323bfff0 fix(send): snapshot files + clear draft pre-await (gate fixes)
#5912 gate found 2 defects the capture-clear salvage exposed:
- [CORE] uploadPendingFiles() read still-live S.pendingFiles, so a re-entrant/
  interrupt send during the upload window inherited + re-uploaded the first
  send's attachment. Now snapshot _submittedFiles before the await, detach from
  S.pendingFiles, and upload exactly that snapshot.
- [SILENT] the persisted-draft clear ran AFTER the await, cancelling a draft
  typed during the upload window. Moved it (and _composerDraftClearPromise)
  alongside the textarea clear, before any await. Failed-send restore still
  chains off the promise via the pre-await _failedSendFilesSnapshot.
Updated source-assertion tests for the new call shape + ordering.
2026-07-11 03:58:52 +00:00
nesquena-hermesandharryazj 67c9277ed4 fix(send): clear composer immediately after capture to prevent stalled-text race (salvage of #4750)
Salvaged from #4750 by @harryazj — pulling ONLY the atomic composer capture-and-clear fix from the larger PR. Prevents a re-entrant/interrupt-mode send from re-reading stale textarea DOM and double-sending.

Co-authored-by: harryazj <harryazj@users.noreply.github.com>
2026-07-11 01:39:19 +00:00
nesquena-hermes bdb245346d Merge #5739: worklog event timestamps (+ gate fixes) 2026-07-11 00:43:24 +00:00
nesquena-hermesandrodboev. Gate findings from adversarial Codex re-gate. a86324985e fix(worklog): persist event timestamps through settlement + reject invalid epochs
Two gate findings on the worklog-event-timestamp feature (#5700/#5739):

1. Thinking-row timestamps disappeared after settlement. Reasoning source events
   carry no server timestamp; the live DOM showed a fallback time but the settled
   scene row rebuilt with created_at:null. Now stamp created_at on the source event
   the first time it's seen (messages.js apply path) and carry row.created_at
   through snapshot hydration, so the timestamp survives live->settled.

2. A garbage huge numeric timestamp (e.g. 1e20) passed the finite/>0 checks and
   rendered literal 'Invalid Date'. _timestampSeconds now rejects epochs outside
   JavaScript's valid Date range. Added a regression test.

Co-authored-by: rodboev. Gate findings from adversarial Codex re-gate.
2026-07-11 00:23:48 +00:00
nesquena-hermes 0af0a5fcf0 Merge #5746: final answer only activity display mode 2026-07-10 23:50:28 +00:00
8cc2d6f881 Release: model-picker Selected badge + Transparent Stream dupe-prefix suppression (#5757, #5758) (#5850)
Two visible render PRs, gate-passed (deep-review round), Nathan-approved to ship.

- #5757 (rodboev): mark active model with a Selected badge + open its provider group (#5748).
  Screenshot-verified (badge clear + correctly placed on active row).
- #5758 (rodboev): suppress the live-token prose row that is a strict prefix of the final
  answer (#5749). Provenance-gated (live-prose:* token rows after last tool row only) so it
  can't eat legitimate narration; reproduction test proves the dupe is gone.

Codex SAFE + full regression green (1576 render tests). 18 own tests pass together.

Co-authored-by: t <a@b>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-09 12:51:18 -07:00
2b457f1646 Release: cap StreamChannel offline replay buffer (#5788) (#5849)
Bounded deque(maxlen) drop-oldest so a disconnect-without-cancel can't grow the
per-turn offline replay buffer unbounded (per-abandoned-turn OOM risk, #4633 family).
Dropped frames recovered via run-journal last_event_id replay path.

Deep-review gate: Codex SAFE (journal-recovery verified end-to-end), 20 own + 1106
regression tests green. Crown-jewel streaming path.

Co-authored-by: t <a@b>
Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
2026-07-09 12:26:05 -07:00
Rod Boev e592498664 fix(chat): keep live reasoning on one renderer path (#5720) 2026-07-09 03:12:37 +00:00
rodboev 8b02a8c260 fix(#5723): complete stale-busy cleanup on dead-run recovery (Fable UX-gate)
_steerClearCurrentOwnerDeadRun cleared busy/activeStreamId/inflight but left
the status line, elapsed timer, and optimistic streaming badge lingering after
a recovered dead run. Add the rest of the app's stale-busy cleanup idiom
(setStatus/setComposerStatus/_clearActivityElapsedTimer/clearOptimisticSessionStreaming),
still bypassing setBusy(false) so its queue-drain can't clobber the restored draft.
2026-07-07 16:32:26 +00:00
nesquena-hermesandrodboev 2717ea0ac5 feat(chat): add final answer only activity display mode (#5706, picker grid fixed)
Clean rebase of rodboev's #5746 (rebase-first).

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-07 14:38:08 +00:00
nesquena-hermes e6b2ac9119 review: merge #5506 (0c1bdce0) 2026-07-06 00:16:29 +00:00
Frank SongandPaperclip 08fc9bd95e Merge latest master into compression recovery action
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-07-05 15:44:44 +08:00
nesquena-hermes ceea2438eb fix: include attachments in steer submissions
Steering (submitting mid-stream) dropped any pending composer attachments. Now the
pending files are uploaded before the /steer payload is sent, the uploaded file
paths are appended to the steer text so the active agent can inspect them with
tools, file chips stay staged if the steer fails (so nothing is lost), and
file-only busy submissions are handled.

Contributor stage; gate-pass. Co-authored-by: ruizanthony <ruizanthony@users.noreply.github.com>
2026-07-05 06:50:34 +00:00
Frank SongandPaperclip 4deb1a9bfb Merge latest master into compression recovery action
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-07-05 06:29:08 +08:00
Frank SongandPaperclip 9280c073ba Merge latest master into compression recovery action
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-07-05 06:22:24 +08:00
nesquena-hermesandrodboev ae43091993 fix(#5550): keep Transparent Stream final replies visible after settle
The live-anchor scene renderer hardcoded mode:'compact_worklog' in
_renderAnchorLiveScene / _projectLiveAnchorActivityScene / the settle projection,
regardless of the user's actual active display mode. In Transparent Stream mode
the final reply was therefore projected with compact-worklog display hints, so on
turn-settle it folded into the collapsed worklog disclosure group (a refresh fixed
it because the stored transcript was correct). Now the renderer reads the real
active mode via _anchorSceneActiveMode() (window.chatActivityMode() ->
_chatActivityDisplayMode -> _transparentStream, default compact_worklog) and
applies per-mode display hints via _anchorSceneRowDisplayHintForMode(), so the
final answer stays a visible chronological reply in Transparent Stream.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-04 22:09:50 +00:00
nesquena-hermesandluperrypf cebfdf41f9 fix(#5541): iOS PWA streaming freeze + SSE reconnect reliability
Two mobile reliability fixes on the crown-jewel chat streaming path:
(1) content-visibility:auto on off-screen .msg-row under @media(pointer:coarse)
so WKWebView skips layout/paint for off-screen rows during streaming (kills the
long-chat freeze). The LIVE turn is kept content-visibility:visible via the
STABLE #liveAssistantTurn id (covers ALL render modes — Compact Worklog,
Transparent Stream, restored-live — not just the Transparent-Stream path that
stamps data-live-assistant-turn), so a normal live turn on touch never blanks
mid-stream and its height keeps growing so the new-message cue still fires.
contain-intrinsic-size:auto 1px preserves flick-scroll momentum. Scoped to
touch — desktop find-in-page untouched. Deliberately does NOT set
overflow-anchor:none (inert on iOS WebKit; re-opens #4856/#5338).
(2) SSE reconnect ladder extended 4->6 steps + a last-ditch
_restoreSettledSession full-session poll (8s watchdog) after retries exhaust, so
a response completed during an iOS Tailscale/VPN reconnect is recovered without
an error banner.

Co-authored-by: luperrypf <luperrypf@users.noreply.github.com>
2026-07-04 21:51:38 +00:00
Rod Boev 0c1bdce023 fix(#5493): honor reduced motion for transparent fade 2026-07-04 10:07:34 -04:00
Rod Boev 45a203b36b fix(#5493): preserve anchor prose before body mount 2026-07-04 10:04:15 -04:00
Rod Boev 3918dd132e Fade transparent prose without preference gate 2026-07-04 10:04:15 -04:00
Rod Boev 86e55c7856 Feed transparent fade rows revealed text 2026-07-04 10:03:35 -04:00
Rod Boev 26eda1adbd Make appended token fade perceptible 2026-07-04 10:02:39 -04:00
Rod Boev 28d058ad2e Start fade spans when appended 2026-07-04 10:02:39 -04:00
Rod Boev af2f534f21 Append fade spans instead of replaying streamed prose 2026-07-04 10:02:39 -04:00
Rod Boev c15cb84867 fix(#5493): restore transparent stream prose fade 2026-07-04 10:02:39 -04:00
Frank Song dfc0686dbd Live Stream: add compression recovery action 2026-07-04 21:42:32 +08:00
Agent Zero 68a692a8e0 fix(composer): scope draft restore suppression to payload 2026-07-04 08:06:33 +00:00
Anthony Ruiz b1a200f166 fix(composer): suppress stale draft restore after send 2026-07-04 08:06:33 +00:00
nesquena-hermes c77f4cf014 release #5516: composer-grow re-pin — hot-path optimization + CHANGELOG
Addresses Greptile P2 nit: autoResize() now only calls the re-pin when the
composer actually grew (offsetHeight before/after compare), skipping the DOM
read on a no-height-change keystroke. The #composerWrap ResizeObserver remains
the safety net for growth paths that bypass autoResize. Test tightened to assert
the grow-guard. CHANGELOG entry added (self-built, release owns changelog).
2026-07-04 06:06:05 +00:00
nesquena-hermes dde008748e fix(chat): re-pin transcript to bottom when the composer grows (#5514, #5515)
#5514 (deterministic): with the chat pinned to the bottom, growing the composer
past one row (multi-line typing, Shift+Enter, a multi-line / WisprFlow paste)
shrinks the flex:1 .messages viewport by the same delta. scrollTop is unchanged
but max-scrollTop rose by delta, so the reader is stranded delta-px above the
bottom — the transcript 'scrolls up one row per composer row.' autoResize()
resized the textarea and called updateSendBtn() but never re-pinned the transcript.
Reproduced live: composer 176px -> viewport shrank 745->613 -> transcript stranded
132px above bottom while _scrollPinned stayed true.

#5515 (intermittent 'random scroll up'): the same viewport-shrink from composer
growth via paths that don't route through input->autoResize (paste, draft restore,
programmatic value set, reflow) reads to the user as a random upward jump. Covered
by a ResizeObserver seam (below).

Fix: new _repinMessagesAfterComposerResize() (static/ui.js) re-pins to the bottom
ONLY when the reader is genuinely still pinned (honors _messageUserUnpinned /
_scrollPinned — never yanks a reader who scrolled away; verified live). Called from:
 (a) autoResize() after the height write, and
 (b) a ResizeObserver on the #msg textarea that re-pins on GROW only, catching
     every composer height-change path (not just the typed-input seam).

Verified live on a 40-message seeded session: pinned reader stays at bottomDist=0
after a 4-line composer grow; an unpinned (scrolled-up) reader is NOT moved.

Tests: tests/test_issue5514_composer_grow_scroll_pin.py (3 static wiring + 3 node-vm
behavioral). 524 scroll/composer regression tests across 39 files still green.

NOTE on #5515: this resolves the composer-growth cause (which the reporter's
WisprFlow-paste symptom points at). If a purely non-composer intermittent jump
persists, the remaining suspect is touch-device overflow-anchor:auto on content-
above height change — that path has no repro yet and is tracked as needinfo on
#5515.
2026-07-04 05:41:33 +00:00
nesquena-hermes 5c54fd598c fix(#5472): order draft re-persist after clear + honest happy-path test (#5488)
* fix(#5472): harden failed-send draft restore — order persist after clear, honest test

Two NIT-level follow-ups from the Opus review of #5484 (both on already-shipped
v0.51.842 code; neither is a functional break):

1. Draft clear/save ordering. send() fires an unawaited _clearComposerDraft POST
   (text:'') at send time, and the failed-send restore fired a separate
   _saveComposerDraftNow POST (text:<draft>). Under HTTP/2 multiplexing those two
   same-origin writes can be reordered, leaving the SERVER draft empty after a
   reload (the in-memory composer restore was always fine). send() now captures
   the clear POST's promise and passes it to the restore helper, which chains the
   re-persist after the clear resolves (falls back to immediate persist if the
   promise is absent/rejects).

2. Honest happy-path test. test_send_still_clears_composer_on_the_happy_path
   asserted a bare $('msg').value='' string that matches ~13 sites, so it would
   pass even if the MAIN-path clear were removed. It now anchors to the unique
   main-path _clearComposerDraft line and window-checks the composer wipe
   immediately precedes it.

Tests: +2 (promise-ordering behavioral via node-vm microtask ordering; absent-promise
fallback), signature/call assertions updated. 14 in the #5472 file, 93 across the
related composer/send/draft/upload surface — all green.

* fix(#5472): make deferred draft persist stale-aware (Codex #5488 catch)

The promise-ordering fix introduced a regression: the deferred _persist saved the
CAPTURED snapshot and its _saveComposerDraftNow clearTimeout cancelled any newer
debounced save. So if the user edited the restored composer before the clear POST
settled, the delayed persist overwrote their edit with the stale original text
(lost on reload).

Restructure the helper so the deferred persist is stale-aware at FIRE time:
- If the failed session is still visible, re-read the LIVE composer (.value
  + current S.pendingFiles) instead of the captured snapshot — a post-restore edit
  is captured, not clobbered.
- If we restored the visible session but the user has since switched away, SKIP the
  stale write entirely (the session-switch save path already saved that session).
- Background failure (sid never visible): persist the captured snapshot, since
  there's no live composer to read — it's the only copy.

The visible-composer restore + session guard are unchanged in behavior (moved above
the persist). Helper now returns whether it restored the VISIBLE composer.

Tests: +3 regression cases (post-restore edit captured; switch-away skip; background
snapshot persist), signature/guard assertions updated. 17 in the #5472 file, 87
across the related surface — all green.
2026-07-03 16:36:21 -07:00
nesquena-hermesandnesquena-hermes 603375d71c fix(#5472): preserve composer draft when a send fails (#5484)
* fix(#5472): preserve composer draft when a send fails

When a provider/background error aborts a send, send() has already cleared
the composer and the persisted draft before /api/chat/start durably accepts
the turn. On a start-time throw the turn is never persisted, so the user's
typed message is lost and must be retyped.

Add _restoreComposerDraftAfterFailedSend(text, sid): restores the typed text
into the composer, keeps staged files in S.pendingFiles, and re-persists the
draft (via _saveComposerDraftNow) so it also survives a reload. Guarded to
no-op when there's nothing to restore and to never clobber a new message the
user began typing during the async send window. Wired into the /api/chat/start
throw handler's general-error branch. Mirrors the existing _trySteer /
_stashClarifyDraft draft-restore idioms.

Tests: tests/test_issue5472_preserve_draft_on_failed_send.py — 3 static wiring
assertions + 4 node-vm behavioral tests of the helper's branching logic.

* fix(#5472): address gate findings — original-payload snapshot, re-stage files, session-guard

Codex + Opus gate on the first commit converged on three real edges:

1. /moa and bundle sends rewrite `text` before /api/chat/start, so restoring
   `text` replayed the transformed payload, not the user's typed command.
   Fix: capture an immutable _failedSendDraftText snapshot right after the
   post-flush trim, before any slash rewrite, and restore that.

2. uploadPendingFiles() clears S.pendingFiles=[] before the start call, so the
   original 'keep staged files' assumption was false — a resend silently
   dropped attachments. Fix: snapshot S.pendingFiles before the upload drain
   (_failedSendFilesSnapshot) and re-stage from it (+ renderTray()) on failure.

3. A start failure after a session switch could write the failed old session's
   draft into the currently-visible composer. Fix: the helper now only mutates
   the visible composer/tray when sid === the visible session; the sid-keyed
   _saveComposerDraftNow persist stays unconditional so the draft survives a
   switch-back/reload.

Helper signature is now (draftText, filesSnapshot, sid). Tests expanded to 11
(5 static + 6 node-vm behavioral) covering all three edges: original-vs-mutated
payload, re-stage drained attachments, and cross-session no-pollution.

* test(#5472): bound send() body slice to LIVE_STREAMS instead of fixed char window

The #5472 snapshot-capture lines added ~400 chars near the top of send(), which
pushed 'cancelStream' (in the busy-interrupt branch) past the arbitrary
[send_idx:send_idx+5000] window that test_1062's structural assertions used —
a real CI failure (shard 1, all 3 Pythons) caused by a brittle test, not by the
fix logic. Bound both send()-body slices in test_1062 to the actual function
extent (up to the 'const LIVE_STREAMS=' decl that immediately follows send()) so
future additions near the top of send() don't false-fail these assertions.

---------

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-07-03 15:57:33 -07:00
nesquena-hermes eaf7d1e9ab gate-fix #5466: stamp anchor attrs on incremental node + teardown on all terminal paths
Codex gate findings (2 SILENT, both fixed):
- ui.js: incremental prose node now flows through the shared row-decoration
  block (data-anchor-scene-row/-row-id/-row-role/-source-event-type) instead of
  returning early, so live incremental rows keep the identity attrs the scene
  reconciler matches on.
- messages.js: _cancelThrottledSnapshotTimer() + _clearAnchorProseIncrementalNode()
  now run on apperror, cancel, _handleStreamError, and _restoreSettledSession
  terminal paths (previously only fallback/done/stream_end) — closes the
  snapshot-timer/anchor-cache/window-global teardown gap.
- test: extend test_messages_js_stream_perf_cleanup_lifecycle to pin teardown on
  all four additional terminal paths.

CORE equivalence (Codex flagged smd != renderMd): reconciled — Opus's 1498-frame
harness against real smd.min.js proves incremental-smd == whole-smd byte-identical;
live-smd-vs-renderMd difference is the already-shipped fidelity model (main live
body streams smd, settles renderMd), and the incremental path is gated on !settled
so settled DOM still comes from renderMd. Not a regression.
2026-07-03 21:24:08 +00:00