Skip to content

tower: delivery write edge — verify-or-retry 'delivered', settle-before-write, loud size limit (silent-loss residuals of #1564/#1521) #1573

Description

@waleedkadous

Problem

Two field bugs share one disease — silent message loss with a success receipt at the sender — on the channel that carries gate decisions:

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."
  • The write window is length-proportional ((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.
  • No size limit exists anywhere between the CLI flag and the PTY bytes (only the generic 1MiB HTTP body cap and the 48KB --file cap).

Fix (prescribed — trimmed scope, owner-approved 2026-09-01)

Three changes, in leverage order. All in the post-#1492 converged write edge.

  1. 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.
  2. 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.
  3. 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.
  4. (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.
  • Echo-verify: header present → delivered; absent → held and redelivered by backstop; the [PIR #1365] Serializer convergence: one lock at the terminal write edge #1492 suite stays green (same files).

Refs: #1564, #1521, #1365 / PR #1492, tracking #1483.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/towerArea: Tower server / agent farm CLI

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions