You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PIR #1365 (PR #1492, merged 2026-09-01) closed the biggest trigger — a Tower-side interrupt/escape landing mid-write — and ended false delivered from lock-taking writers. This issue closes the residuals: the receiving terminal itself can still eat bytes, and Tower cannot tell.
Root cause (proven from code, 2026-08-30 investigation)
[ok] Message delivered means "frames queued on a connected socket": writeMessagePaced resolves true when every session.write() returned true (message-write.ts), and ShellperClient.write returns true iff the socket object is connected (shellper-client.ts) — no echo/ACK of any kind before markDelivered (mailbox-delivery.ts).
The render gate's "clean prompt" has no stability requirement: a screen that repainted 1ms ago passes identically to one idle a minute. session.lastDataAt (pty-session.ts:823) is never consulted before the write. The quiescence drain trigger has an accidental 500ms settle window; the on-demand send path (handleSend → immediate delivery attempt) and the 'submit' fast trigger have none — matching tower: mailbox delivery race eats leading bytes when the write lands mid-composer-settle #1521's "on-demand relays hit it more."
Three changes, in leverage order. All in the post-#1492 converged write edge.
Settle-before-write (~one line + constant + test): in deliverAgentMail, immediately before the write, require now − session.lastDataAt ≥ SETTLE_BEFORE_WRITE_MS (~250ms, named constant) else hold('busy') — the backstop drainer retries on its existing schedule. This gives the request-time and submit-trigger paths the same accidental safety the quiescence path already has.
Loud size limit + delivered-length echo: reject message bodies over 48KB (match the existing --file bound; one shared constant) at handleSend with a clear error naming the limit and suggesting --file or splitting; mirror the check client-side in commands/send.ts. Add bodyLength to the send response and print it in the CLI result. Never silently truncate.
Echo-verification before markDelivered (the medium change — strictly scoped): after writeMessagePaced resolves and before markDelivered, wait a short bounded interval and check the session's rendered mirror (the same buffer the render gate classifies) for the message's header line only. Present → delivered. Absent → do NOT mark delivered; hold the row (existing hold/redeliver machinery; rows already escalate for visibility, so a persistent failure is loud, not a loop). Direction of error becomes duplicate-delivery, never silent loss.
Scope guard: header-line presence only. No screen diffing, no repair logic, no per-harness heuristics. If this cannot be done narrowly and reliably (e.g. the mirror scrolls the header away for very long bodies — note the 48KB cap bounds this), implement 1+2, cut 3, and say so in the PR.
(Optional, only if ≤ a couple of lines): sacrificial leading newline before the header so residual head-eating costs nothing. Drop at the first sign of harness weirdness.
Explicitly out of scope: bracketed-paste conversion of the body (structural fix for bare-\n Enter risk — needs per-harness verification; file separately), any change to gate classification, locks (#1492 territory), or registry/routing (#1368's lane).
Testing
Control test reproducing today's false-delivered: a fake session whose mirror never shows the header still gets markDelivered on current code; fails post-fix.
Settle: a write attempted <250ms after mirror data → held; after → proceeds.
Size: over-limit rejected loudly at route and CLI; at-limit passes; bodyLength echoed.
Problem
Two field bugs share one disease — silent message loss with a success receipt at the sender — on the channel that carries gate decisions:
[ok] Message deliveredboth times.[USER via VS Code]→ER via VS Code).PIR #1365 (PR #1492, merged 2026-09-01) closed the biggest trigger — a Tower-side interrupt/escape landing mid-write — and ended false
deliveredfrom lock-taking writers. This issue closes the residuals: the receiving terminal itself can still eat bytes, and Tower cannot tell.Root cause (proven from code, 2026-08-30 investigation)
[ok] Message deliveredmeans "frames queued on a connected socket":writeMessagePacedresolves true when everysession.write()returned true (message-write.ts), andShellperClient.writereturns true iff the socket object is connected (shellper-client.ts) — no echo/ACK of any kind beforemarkDelivered(mailbox-delivery.ts).session.lastDataAt(pty-session.ts:823) is never consulted before the write. The quiescence drain trigger has an accidental 500ms settle window; the on-demand send path (handleSend→ immediate delivery attempt) and the'submit'fast trigger have none — matching tower: mailbox delivery race eats leading bytes when the write lands mid-composer-settle #1521's "on-demand relays hit it more."(lines−1)×10ms + 80ms, no bracketed paste, per-line writes) with zero mid-write or post-write verification — matching afx send: long architect→builder messages truncated or delivered empty-body #1564's ~600-char threshold and tail-only/delimiter-only shapes.--filecap).Fix (prescribed — trimmed scope, owner-approved 2026-09-01)
Three changes, in leverage order. All in the post-#1492 converged write edge.
deliverAgentMail, immediately before the write, requirenow − session.lastDataAt ≥ SETTLE_BEFORE_WRITE_MS(~250ms, named constant) elsehold('busy')— the backstop drainer retries on its existing schedule. This gives the request-time and submit-trigger paths the same accidental safety the quiescence path already has.--filebound; one shared constant) athandleSendwith a clear error naming the limit and suggesting--fileor splitting; mirror the check client-side incommands/send.ts. AddbodyLengthto the send response and print it in the CLI result. Never silently truncate.markDelivered(the medium change — strictly scoped): afterwriteMessagePacedresolves and beforemarkDelivered, wait a short bounded interval and check the session's rendered mirror (the same buffer the render gate classifies) for the message's header line only. Present →delivered. Absent → do NOT mark delivered; hold the row (existing hold/redeliver machinery; rows already escalate for visibility, so a persistent failure is loud, not a loop). Direction of error becomes duplicate-delivery, never silent loss.Explicitly out of scope: bracketed-paste conversion of the body (structural fix for bare-
\nEnter risk — needs per-harness verification; file separately), any change to gate classification, locks (#1492 territory), or registry/routing (#1368's lane).Testing
delivered: a fake session whose mirror never shows the header still getsmarkDeliveredon current code; fails post-fix.bodyLengthechoed.Refs: #1564, #1521, #1365 / PR #1492, tracking #1483.