[Spec 1313] afx send: mailbox-first delivery (never force-inject) - #1330
[Spec 1313] afx send: mailbox-first delivery (never force-inject)#1330mohidmakhdoomi wants to merge 144 commits into
Conversation
Draft spec for afx send mailbox-first delivery (never force-inject), triaged against a 3-way consistency review: cron supersede made unconditionally per-task, --interrupt reframed as deliberate sender bypass reconciled with the no-force-path decision, canonical held-reason tokens, enqueue added to delivery moments, ok:true on held, per-PTY write serialization with per-agent enqueue ordering, two named broadcast events, and area/cross-cutting per label policy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address 3-way spec consultation (Gemini APPROVE, Codex REQUEST_CHANGES, Claude COMMENT — all HIGH confidence): - Add missing ## Expert Consultation section (unanimous ask) - Decision 8: make afx inbox workspace-scope + dismiss authorization explicit; note attention-state visual is plan-level - Decision 6: state supersede keys are cron-only - Test scenarios: add #16 escalation-age-threshold scenario No baked decisions changed. Also seeds the cohort thread file. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Documents resolution of all 3-way feedback (Codex REQUEST_CHANGES fully addressed; Gemini APPROVE; Claude COMMENT). No baked decisions changed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…complete) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9-phase decomposition: mailbox store → rendered-empty gate (claude/codex) → agy profile (blocking) → delivery orchestration + write serialization → fast delivery triggers → cron rerouting → afx inbox + SSE/escalation → dashboard/VSCode indicators → docs + skeleton mirror. Grounded in real architecture (UIs use SSE/overview, not WebSocket; VSCode badge precedent; migration v15). Offers phase-merge knobs for the gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address 3-way plan consult (Gemini APPROVE, Codex REQUEST_CHANGES, Claude APPROVE — all HIGH): - Phase 4: client-side send contract (tower-client.ts + commands/send.ts), automated e2e for #1265, pruneTerminal invocation, liveness telemetry - Phase 7: name lib/config.ts loader for escalation-age; liveness surfacing - Phase 1: name lib/config.ts for retention-days - Phase 5: drain-coalescing test - Exec summary WS->SSE; optional Phase 7 split offered No phase scope changed. Includes rebuttal + thread. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address iteration-2 plan consult (Gemini APPROVE, Codex REQUEST_CHANGES, Claude APPROVE — all HIGH; Gemini+Claude verified all iter-1 fixes landed): - Phase 4: dead-session targeting seam (agent-registry fallback in resolveTarget + handleSend restructure to persist no-live-pty instead of 404); --all honest reporting (sendToAll); fix tower-client shape desc - Phase 2: PtySession app-identity seam (command/args private today) for resolveProfile All Codex points verified against code. No scope change. Includes rebuttal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… layer (v15) Phase 1 of mailbox-first delivery: a durable home for every 'afx send' so nothing is lost to a Tower crash/restart/shutdown. Pure, delivery-decoupled data operations that Phase 4 wires against. - schema.ts: additive mailbox table + 3 indexes in GLOBAL_SCHEMA (agent-addressed via to_agent, not PTY-keyed). - index.ts: migration v15 (GLOBAL_CURRENT_VERSION 14->15) creating the table on existing installs; re-export new row types. - types.ts: DbMailbox row + MailboxStatus/MailboxReason unions. - db/mailbox.ts: repository (enqueue/getById/listHeld/findHeldForAgent/markDelivered/dismiss/supersede/pruneTerminal). State machine enforced (transitions touch only held rows); timestamps injectable for tests. - Tests: mailbox.test.ts (lifecycle, per-agent ordering, crash/reopen recovery) and spec-1313-migration.test.ts (v15; fresh vs migrated shapes converge). Refs #1313. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e/codex profiles Phase 2 of mailbox-first delivery: the sole authority that proves a composer is a clean, empty prompt before any message body is written to it. Corruption is eliminated by construction — a message can never fuse with a draft because it is never delivered while one exists. Unreferenced by any live path until Phase 4. - render-gate.ts: classifyScreen(snapshot, profile) replays a seed-capped ring snapshot (the production reconnect-replay data path) through a transient @xterm/headless terminal and inspects the composer region. CLEAN requires a composer marker AND zero normal-intensity, non-whitespace, non-chrome cells (dim placeholder/hint text is OK — an SGR attribute both TUIs use). Anything unrecognized → not clean → held. No force path. Direct port of the spike-1265 G-lite classifier. - gate-profiles.ts: claude + codex profiles (marker + region-end patterns from spike facts) and strict resolveProfile(identity) → profile | null via detectHarnessFromCommand. Deliberately no claude fallback: agy/gemini/opencode/unknown/wrapped launches resolve to null (held with no-profile), never a guessed profile. - pty-session.ts: app-identity seam — read-only command/launchArgs getters so resolveProfile has an authoritative source instead of guessing from the label. - package.json: promote @xterm/headless (^6.0.0) from spike-only to a dependency. - Fixtures + tests: render-gate.test.ts over REAL captured claude/codex byte streams (idle/draft/menu/picker) plus synthetic branch coverage, a seed-cap performance bound, and strict resolveProfile identity cases. Fixtures README documents provenance (claude-idle synthesized to the spike's real-claude dim-placeholder measurement, since the sandbox claude shim renders the idle placeholder without de-emphasis). Refs #1313. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Refs #1313. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cker, perf bound, CJS interop) Iteration 2 of Phase 2, addressing the iter-1 3-way review (Gemini + Claude APPROVE, Codex REQUEST_CHANGES): - Codex #1 (missing claude-picker fixture): add a synthesized claude `/model` picker fixture (claude-picker.busy.txt) whose highlighted row starts with the same ❯ glyph as the composer marker, with normal-intensity model names. Pins that a picker's selection-cursor + list classifies busy via the user-text path, never a false-clean; mirrors the real codex-picker capture (`› 1. …`). Wired into the required-states assertion; suite now 23/23. Documented as synthesized in the fixtures README (sandbox claude is the ez-cli shim, same reason as claude-idle). - Codex #2 (perf assertion too loose): replace the single cold-run < 500ms with warm-up + best-of-5 min < 75ms. The min strips JIT/GC/scheduling noise (42.7ms cold vs 14.5ms native steady-state here), so it validates the spec's ≤~50ms seed-cap budget (measured best-of-5 = 19.2ms) instead of flaking. 5x tighter than before; 75ms is the CI-noise ceiling, not a near-budget claim (the logged value is the evidence). - Bonus latent production bug, found while grounding the perf measurement against the compiled dist under native node: @xterm/headless resolves to its CommonJS entry (no exports map / type:module) with non-analyzable named exports, so `import { Terminal }` throws "Named export 'Terminal' not found" under native-node ESM — how the compiled bins run in production. Masked by vitest (vite interop) and dormant until Phase 4 wires the gate. Switch to the default-import form (codebase convention, cf. `import Database from 'better-sqlite3'`) plus a type-only alias for the one type-position use. Refs #1313. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Refs #1313. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…erf, CJS interop) Refs #1313. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…h + review rewrite
…essons routing Rewrite codev/reviews/1313-afx-send-mailbox-first-delivery.md from scratch off the SPIR review template (the prior file was stale, authored pre-rollback and only half-swept by the post-rollback implement commits). Reconstruct every section against the CURRENT implementation and git history: Summary, Spec Compliance (11/11 SC met), Deviations, Key Metrics, Timelog, Consultation Iteration Summary + full Consultation Feedback for every phase/round/model (cross-checked against the per-iteration evidence files), Lessons, Architecture/Lessons Updates, Technical Debt, Flaky Tests, Follow-ups. Governance routing for the post-rollback render-gate change: - arch.md sec 7 Message Delivery: "seed-capped output ring" -> whole-ring render at any size (over-ceiling hold removed) + ringToken verdict memo + backstop backoff. - lessons-learned.md (cold, Testing): validate a screen/output classifier against REAL captured output, not synthesized fixtures (the render-gate false-busy saga). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…us fields + round-2 record) Fresh 3-way Review consult (round 2, post-rewrite): Gemini APPROVE, Claude APPROVE, Codex COMMENT (non-blocking). Addressed Codex's hygiene comments: - spec Status draft -> specified; plan Status draft -> approved (both already carry approval frontmatter). - Recorded Review round 2 in the review doc's Consultation Feedback + Iteration Summary. PR #1330 body refreshed separately (stale 4162->~4267 test count, agy "deferred"-> live-verified, + the missing post-gate hardening arc). The spec-1280 T16 re-scope stays flagged for its owner; transient consultation artifacts remain deliberately untracked. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…passed) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Architect Integration Review — Changes Requested3-way integration CMAP on this PR: Gemini APPROVE · Claude COMMENT · Codex REQUEST_CHANGES (HIGH). I verified all three Codex claims against source (Claude and Codex contradicted each other on the TOCTOU point, so I read the code, not the summaries). Net: one blocking item + two cleanups. The gate stays parked — not approved. 🔴 Must fix — dropped PTY write reported as
|
Summary
Replaces
afx send's timer-based, in-memory, force-flushing delivery (SendBuffer, Spec 403) with a mailbox-first pipeline. Every message is persisted toglobal.dbbefore the send returns, and its body is only ever written to a prompt a headless-terminal render-gate proves is empty. Corruption is eliminated by construction — a message can never fuse with a draft, menu, dialog, or wrapper screen — and there is no force path: no timeout, valve, or fallback ever writes onto a non-clean screen. A busy line means a human is present; the response is to escalate visibility (indicator +afx inbox), never to fight for the keyboard.Closes #1313
Changes (9 phases, single PR)
mailboxtable (migration v15, agent-addressed rows); kills silent loss.render-gate.tsreplays the output ring through@xterm/headless; claude/codex profiles. Idle-gate cost well under the ≤ ~50ms budget.placeholderFgPaletterule (dim/bold couldn't separate idle from draft). Trust dialog classifies not-clean (no blind Enter confirms filesystem trust).handleSendrewrite: persist → serialize → gate → deliver/hold. RetiresSendBufferand every force path. Per-PTY write serialization (write-queue.ts). Response vocabdelivered | held+reason (busy/no-profile/no-live-pty), surfaced client-side (single +--all).handleUserInputchokepoint onPtySession; held mail delivers near-immediately once the line clears.afx inbox+ broadcasts + escalation — CLI (list/show/dismiss, workspace-scoped),overview-changedheld-count SSE,mailbox-escalationvisibility event (never delivery), liveness telemetry.afx inbox, mailbox config; CLAUDE.md/AGENTS.md byte-identical; skeleton mirrored.Post-gate hardening (folded into this PR)
After the first pr-gate approval, live testing on installed code surfaced real defects; the architect authorized a verify→implement rollback to fold the fixes into this PR (7 additional CMAP rounds):
afx send architectwas alwaysheld(no-profile)(architect sessions had no persistedcommand). Fixed with a restart-safe identity SSOT on the session row (migration v16) + a legacy self-heal that honorsTOWER_ARCHITECT_CMDprecedence.claude-idlefixture, so real-output false-busybugs (a bg-task panel displacing the composer boundary; a >1MB ring torn by a fixed tail-slice) shipped latent. It now renders the whole ring at any size (no tail slice, no delivery-blocking cap); an over-ceiling permanent-hold outage was removed; a per-ringTokenverdict memo + backstop backoff keep it cheap. Captured real gzipped rings as fixtures.Testing
busy) → submit → clean delivery (subprocess harness).packages/codev~4267 passing / 48 skipped; VSCode 667; dashboard 328.busy)→delivers on clear; bg-task/monitor→delivered (no false-busy); real >1MB rings classify clean; agy trust-dialog held. No held-message regressions.chdirrace under concurrent-builder load (not a code defect; green on direct run + retry) and the CI-aware render-gate perf bound.Notes for reviewers
POST-only method guard on/api/inbox/:id/dismiss+ regression test) landed and Claude approved, but there was no iter-4 re-consult. Flagged honestly in the review; this PR gate is the backstop.spec-1280T16 manifest guard was re-scoped by this PR (its unscopedorigin/main...HEADpredicate mis-fires on any branch touching a prompt surface after merging main); flagged for the 1280 owner's ack in the review's Technical Debt.Spec / Plan / Review
codev/specs/1313-afx-send-mailbox-first-delivery.mdcodev/plans/1313-afx-send-mailbox-first-delivery.mdcodev/reviews/1313-afx-send-mailbox-first-delivery.md🤖 Generated with Claude Code