Skip to content

afx send: mailbox-first delivery — persist, gate on rendered-empty, never force-inject #1313

Description

@mohidmakhdoomi

Decision (supersedes the delivery-maneuver direction of #1265 / spike 1265)

No message is ever forced onto a busy line. A busy line means a human is present at that terminal — the right response is to tell the human through UI, not to fight them for the keyboard with clear/restore maneuvers. This drops the 60s max-age force-inject requirement, and with it ~70% of the apparatus the spike explored (H byte-replay, kill/yank, stash, per-app delivery forms, cursor-aware draft modeling, pre-Enter equality gate, canonical-stream verify).

Problem (from #1265)

afx send can land mid-draft and submit the user's half-typed input fused with the message. shouldDefer (tower-routes.ts:1570) is a 3s idle timer — a bad proxy for line occupancy. Additionally, today's SendBuffer is in-memory (held messages die with a Tower crash) and stop() force-flushes onto the line at shutdown; two concurrent sends blob into one submit (spike w1a); menus/trust dialogs/wrapper screens silently eat or strand deliveries.

Shape

1. Persistent mailbox at enqueue (spike option K, core)

Every send is persisted as a mailbox row before the send response returns. The in-memory SendBuffer queue collapses into it (fixes crash loss and the shutdown force-flush). Response vocabulary: delivered | held+id. Rows address agents, not PTYs, so a respawned terminal drains its predecessor's mail. Held-too-long escalates visibility (broadcast/dashboard/inbox), never injection.

2. Rendered-empty gate before every delivery (spike G-lite — validated)

Replay the existing output ring buffer through headless @xterm/headless and classify: composer marker present AND zero normal-intensity text in the composer region → safe. Measured 2–22ms per check. One mechanism answers "is a menu open?", "is the line occupied?", and "is there an agent behind this PTY at all?" — it correctly trips on slash menus, the codex model picker, agy's trust dialog, half-typed drafts (including afx attach-typed ones input tracking can't see), and the builder launch-loop's relaunch/boot screens. Sessions are born dirty; they converge only via this rendered proof.

Delivery moments: after a user submit, on output quiescence, and a poll backstop. Clean → deliver; not clean → keep holding. There is no force path.

3. Minimal per-session write serialization

Concurrent sends to one session currently interleave/blob (spike w1a, real message-write.ts). A small per-session FIFO with completion-based chaining fixes it. --interrupt stays an explicit human-invoked bypass.

Explicitly cut (archived as spike evidence, path not taken)

  • DraftTracker cursor/line modeling; per-keystroke occupancy state machines
  • All busy-line delivery maneuvers: H byte-replay, I/B kill/yank, ^S stash
  • Pre-Enter equality gate and canonical-stream differential verification
  • Per-app delivery form matrix (bracketed/unbracketed atomic forms) — only per-app marker profiles for the gate are needed, because delivery only ever targets an empty verified prompt

Why this eliminates corruption by construction

Message bodies are only ever written to an empty, rendered-verified prompt. A message cannot fuse with a draft because it is never delivered while one exists; a draft cannot be destroyed because nothing ever clears it.

Estimate

~400–700 LOC (vs. the spike's ~2,400–2,800 for the full-maneuver design). Salvage from spike 1265: the G-lite classifier approach + measured per-app marker/dim-placeholder facts, the keystroke/submit-semantics table (informs the submit-detection delivery moment), the born-dirty constraint, the wrapper-loss and attach-bypass findings (motivating the output-side gate), and the POC harness as a version-bump smoke test.

Relevant files

  • packages/codev/src/agent-farm/servers/tower-routes.ts (handleSend, shouldDefer)
  • packages/codev/src/agent-farm/servers/send-buffer.ts (collapses into mailbox)
  • packages/codev/src/agent-farm/servers/message-write.ts (write serialization)
  • packages/codev/src/terminal/pty-session.ts (ring buffer source for the gate)
  • Spike findings: codev/spikes/1265-afx-send-line-occupancy.md (branch spike-1265)

Refs #1265.

Metadata

Metadata

Labels

area/cross-cuttingTouches multiple areas — needs coordinated handling

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions