* 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>