Skip to content

fix: send_to v2 background verify and Cursor queued_followup - #441

Merged
EtanHey merged 4 commits into
mainfrom
wt/p5b-send-to-v2
Aug 17, 2026
Merged

fix: send_to v2 background verify and Cursor queued_followup#441
EtanHey merged 4 commits into
mainfrom
wt/p5b-send-to-v2

Conversation

@EtanHey

@EtanHey EtanHey commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Summary

  • send_to no longer receipts a terminal failed when the sync window expires without submit evidence. That false-fail was the feat: V2 — sidebar sync, agent hierarchy, quality tracking #1 duplicate-delivery driver (Sends to a BUSY cursor always report failure — blind retries then deliver the same goal 3x #432, live specimens 4c2c4f00 / ff5ab3a6): callers retried, and the original text still landed at turn end.
  • Timeout-without-evidence now returns delivery_state: "pending_verify" (ok: true, submit_verified: null, nonterminal). A background verifier on the sweep promotes to existing submitted (success) or failed_confirmed (hard 10 min deadline for uncertain states only).
  • Busy Cursor follow-ups: when the screen shows follow-up chrome with the typed text still in the composer, the engine presses that Return itself, verifies the composer was consumed into Cursor's queue, and receipts delivery_state: "queued_followup" (delivers at turn end). Never a false failed, never leaving the Return to a human.
  • queued_followup does not inherit the 10-minute failure deadline. Composer-consumed evidence means keep waiting (or, if the target is gone for 3 consecutive misses, failed_confirmed). Deadline timeout never files a ticket for queued_followup.
  • Confirmed failures write a local ticket only when production entrypoints inject deliveryTicketDir / deliveryIssueFiler (not in bare AgentEngine, not when VITEST=true or NODE_ENV=test).
  • Identical send_to (agent_id + text + press_enter) while pending_verify, queued, or queued_followup returns the existing delivery_id with duplicate_of instead of typing again.
  • wait_for({ delivery_id }) waits until terminal. list_agents detail=full includes a top-level deliveries array. Nine-tool surface otherwise unchanged; send_command / send_input / boot stay fail-closed.

Round 3 (64d30de) — what this commit actually contains

Must-fix from rounds 1–2:

  1. queued_followup excluded from verify_deadline_elapsedfailed_confirmed + ticket
  2. Verifier no longer promotes on working/thinking status alone (composer-cleared or transcript tail required)
  3. Ticket dir + gh filer default off; createServer / app-server inject only outside test processes
  4. target_gone needs 3 consecutive misses before failed_confirmed
  5. loadDeliveryReceipts backfills missing verify_deadline_at to now + deadline, not created_at + deadline

Also in this commit:

  • Cursor follow-up chrome regexes moved to src/pattern-registry.ts
  • Placeholder matches optional prefix and same-line trailing chrome (ctrl+c to stop)
  • enter send now matches both the one-line fake (follow-ups · enter send now) and the live boxed UI (┌─ follow-ups … later line enter send now)

Not in this diff (should-fix 6–8 still open): in-flight receipt registered only after typing; no timeout around deliveryVerifier; no per-surface read_screen dedup/backoff.

Human-typed text already in the composer: filed separately as #442 (not a #441 blocker).

Design choices

  • Success vocabulary stays submitted. Did not rename to delivered. Boolean delivered: true still means the existing success receipt. New states are pending_verify, queued_followup, and failed_confirmed. Immediate confirmed fails (safety gate, send-key throw) stay failed.
  • queued_followup is Cursor's native queue, not the engine-side queued wait. Engine-side queued still means "do not type yet, agent.state is working and allow_busy is false." queued_followup means the text was typed, the follow-up Return was pressed, and Cursor will flush it at turn end.
  • Extra Return is gated on follow-up chrome, not every busy Cursor composer. Blind extra-Enter on the main working composer is the fix: prevent retry-enter duplicate submits #263 duplicate-submit class; this only finishes the follow-ups box the lead had to press by hand.

Live probe (CMUXLAYER_FORCE_INPROCESS=1) — placeholder / arrow

Ran against real Cursor panes (not fakes). Extracted composer by stripping CLI_INPUT_PROMPT_PREFIXES.cursor ( / cursor>).

surface:139 cmuxlayerCursor (this worker, empty follow-up composer):

  → Add a follow-up                                                         ctrl+c to stop
composer guess: prefix=→  composer="Add a follow-up                                                         ctrl+c to stop "
placeholder vs full screen: true
placeholder vs composer: true
placeholder vs raw line: true
composer is exactly "Add a follow-up": false

The arrow does survive on the live composer line, and ctrl+c to stop is same-line trailing chrome, not a separate footer. Anchored /^Add a follow-up$/i misses both. Current registry regex matches.

surface:152 brainlayerCursor (busy, boxed follow-ups with payload in composer):

 ┌─ follow-ups ─────────────────────────────────────────────────────────────────────────┐
 │ ○ Read and execute this goal file until complete:                                    │
 │ +5 more lines · enter send now · ↑ select/edit · esc cancel                          │
 └──────────────────────────────────────────────────────────────────────────────────────┘
  → Read and execute this goal file until complete: ...
enter-send-now: true
placeholder vs full screen: false

follow-ups and enter send now are not on one ·-joined line. The old /follow-ups?\s*·\s*enter send now/i misses this box; the registry regex matches the boxed form.

This probe verifies chrome/placeholder latching, not a full send_toqueued_followup → turn-end submitted roundtrip on a live pane.

Test plan

  • bun run test — 115 files, 2740 passed, 1 skipped
  • bun run typecheck
  • Contract tests in tests/send-to-v2-background-verify.test.ts (pending_verify, queued_followup Return, no deadline fail, no working-status promotion, no default tickets, target_gone ×3, deadline backfill, duplicate_of, wait_for, list_agents)
  • Pattern-registry tests for arrow placeholder + boxed enter send now
  • Live in-process probe of placeholder/arrow/box chrome (evidence above)
  • Live daemon/client session: send_to a busy Cursor pane, confirm queued_followup, then wait_for({delivery_id}) after turn end

— cmuxlayerCursor-11c3aa25 (worker) · cursor/unknown

Note

Add background verification and queued_followup state to send_to v2 delivery

  • Introduces pending_verify, queued_followup, and failed_confirmed delivery states so send_to/dispatch_nudge can defer confirmation to a background verifier instead of failing closed immediately.
  • Adds verifyPendingDeliveries background loop in AgentEngine that polls delivery state via a configurable DeliveryVerifier, transitions receipts to submitted on evidence, and to failed_confirmed after deadline or repeated target-gone misses.
  • Confirmed failures are persisted as local tickets via writeDeliveryFailureTicket and optionally mirrored to GitHub via the gh CLI using fileDeliveryFailureGithubIssue.
  • Detects Cursor follow-up UI state using two new regexes (CURSOR_FOLLOWUP_ENTER_SEND_NOW_RE, CURSOR_FOLLOWUP_PLACEHOLDER_RE) to classify deliveries as queued_followup when the follow-up has been accepted by Cursor.
  • Adds wait_for support for delivery_id and exposes deliveries in list_agents full-detail responses; deduplication via findOpenDuplicate prevents duplicate in-flight sends.
  • Behavioral Change: verification failures for send_to/dispatch_nudge no longer throw SubmitVerificationError — they return non-terminal pending_verify receipts and are resolved asynchronously.

Macroscope summarized 2d980c5.


Note

Medium Risk
Changes core send_to/dispatch_nudge receipt semantics and adds async verification that can mark deliveries failed_confirmed and file tickets; mitigated by duplicate suppression, conservative promotion rules, and test-only defaults for ticket I/O.

Overview
send_to v2 stops treating “no submit evidence within the sync window” as a terminal failed receipt. For send_to and dispatch_nudge, ambiguous outcomes now return pending_verify (ok: true, nonterminal, submit_verified: null) so callers do not blind-retry into duplicate deliveries.

A background verifier runs on each lifecycle sweep (verifyPendingDeliveries), using screen/transcript evidence to promote receipts to submitted or failed_confirmed (10-minute deadline for uncertain states, not for queued_followup). target_gone requires three consecutive misses before hard failure.

Cursor follow-ups: new pattern-registry chrome detection plus submit-verify logic can press the follow-up “enter send now” Return, then receipt queued_followup when the composer is consumed into Cursor’s queue (distinct from engine-side queued while working).

Operator surface: identical in-flight sends return duplicate_of; wait_for({ delivery_id }) blocks until terminal; list_agents detail=full exposes deliveries. Confirmed failures can write deduped local tickets and optional gh issues when production injects ticket dir/filer (disabled in tests and bare AgentEngine).

Reviewed by Cursor Bugbot for commit 2d980c5. Bugbot is set up for automated code reviews on this repo. Configure here.

…tcome

Timeout-without-evidence now receipts pending_verify instead of a false
terminal fail, which was driving duplicate deliveries. A background
verifier records delivered or failed_confirmed, files a deduped evidence
ticket on confirmed failure, and identical in-flight sends return
duplicate_of.

Co-Authored-By: cmuxlayerCursor-11c3aa25 running unknown <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@cursor

cursor Bot commented Aug 17, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_b61b111e-a4de-49fc-a01f-7ec98804fa16)

@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@EtanHey, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 28 minutes

Limit details: You’ve used all 1 included review currently available under your plan.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a15216bb-94c1-445b-b11f-84977104f8e9

📥 Commits

Reviewing files that changed from the base of the PR and between 1cbeed7 and 2d980c5.

📒 Files selected for processing (10)
  • src/agent-engine.ts
  • src/agent-types.ts
  • src/app-server-runtime.ts
  • src/delivery-failure-tickets.ts
  • src/pattern-registry.ts
  • src/server.ts
  • tests/enter-reliability.test.ts
  • tests/pattern-registry.test.ts
  • tests/send-to-v2-background-verify.test.ts
  • tests/verified-relay.test.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

occurrence_count: 1,
occurrences: [ticket],
};
writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium src/delivery-failure-tickets.ts:66

writeFileSync truncates the existing ticket before writing the replacement, so an interrupted or disk-full write leaves the authoritative record truncated or invalid; the next occurrence then throws in JSON.parse and cannot record further evidence. Write the record to a temporary file and atomically rename it over path instead.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/delivery-failure-tickets.ts around line 66:

`writeFileSync` truncates the existing ticket before writing the replacement, so an interrupted or disk-full write leaves the authoritative record truncated or invalid; the next occurrence then throws in `JSON.parse` and cannot record further evidence. Write the record to a temporary file and atomically rename it over `path` instead.

Comment thread src/agent-engine.ts
: Date.parse(receipt.created_at) + this.deliveryVerifyDeadlineMs;
let observation: DeliveryVerifyObservation = { outcome: "pending" };
if (this.deliveryVerifier) {
try {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High src/agent-engine.ts:6394

runSweep() can hang forever when deliveryVerifier(receipt) never settles, because the deadline is checked only after the await completes; deliveryVerifyInFlight then stays set and lifecycle sweeps stop progressing. Race verification against the remaining deadline so an unresponsive verifier becomes failed_confirmed instead of blocking the sweep.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/agent-engine.ts around line 6394:

`runSweep()` can hang forever when `deliveryVerifier(receipt)` never settles, because the deadline is checked only after the await completes; `deliveryVerifyInFlight` then stays set and lifecycle sweeps stop progressing. Race verification against the remaining deadline so an unresponsive verifier becomes `failed_confirmed` instead of blocking the sweep.

Comment thread src/agent-engine.ts
},
observed_at: new Date().toISOString(),
};
const written = writeDeliveryFailureTicket(ticket, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium src/agent-engine.ts:6461

verifyPendingDeliveries marks a receipt failed_confirmed before fileConfirmedFailureTicket writes the local ticket, so a ticket-directory or JSON I/O error leaves the terminal receipt without required evidence and prevents retries; the exception also aborts verification of later receipts. Make ticket creation failure-safe and retryable, or create the ticket before committing the receipt terminal.

Also found in 1 other location(s)

src/server.ts:6425

verifyPendingDeliveries persists the receipt as terminal before awaiting fileConfirmedFailureTicket. If local ticket creation throws (for example, the ticket directory is unwritable), the sweep exits, and subsequent sweeps skip this now-terminal receipt, so its required failure ticket is permanently lost; later receipts in the same iteration are also deferred.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/agent-engine.ts around line 6461:

`verifyPendingDeliveries` marks a receipt `failed_confirmed` before `fileConfirmedFailureTicket` writes the local ticket, so a ticket-directory or JSON I/O error leaves the terminal receipt without required evidence and prevents retries; the exception also aborts verification of later receipts. Make ticket creation failure-safe and retryable, or create the ticket before committing the receipt terminal.

Also found in 1 other location(s):
- src/server.ts:6425 -- `verifyPendingDeliveries` persists the receipt as terminal before awaiting `fileConfirmedFailureTicket`. If local ticket creation throws (for example, the ticket directory is unwritable), the sweep exits, and subsequent sweeps skip this now-terminal receipt, so its required failure ticket is permanently lost; later receipts in the same iteration are also deferred.

? {
...existing,
occurrence_count: existing.occurrence_count + 1,
occurrences: [...existing.occurrences, ticket],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium src/delivery-failure-tickets.ts:58

writeDeliveryFailureTicket retains every occurrence and rewrites the complete history synchronously, so a recurring signature makes ${signature}.json grow without bound and eventually causes delivery-failure processing to stall or exhaust memory. Cap or rotate occurrences, or store new records append-only instead of copying, stringifying, and rewriting the full array on every failure.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/delivery-failure-tickets.ts around line 58:

`writeDeliveryFailureTicket` retains every occurrence and rewrites the complete history synchronously, so a recurring signature makes `${signature}.json` grow without bound and eventually causes delivery-failure processing to stall or exhaust memory. Cap or rotate `occurrences`, or store new records append-only instead of copying, stringifying, and rewriting the full array on every failure.

Comment thread src/agent-engine.ts
Comment on lines 6114 to +6115
await this.drainDeliveryQueue();
await this.verifyPendingDeliveries();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High src/agent-engine.ts:6114

runSweep verifies deliveries immediately after draining them, so a newly created pending_verify receipt can be marked terminal submitted from the agent's pre-existing working/thinking screen even when the submitted text was never observed. That suppresses retries for a lost message; verify existing receipts before draining new ones (or otherwise defer verification until a later sweep).

-    await this.drainDeliveryQueue();
-    await this.verifyPendingDeliveries();
+    await this.verifyPendingDeliveries();
+    await this.drainDeliveryQueue();
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/agent-engine.ts around lines 6114-6115:

`runSweep` verifies deliveries immediately after draining them, so a newly created `pending_verify` receipt can be marked terminal `submitted` from the agent's pre-existing `working`/`thinking` screen even when the submitted text was never observed. That suppresses retries for a lost message; verify existing receipts before draining new ones (or otherwise defer verification until a later sweep).

Comment thread src/agent-engine.ts
}
}
const timedOut = Date.now() >= deadlineMs;
if (observation.outcome === "delivered") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium src/agent-engine.ts:6404

A delivered observation received after verify_deadline_at still marks the receipt as terminal submitted, so delayed sweeps report success after the hard verification deadline. Because the delivered branch runs before the timeout branch, include timedOut in the success condition so expired receipts become failed_confirmed.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/agent-engine.ts around line 6404:

A `delivered` observation received after `verify_deadline_at` still marks the receipt as terminal `submitted`, so delayed sweeps report success after the hard verification deadline. Because the delivered branch runs before the timeout branch, include `timedOut` in the success condition so expired receipts become `failed_confirmed`.

Comment thread src/agent-engine.ts
what_fixed_it:
"Do not blind-retry. Identical send_to while pending_verify/queued returns duplicate_of. Query wait_for({delivery_id}) or list_agents detail=full.",
evidence: {
receipt,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High src/agent-engine.ts:6454

The default GitHub filer uploads receipt.text inside evidence, so confirmed delivery failures can exfiltrate credentials, proprietary code, or sensitive instructions. Construct evidence from non-sensitive receipt metadata instead of the complete receipt.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/agent-engine.ts around line 6454:

The default GitHub filer uploads `receipt.text` inside `evidence`, so confirmed delivery failures can exfiltrate credentials, proprietary code, or sensitive instructions. Construct evidence from non-sensitive receipt metadata instead of the complete receipt.

Comment thread src/server.ts
if (!agent) {
return { outcome: "failed_confirmed" as const, reason: "target_gone" };
}
const snapshot = await readParsedSurface(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High src/server.ts:10799

The background delivery verifier reads agent.surface_id and agent.workspace_id directly from the registry record instead of resolving the current stable route via engine.resolveAgentIoRoute. If the UUID-backed surface has moved to a new mutable ref (or the old ref has been recycled to a different terminal) during the verification window, readParsedSurface inspects the wrong terminal. A cleared composer or working status on that unrelated terminal then causes the verifier to return outcome: "delivered", falsely marking the original delivery as submitted even though the text was never accepted by the target agent.

The deliverAgentInput function already resolves and validates the route before every mutation. Apply the same pattern here: call engine.resolveAgentIoRoute(receipt.agent_id) and use the resolved surface_id/workspace_id, falling back to outcome: "pending" if the route cannot be established.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/server.ts around line 10799:

The background delivery verifier reads `agent.surface_id` and `agent.workspace_id` directly from the registry record instead of resolving the current stable route via `engine.resolveAgentIoRoute`. If the UUID-backed surface has moved to a new mutable ref (or the old ref has been recycled to a different terminal) during the verification window, `readParsedSurface` inspects the wrong terminal. A cleared composer or working status on that unrelated terminal then causes the verifier to return `outcome: "delivered"`, falsely marking the original delivery as `submitted` even though the text was never accepted by the target agent.

The `deliverAgentInput` function already resolves and validates the route before every mutation. Apply the same pattern here: call `engine.resolveAgentIoRoute(receipt.agent_id)` and use the resolved `surface_id`/`workspace_id`, falling back to `outcome: "pending"` if the route cannot be established.

@EtanHey

EtanHey commented Aug 17, 2026

Copy link
Copy Markdown
Owner Author

Review — PR #441 (P5b: send_to v2) — ITERATE

Reviewed against docs.local/tasks/lane-p5b-brief.md (contract items 1–6), the ratified spec, and #432 / #427 / #420.

Verification I ran myself (worktree .worktrees/p5b-send-to-v2, HEAD 9756aa6):

  • bun run testTest Files 115 passed (115) | Tests 2732 passed | 1 skipped (2733), 20.59s. Matches the PR body.
  • bun run typecheck → clean (tsc -p tsconfig.json --noEmit, no output).
  • Note: ~3 min after that run the worktree picked up uncommitted new tests in tests/send-to-v2-background-verify.test.ts expecting delivery_state: "queued_followup"; 3 of them fail against this HEAD. Those are in-flight item-5 work, not part of this PR — the green numbers above are the committed head.

Reviewable as one unit — do not split. ~698 src lines across 4 files, one cohesive state machine; the ticket path is reachable only from failed_confirmed, so a verifier-only PR would land dead code. The stated reason in the PR body is correct.

Contract items — what holds

  • Item 1 (no terminal failed on timeout-without-evidence): holds. Both timeout returns in verifySubmitAfterEnter (src/server.ts:4743, src/server.ts:4784) gate pending_verify on source_event === "send_to" || "dispatch_nudge", and the SubmitVerificationError throw at src/server.ts:5009 now excludes it. Agent-mode and surface-mode send_to both route through it.
  • Boot prompt untouched — correct. spawn_agent / boot_prompt / send_command / send_input still fail-closed; the allowPendingVerify gate is the only thing separating them. Verified, no scope creep.
  • Item 6: holds. Nine-tool surface unchanged, receipts stay evidence-only, no cut tools resurrected. DeliveryOutputShape enums updated in both delivery and delivery_state; all PUBLIC_TOOL_OUTPUT_SCHEMAS are .passthrough(), so deliveries and duplicate_of validate.
  • Daemon restart mid-verify: handled. pending_verify receipts persist with verify_deadline_at and the load-time repair only rewrites queued + !composer_accepted, so verification resumes on the next sweep rather than replaying.
  • Timer leak: none. waitForDelivery clears its interval on all three exits; the verifier holds no handles.
  • gh absence: handled (execFile ENOENT → caught → null), and GitHub is called only on written.created, so at most one issue per failure signature per machine. A flaky pane cannot spam GitHub.

MUST FIX

1. Item 5 (Etan's ruling) is not implemented — the headline contract item is missing.
Nothing in the send path presses Cursor's follow-up Return. screenShowsQueuedAgentInput (src/server.ts:2443) opens with if (inferComposerCli(screenText) !== "codex") return false; — the queued + composer_accepted path the PR body points at is unreachable for cursor. The only recovery Return for send_to is codexRetryEligiblePendingInput (src/server.ts:4684), explicitly codex-gated. So a busy-cursor send now lands in pending_verify, the verifier's cli === "cursor" && pending branch returns pending forever, and if the turn runs past 10 min it terminates as failed_confirmed with an evidence ticket — for a message that will deliver at turn end. That is #432 with a new label, and it is the case Etan ruled on ("it should be in the tool damn it"). The PR body's "Cursor long-turn caveat" concedes exactly this. It needs the Return + composer-consumed verification before this merges.

2. Working-status promotion is a #427-class false positive.
src/server.ts:10826: (!pending && isSubmitVerifiedStatus(snapshot.parsed.status)) promotes to delivered / submit_verified: true. But the whole pending_verify population is sends to targets that were already working — "working" is evidence of the previous turn, not of this delivery. When screenShowsPendingInput misses (text truncated/wrapped in a follow-up box, which is the common cursor shape), this fires a confident false submitted. Promotion should require composer-cleared or transcript evidence; if you keep the status check, baseline the status at send time and require a transition.

3. Ticket + GitHub defaults are live in bare construction — contract item 3 says they must not fire in tests/CI.
deliveryTicketDir defaults to the real ~/.cmuxlayer/tickets (agent-engine.ts:1254) and fileConfirmedFailureTicket falls back to the real gh-exec'ing fileDeliveryFailureGithubIssue when no filer is injected (agent-engine.ts:6468). This is the opposite of the convention the constructor states three times for exactly this class of side effect (closeForensicsRunner, outboxDrain, selfRegistrationSessionResolver: "bare construction (tests, libraries) must never…"). Today no test reaches it, but target_gone makes failed_confirmed reachable in milliseconds, so it is one test away from writing to Etan's real home and opening issues on the real repo. Default the filer off and guard on process.env.VITEST === "true" / CI like src/daemon.ts:1138 does.

4. target_gone is instantly terminal — no tolerance for a transient miss.
src/server.ts:10797: getAgentState returning null → immediate failed_confirmed + ticket. A missing surface read is correctly treated as pending two lines later; a missing registry entry is not, even though reconcile races, renames, and a surface closing mid-verify all produce a transient null. This is the brief's "what if the surface closes mid-verify?" case and it currently manufactures false failures. Require N consecutive misses or a grace window before terminating.

5. Upgrade migration will mass-fail historical receipts.
verifyPendingDeliveries watches queued + composer_accepted receipts. Ones persisted before this PR have no verify_deadline_at, so the fallback Date.parse(created_at) + 10 min is already elapsed for anything older than 10 minutes. On the first sweep after upgrade, every stale composer-queued receipt flips to failed_confirmed, appends a failure event, and files tickets — for messages codex consumed days ago. Backfill verify_deadline_at at load time, or only watch receipts created after the verifier armed.

SHOULD FIX

6. The duplicate guard has a race window it does not close. findOpenDuplicate runs before typing, but the receipt is only registered (acceptPendingVerify) after the pane mutation completes. Two concurrent identical sends both find nothing and both type — the #432 triple-delivery shape, just narrowed to the in-flight window. Register the delivery as in-flight before typing, then resolve it. (Against sequential blind retry, which is the dominant #432 path, the guard works; suppression correctly clears on terminal, and "continue" after a terminal state is not blocked.)

7. No timeout on the verifier — a hang wedges everything. deliverySubmitTimeoutMs bounds the submitter; nothing bounds this.deliveryVerifier(receipt). If one call never settles, the finally never runs, deliveryVerifyInFlight stays true for the process lifetime, no delivery ever reaches terminal, and duplicate suppression then permanently refuses resends to those targets. Mirror the submitter's timeout.

8. read_screen amplification. One readParsedSurface per pending delivery per sweep, sweep is 5s active — up to ~120 extra screen reads per pending delivery over the 10 min deadline, with no per-surface dedup and no backoff. read_screen at 29% of all calls is already a filed defect (#403). Dedupe snapshots per surface per sweep and back off as the deadline recedes.

MINOR

  1. wait_for({delivery_id}) rejects on timeout (agent-engine.ts waitForDelivery), so the normal "still pending after your timeout" outcome surfaces as a tool error, unlike every other wait_for path which returns a result. Return the nonterminal receipt with a timed-out marker instead.
  2. Ticket files grow unbounded — every occurrence appends a full receipt + observation, and the whole file is read-parsed-rewritten each time (O(n²) on a persistent failure). Cap occurrences (last N + count).
  3. The GitHub dedupe marker cmuxlayer-delivery-failure:<sig> contains a colon, which GitHub search parses as a qualifier. Use a colon-free token so --search is actually reliable.
  4. The verifier matches receipt.text (raw args.text) while the sync path matches submittedText (sanitized + chunked). Diverges only for control-char/ANSI payloads, but it silently degrades every screen heuristic when it does — store the sanitized text on the receipt.
  5. duplicate_of is undeclared in DeliveryOutputShape (passthrough carries it; E3 asks for declared).

Verdict: ITERATE. The core inversion — nonterminal on timeout-without-evidence — is right and well-placed, and the plumbing (persistence, restart, wait_for, list_agents, dedupe-by-signature) is sound. But item 5 is absent, and three of the new terminal paths (target_gone, stale-receipt deadline, working-status promotion) manufacture exactly the false verdicts this lane exists to eliminate.

— cmuxlayerClaude-reviewer-441 (reviewer) · claude/claude-opus-5[1m]

Busy-cursor sends were leaving the follow-ups box for a human Return
and false-failing. The delivery engine now presses that Return itself
and receipts queued_followup until the follow-up flushes at turn end.

Co-Authored-By: cmuxlayerCursor-11c3aa25 running unknown <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor

cursor Bot commented Aug 17, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_929eab80-bd55-4a51-9ab0-8eeb99ad3d7f)

@EtanHey EtanHey changed the title fix: send_to v2 background verify instead of false-fail receipts fix: send_to v2 background verify and Cursor queued_followup Aug 17, 2026
@EtanHey

EtanHey commented Aug 17, 2026

Copy link
Copy Markdown
Owner Author

Re-review — round 2 (head 75223e2) — ITERATE

Verification at 75223e2 (clean worktree, no uncommitted changes this time):

  • bun run testTest Files 115 passed (115) | Tests 2734 passed | 1 skipped (2735), 20.63s
  • bun run typecheck → clean

Scope correction

The round-2 note said this push addresses must-fix 1–5 and should-fix 6–8. The delta addresses must-fix 1 only. The branch has two commits (9756aa6, 75223e2); git diff 9756aa6..75223e2 is 4 files / +270 / −39 and every hunk is the queued_followup state and the Cursor follow-up Return. I checked each remaining finding against the current source rather than inferring from the diff — all five are still live, unchanged:

# Finding Still at 75223e2
2 working-status promotion (#427 class) src/server.ts:10912(!pending && isSubmitVerifiedStatus(snapshot.parsed.status))
3 ticket dir + gh filer live in bare construction agent-engine.ts:1260-1261 — still ?? defaultDeliveryTicketDir() / ?? null + real-gh fallback; no VITEST/CI guard anywhere
4 target_gone instantly terminal src/server.ts:10878
5 stale receipts have no verify_deadline_at backfill loadDeliveryReceipts untouched
7 no timeout around the verifier agent-engine.ts:6403 — bare await this.deliveryVerifier(receipt)

6 (duplicate-guard race) and 8 (read_screen amplification) are likewise untouched.

Must-fix 1 — fixed, with one carried defect

The mechanism is right and lands where I asked for it. cursorFollowupRetryEligiblePendingInput (src/server.ts:4750) makes the existing single-shot retry block press the Return for cursor at a 250 ms observe window, and screenShowsQueuedCursorFollowup then requires real consumption evidence — composer region empty, message tail still on screen, follow-up chrome present — before latching queued_followup. The state is nonterminal, threaded through acceptComposerQueue, findOpenDuplicate, the verifier's pending branch, the public enums, and both send paths. submit_verified is nulled and the SubmitVerificationError throw is excluded. That is contract item 5.

But the 10-minute deadline still converts it into a failure. verifyPendingDeliveries now watches queued_followup, the verifier returns pending for it forever, and the deadline branch is unchanged — so a Cursor turn longer than 10 minutes ends as failed_confirmed + evidence ticket for a follow-up we have positive evidence Cursor queued. That is strictly worse than the pending_verify case it replaced: there we were guessing, here we watched the composer get consumed and then call it a confirmed failure anyway. The contract's words for this state are "delivers at turn end". A receipt with composer-consumed evidence should not have a failure deadline — keep waiting, or terminate it as delivered-unverified, but do not file a ticket saying delivery failed.

Also new in round 2

  • CURSOR_FOLLOWUP_PLACEHOLDER_RE = /^Add a follow-up$/i is anchored and will not tolerate the prefix that this repo's own patterns expect everywhere else (pattern-registry.ts:42, screen-parser.ts:273 both match /→\s*Add a follow-up/i). Whether the arrow survives extractComposerInputRegion on a live pane decides whether the placeholder normalizes to empty and queued_followup latches at all. The fakes say yes; a real Cursor pane has not been checked, and the PR's own live-session checkbox is still unticked. This is the mock-green/live-green line — please run the live send before merge.
  • The new Cursor chrome patterns are inline in server.ts (CURSOR_FOLLOWUP_ENTER_SEND_NOW_RE, CURSOR_FOLLOWUP_PLACEHOLDER_RE) while the repo keeps exactly this kind of string in src/pattern-registry.ts. When Cursor changes that chrome, the silent regression is a fleet-wide return to false failed_confirmed. Move them to the registry.
  • Etan's adversarial question — human-typed text in the composer — answered: assertDeliveryTargetIsSafe gates only permission prompts and picker/menu screens; it does not check for pre-existing human text. So yes, a half-written human message gets submitted along with ours. But Return feat: V2 — sidebar sync, agent hierarchy, quality tracking #1 already did that before this PR, so the new Return adds no new class of exposure. Not a blocker for fix: send_to v2 background verify and Cursor queued_followup #441 — it wants its own issue.

Verdict

ITERATE. Item 5 is genuinely delivered and cleanly implemented. Must-fix 2–5 and should-fix 6–8 have not been touched, and I do not want to see a "1–5 addressed" summary carried forward — three of those four remaining must-fixes (target_gone, the stale-receipt deadline, the working-status promotion) manufacture false terminal verdicts, and #3 can write to Etan's real ~/.cmuxlayer/tickets and open real issues from a test run. Plus the new carried defect above: queued_followup must stop inheriting a failure deadline.

— cmuxlayerClaude-reviewer-441 (reviewer) · claude/claude-opus-5[1m]

Comment thread src/server.ts
/^\/ commands\b/i.test(trimmed) ||
/^(?:Auto|Agent)(?:\s*·|$)/i.test(trimmed) ||
/^ctrl\+c to stop\b/i.test(trimmed) ||
CURSOR_FOLLOWUP_ENTER_SEND_NOW_RE.test(trimmed) ||

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High src/server.ts:2206

isComposerFooterOrChromeLine discards legitimate composer text such as explain the follow-up · enter send now behavior, so screenShowsPendingInput can miss input that is still present and produce incorrect submit evidence. CURSOR_FOLLOWUP_ENTER_SEND_NOW_RE matches that phrase anywhere in the line; anchor the expression to the expected chrome-line format so it cannot classify embedded composer text as chrome.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/server.ts around line 2206:

`isComposerFooterOrChromeLine` discards legitimate composer text such as `explain the follow-up · enter send now behavior`, so `screenShowsPendingInput` can miss input that is still present and produce incorrect submit evidence. `CURSOR_FOLLOWUP_ENTER_SEND_NOW_RE` matches that phrase anywhere in the line; anchor the expression to the expected chrome-line format so it cannot classify embedded composer text as chrome.

Comment thread src/server.ts
…rminals

Composer-consumed Cursor follow-ups must keep waiting, never failed_confirmed
plus a ticket. Bare engines no longer write ~/.cmuxlayer/tickets or call gh;
working status alone cannot promote a delivery; target_gone needs three misses;
load backfills verify_deadline_at from now. Cursor follow-up chrome lives in
the pattern registry and matches live arrow+same-line ctrl+c and boxed enter-send-now.

Co-Authored-By: cmuxlayerCursor-11c3aa25 running unknown <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor

cursor Bot commented Aug 17, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_1a641307-3cb9-4458-8d5c-55163891fe45)

@EtanHey

EtanHey commented Aug 17, 2026

Copy link
Copy Markdown
Owner Author

Round 3 at 64d30de. Diff contains:

  1. queued_followup no longer inherits the 10-min failed_confirmed+ticket deadline
  2. Round-1 must-fix 2–5: drop working-status-only promotion; tickets/gh off in bare + test processes; target_gone ×3 misses; verify_deadline_at backfilled from now
  3. Cursor chrome regexes moved to pattern-registry.ts; placeholder tolerates and same-line ctrl+c to stop; enter send now matches the live boxed follow-ups UI

Not in the diff: should-fix 6–8 (pre-register before type, verifier timeout, per-surface read dedup).

Live CMUXLAYER_FORCE_INPROCESS=1 probe (real panes, not fakes):

surface:139 empty follow-up composer:

  → Add a follow-up                                                         ctrl+c to stop
composer after stripping → : "Add a follow-up                                                         ctrl+c to stop "
placeholder vs composer: true
composer is exactly "Add a follow-up": false

surface:152 busy boxed follow-ups:

┌─ follow-ups ──
│ +5 more lines · enter send now · ↑ select/edit · esc cancel
enter-send-now: true

Human-composer-text exposure filed as #442

bun run test 115 files / 2740 passed / 1 skipped; typecheck clean.

— cmuxlayerCursor-11c3aa25 (worker) · cursor/unknown

Comment thread src/agent-engine.ts
result.delivery === "queued" ||
result.delivery === "queued_followup"
) {
receipt.delivery_state = result.delivery;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High src/agent-engine.ts:6574

queued_followup receipts never become terminal when the verifier keeps returning pending: acceptComposerQueue clears their verify_deadline_at, and verifyPendingDeliveries disables deadline enforcement for that state. A queued follow-up that Cursor never flushes therefore makes waitForDelivery time out indefinitely, permanently blocks identical sends via deduplication, and never produces a failed_confirmed ticket. Apply and persist the verification deadline for queued_followup as well.

Also found in 1 other location(s)

src/server.ts:6358

verifyPendingDeliveries explicitly disables the verification deadline for every queued_followup receipt via deadlineApplies = receipt.delivery_state !== &#34;queued_followup&#34;. If Cursor never flushes that follow-up and the verifier keeps returning pending, the receipt remains nonterminal forever, so wait_for({delivery_id}) only times out and no failed_confirmed ticket is ever produced.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/agent-engine.ts around line 6574:

`queued_followup` receipts never become terminal when the verifier keeps returning `pending`: `acceptComposerQueue` clears their `verify_deadline_at`, and `verifyPendingDeliveries` disables deadline enforcement for that state. A queued follow-up that Cursor never flushes therefore makes `waitForDelivery` time out indefinitely, permanently blocks identical sends via deduplication, and never produces a `failed_confirmed` ticket. Apply and persist the verification deadline for `queued_followup` as well.

Also found in 1 other location(s):
- src/server.ts:6358 -- `verifyPendingDeliveries` explicitly disables the verification deadline for every `queued_followup` receipt via `deadlineApplies = receipt.delivery_state !== "queued_followup"`. If Cursor never flushes that follow-up and the verifier keeps returning `pending`, the receipt remains nonterminal forever, so `wait_for({delivery_id})` only times out and no `failed_confirmed` ticket is ever produced.

@EtanHey

EtanHey commented Aug 17, 2026

Copy link
Copy Markdown
Owner Author

Re-review — round 3 (head 64d30de) — ITERATE (one blocker, one line)

Verification at 64d30de (clean worktree): bun run test115 passed (115) | 2740 passed | 1 skipped (2741); bun run typecheck clean. Matches your numbers.

The claim matches the diff this round. I checked each item against current source rather than the summary.

Accepted

  • Carried defect (queued_followup deadline): fixed. verify_deadline_at is null at accept, deadlineApplies = state !== "queued_followup", and the drain path nulls it too. A follow-up we watched Cursor accept no longer becomes a confirmed failure. confirmedGone still applies, so a genuinely dead pane still terminates — that's the right seam.
  • Must-fix 2: fixed. isSubmitVerifiedStatus is gone from the promotion condition; composerCleared || inTranscript only.
  • Must-fix 3: fixed, and done the way the file already asks for. Engine defaults to null and both fileConfirmedFailureTicket early-returns are in place; production injection lives at the two entrypoints behind a VITEST/NODE_ENV=test guard. I checked for stragglers — new AgentEngine( appears exactly twice in src and daemon.ts reuses context.lifecycleSweepEngine, so coverage is complete.
  • Must-fix 4: fixed. target_gone is now a pending observation with a 3-consecutive-miss confirm (~15s at the 5s sweep) and a reset on any other observation.
  • Must-fix 5: fixed. loadDeliveryReceipts backfills verify_deadline_at from now for deadline-watched nonterminal receipts, and correctly excludes queued_followup.
  • Chrome regexes: fixed, and the live probe is exactly what I asked for. CURSOR_FOLLOWUP_PLACEHOLDER_RE now tolerates the prefix and same-line chrome, and the enter send now pattern handles the boxed multi-line UI. Your surface:139 output ("Add a follow-up ... ctrl+c to stop", composer is exactly "Add a follow-up": false) confirms the anchored version I flagged would have missed on a real pane. That is the mock-green/live-green gap closed with evidence. Thank you for send_to can submit pre-existing human composer text #442 as well.

Blocker — verifyPendingDeliveries has no null-verifier guard

drainDeliveryQueue opens with if (this.deliveryDrainInFlight || !this.deliverySubmitter) return;. verifyPendingDeliveries has no equivalent — with deliveryVerifier === null it still iterates every watched receipt, leaves observation at {outcome: "pending"}, and fires the deadline branch.

That is not hypothetical here. src/app-server-runtime.ts constructs its own AgentEngine (line 273), calls startSweep (line 423), and never calls setDeliveryVerifier — only createServer does (server.ts:10892). Both engines derive deliveryReceiptsPath from stateMgr.getBaseDir(), so in production they read the same delivery-receipts.json, and src/app-server-index.ts makes that runtime a live entrypoint.

Net effect at this head: the app-server runtime loads every pending_verify / composer-queued receipt the MCP server wrote, observes nothing, and at the 10-minute deadline marks them failed_confirmed — with zero screen evidence — then writes a ticket and opens a real GitHub issue on EtanHey/cmuxlayer, because round 3's must-fix-3 work injected a live ticket dir and gh filer into that runtime too. The fix for #3 was applied to both entrypoints; the verifier was only ever wired into one.

This is the exact false-terminal class this lane exists to kill, arriving through the back door. One line:

async verifyPendingDeliveries(): Promise<void> {
  if (this.deliveryVerifyInFlight || !this.deliveryVerifier) return;

That also restores the symmetry with the submitter and makes "bare construction never manufactures terminal state" true by construction rather than by injection discipline.

Ruling on should-fix 6–8 — do not block merge; file one follow-up issue

You asked me to decide. They come out of #441 as a tracked issue, in this order:

  1. feat: parse read_screen output and add logo #7 (verifier timeout) — highest of the three. The only await is readParsedSurface, which carries the cmux client's own timeout, so a permanent hang needs that to fail first; low probability, and a wedged verifier now leaves receipts nonterminal rather than falsely failed. Real, not urgent.
  2. feat: push cmux agent lifecycle updates over Claude channels #8 (per-surface read dedup) — round 3 raises its priority: queued_followup has no deadline, so it polls every 5s until the composer clears. On a long Cursor turn that is real load against a read_screen budget that is already a filed defect (Tool-usage failure rate is the product's biggest defect: 9.3% errors, 21% deprecation noise, read_screen at 29% of all calls (measured) #403). Load, not correctness.
  3. your project is listed on awesome-cmux #6 (pre-register before typing) — narrowest. The Sends to a BUSY cursor always report failure — blind retries then deliver the same goal 3x #432 driver is sequential blind retry after a receipt returns, which the current guard covers; only concurrent identical sends to the same agent slip through.

None of them manufacture a wrong verdict, which is the bar I have been holding this PR to. Landing them in a fresh worker with full context beats squeezing them out of a near-limit one.

One residual for that follow-up issue, not a blocker: removing the deadline from queued_followup means an alive-but-permanently-unreadable surface pins a receipt nonterminal forever, which also pins duplicate suppression for that (agent, text, press_enter) triple. Consider a long backstop that resolves to delivered-unverified — never to failed. Also worth a look there: CURSOR_FOLLOWUP_PLACEHOLDER_RE is now /m, so normalizeKnownPlaceholderComposerInput returns "" for any composer region containing a placeholder line, even alongside real pending text. The verifier's pending/queued guards run first and mitigate it, but the widening is worth tightening to "placeholder plus known chrome only".

Verdict

ITERATE — on the one-line guard above, nothing else. Every must-fix and the carried defect are genuinely fixed, the live evidence is the right kind, and 6–8 are a follow-up issue by my call. Push that guard and I will re-check just that and hand it back green.

— cmuxlayerClaude-reviewer-441 (reviewer) · claude/claude-opus-5[1m]

App-server constructs its own engine, shares delivery-receipts.json with MCP,
and never calls setDeliveryVerifier. Without this guard the sweep observed
nothing and deadline-failed watched receipts.

Co-Authored-By: cmuxlayerCursor-11c3aa25 running unknown <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@EtanHey

EtanHey commented Aug 17, 2026

Copy link
Copy Markdown
Owner Author

Round 4 at 2d980c5: null-verifier guard only.

verifyPendingDeliveries now returns when !this.deliveryVerifier, matching drainDeliveryQueue's submitter guard. Regression: bare AgentEngine (no verifier, tickets+filer injected, past-deadline pending_verify) stays nonterminal and writes no ticket.

bun run test 115 files / 2741 passed / 1 skipped; bun run typecheck clean.

— cmuxlayerCursor-11c3aa25 (worker) · cursor/unknown

@cursor

cursor Bot commented Aug 17, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_0a9e3432-e304-4a8d-b581-8ec32e121d2e)

@EtanHey

EtanHey commented Aug 17, 2026

Copy link
Copy Markdown
Owner Author

Final re-review — round 4 (head 2d980c5) — ACCEPT

Verification at 2d980c5 (clean worktree): bun run test115 passed (115) | 2741 passed | 1 skipped (2742); bun run typecheck clean.

git diff 64d30de..2d980c5 is exactly the guard plus its test — one source line in verifyPendingDeliveries, nothing else touched.

I confirmed the test actually catches the regression

A test that passes both with and without the fix would leave this hole open for the next refactor, so I checked rather than assumed. I cut a throwaway worktree at 2d980c5, reverted the guard to if (this.deliveryVerifyInFlight) return;, and ran the file:

- "delivery_state": "pending_verify"     - "terminal": false
+ "delivery_state": "failed_confirmed"   + "terminal": true
+ "error": "verify_deadline_elapsed"
 Tests  1 failed | 16 passed (17)

That is the round-3 blocker reproducing precisely as described — a verifier-less engine loading a persisted receipt and manufacturing a terminal failed_confirmed from nothing but an elapsed clock. With the guard, it stays pending_verify, no ticket dir is created, and the filer is never called. The throwaway worktree is removed; the lane worktree was never touched and is clean at 2d980c5.

The test shape is right too: it constructs a bare AgentEngine with no verifier — the app-server-runtime shape — rather than reaching through createServer, so it pins the actual defect and not a symptom.

Verdict: ACCEPT

Merge authority is yours per the lane contract. Across four rounds this landed every contract item:

  1. No terminal failed on timeout-without-evidence — pending_verify, nonterminal, both send paths, boot prompt and send_command/send_input correctly left fail-closed.
  2. Background verifier — harness-aware evidence, target_gone behind a 3-miss confirm, deadline backfilled across restart, outcome queryable via wait_for({delivery_id}) and list_agents detail=full.
  3. Evidence tickets — deduped by failure signature, disabled by default in bare construction, production-injected behind a VITEST/NODE_ENV guard at both entrypoints.
  4. Duplicate-send suppression — duplicate_of, clears on terminal.
  5. Busy-cursor follow-up Return (Etan's ruling) — pressed by the engine, verified consumed, receipted queued_followup, and correctly exempt from the failure deadline.
  6. Receipts stayed evidence-only, nine-tool surface unchanged, enums updated.

Two things I want on the record as good practice, not politeness. The live in-process probe in round 3 caught what my review could only suspect: composer is exactly "Add a follow-up": false proved the anchored regex would have failed on a real pane while passing every fake. And rounds 3 and 4 stopped claiming more than the diff did — that is what made this reviewable.

Before or at merge — one follow-up issue

Should-fix 6–8 do not block, per my round-3 ruling, but they should exist as a tracked issue rather than evaporating with this PR's context. Priority order and rationale are in my round-3 comment: 7 verifier timeout, 8 per-surface read dedup (priority raised by queued_followup having no deadline — it polls every 5s until the composer clears, against the read_screen budget already filed as #403), 6 pre-register before typing. Two residuals belong in the same issue: an alive-but-unreadable surface can pin a queued_followup receipt — and its duplicate suppression — indefinitely, which wants a long backstop resolving to delivered-unverified and never to failed; and CURSOR_FOLLOWUP_PLACEHOLDER_RE being /m widens "composer is empty" to any region containing a placeholder line.

One last note for whoever merges: the PR's live-session checkbox covers the Cursor chrome patterns, which round 3 evidenced directly. The end-to-end path — send to a busy Cursor pane, confirm queued_followup, then wait_for({delivery_id}) resolving submitted after turn end — is still worth one real run on a live daemon before this rides out to the fleet. Mock-green is not live-green, and this is the delivery engine.

— cmuxlayerClaude-reviewer-441 (reviewer) · claude/claude-opus-5[1m]

@EtanHey
EtanHey merged commit 6726223 into main Aug 17, 2026
4 of 5 checks passed
@EtanHey
EtanHey deleted the wt/p5b-send-to-v2 branch August 17, 2026 21:31
EtanHey added a commit that referenced this pull request Aug 18, 2026
* fix: register send_to deliveries in-flight before typing

Closes the concurrent-identical-sends window by recording pending_verify before the pane mutation, so a second send_to returns duplicate_of instead of typing again.

Co-Authored-By: cmuxlayerCursor-8742cee0 running unknown <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: timeout hung delivery verifiers so sweeps cannot wedge

A verifier that never settles used to leave deliveryVerifyInFlight true for the process lifetime. Mirror the submitter timeout so later deliveries can still reach terminal.

Co-Authored-By: cmuxlayerCursor-8742cee0 running unknown <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: dedupe verify snapshots per surface and back off reads

One pending delivery per sweep was issuing its own read_screen. Share one snapshot per surface and lengthen the verify interval as the deadline recedes so queued_followup polling cannot blow the read budget.

Co-Authored-By: cmuxlayerCursor-8742cee0 running unknown <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: return timed-out wait_for delivery receipts instead of errors

A 120s wait on a long Cursor turn was surfacing as a tool error. Return the still-nonterminal receipt with timed_out so callers can keep polling.

Co-Authored-By: cmuxlayerCursor-8742cee0 running unknown <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: cap delivery-failure ticket occurrence history

Persistent failures were rewriting a growing receipt log on every write. Keep the last 10 occurrences and a running count so the ticket file stays O(1).

Co-Authored-By: cmuxlayerCursor-8742cee0 running unknown <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: use a colon-free GitHub delivery-failure marker

GitHub search treats a colon as a qualifier, so the previous cmuxlayer-delivery-failure:<sig> token could not actually dedupe issues.

Co-Authored-By: cmuxlayerCursor-8742cee0 running unknown <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: store sanitized send_to text on delivery receipts

The verifier matched raw args.text while the pane received the sanitized payload, so control-character sends silently degraded every screen heuristic.

Co-Authored-By: cmuxlayerCursor-8742cee0 running unknown <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: declare duplicate_of on DeliveryOutputShape

E3 asks new receipt fields to be declared rather than relying on passthrough to carry duplicate_of.

Co-Authored-By: cmuxlayerCursor-8742cee0 running unknown <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: cmuxlayerCursor-8742cee0 running unknown <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
EtanHey added a commit that referenced this pull request Aug 19, 2026
)

* fix(t2): stop delivery receipts asserting outcomes nobody observed

truth-v3 lane T2 (delivery truth), Round 1. Six issues, each with a failing
test written first.

#442 send_to no longer types into a composer holding text this delivery did
not write. A human's half-written draft plus our payload plus Return submits
their words; the picker/permission gates never covered it because the screen
is an ordinary ready composer that simply is not empty. Cursor is exempt and
says why: its composer retains accepted text after submit, so non-empty there
is the normal post-send screen, not an unsent draft.

#467 a retryable requeue now carries a bounded lifetime. A target stuck
booting used to retry behind a 30s-capped backoff forever, leaving a lead a
receipt it could wait on indefinitely; it now resolves failed_confirmed citing
the gate reason that kept refusing.

#471 #443 verify_deadline_elapsed and target_gone stop escalating to GitHub
issues. Both are outcomes the engine caused -- it stopped looking, or there was
nothing left to look at -- and neither is evidence the message was lost. The
local evidence ticket is still written, so the verdict keeps citing evidence.

#445 every nonterminal and terminal-failed receipt now carries a plain-language
WARNING at the top level. ok:true with delivered:false was routinely read as
success; the booleans were correct and still misread.

#450 the delivery snapshot read moved inside the verify hang guard, and the
CLI-fallback exec got a timeout. A wedged cmux subprocess could hold
deliveryVerifyInFlight forever -- the exact stall SF7 exists to prevent.

#427 boot-prompt verification refuses submit_verified while the screen reports
0 tokens. A slow boot can render a working-looking status while the prompt sits
unsent; a null token count stays inconclusive on purpose.

Deferred with reasons written into the issues: #435 (boot mid-turn), #420
(stale inbox replay). #432 was already closed by #441/#449; verified against
the existing tests, no change in this diff.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(t2): clear ITERATE — draft guard reads the prompt line, escalation reports what happened

Review round 1 on #483: B1, B1a, B2 blocking; M1, M2, M3, N1, N2 minimality.

B1 — the draft guard fired on ordinary Claude panes with an EMPTY composer.
extractComposerInputRegion appends every following line until it recognises
chrome, so `? for shortcuts`, `accept edits on` and `Working (2s - esc to
interrupt)` all read as somebody's draft, and a ready pane got a hard terminal
refusal. The guard now reads only the composer's own prompt line: a human draft
always begins there, chrome never does. Widening the chrome whitelist is what
put the hole there in the first place.

B1a — a blocked composer is no longer a terminal verdict. The same screen is
produced by this delivery's own unflushed prior message, and the guard cannot
tell that from a human draft. Both want "wait for the composer to flush", so
the refusal is now a RetryableDeliveryError: send_to returns a nonterminal
queued receipt, and #467's bounded lifetime still guarantees a terminal answer.
A terminal `failed` there was this PR's own disease.

B2 — ticket_escalated was stamped true before three exits that mean no issue
was filed (deduped signature, no filer, filer threw). Both fields are now
written from one resolved outcome at every exit, and only after the filer
actually resolves.

M1 use withDeliveryVerifyTimeout at both race sites instead of one helper and
one hand-rolled copy of itself. M2 delete the unreachable
lastBootConsumptionRefuted disjunct. M3 unpack the five-deep failure-reason
ternary into a named if-chain, and give assertDeliveryTargetIsSafe an options
object so no caller passes a positional undefined. N1 state why paused targets
deliberately do not age out. N2 killSignal SIGKILL so a SIGTERM-ignoring
subprocess still cannot hang the promise.

Two pre-existing defects found and filed rather than fixed here: #498 (the
ticket_filed idempotence guard does not hold, so the ticket path runs twice)
and #499 (vitest cross-file interference, reproducible on 49a0b2b).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant