Skip to content

fix(server): re-deliver silently-dropped starting prompts via delivery receipt - #209

Merged
aterrylu merged 1 commit into
mainfrom
terry/prompt-delivery-receipt
Jun 11, 2026
Merged

fix(server): re-deliver silently-dropped starting prompts via delivery receipt#209
aterrylu merged 1 commit into
mainfrom
terry/prompt-delivery-receipt

Conversation

@aterrylu

Copy link
Copy Markdown
Owner

Problem

create_agent with a starting prompt sometimes produced an agent sitting at an empty input box forever while the creating agent waited indefinitely. Intermittent, and it broke every multi-agent workflow it touched.

Root cause: the prompt is delivered only as a CLI arg (claude ... -- <prompt>). The auto-trust watcher fired 5 blind staggered Enters, but CC's TUI attaches its stdin handler 100–500ms after first paint — early Enters were silently swallowed, the trust dialog never dismissed, and the argv-queued prompt never submitted. No fallback existed, and nothing covered prompt delivery in tests.

Solution

Two complementary layers — fix the known race, and make the system self-healing for unknown ones:

1. Needle-driven auto-trust retry (attachStartupWatcherCore, kills the root cause)
Each Enter is now verified against fresh PTY output: if the same dialog needle re-renders, or the PTY stays completely silent (write swallowed pre-attach), send again — capped at 5 attempts, 30s hard timeout. Preserved: autoTrust=false behavior, channels-dialog sequencing.

2. Delivery receipt + fallback re-delivery (agents/promptDelivery.ts, safety net)

flowchart LR
    A[spawn with prompt] --> B{SessionStart<br/>within 15s?}
    B -- no --> W1[warn + SystemWarning<br/>no paste into broken boot]
    B -- yes --> C{UserPromptSubmit<br/>within 20s?}
    C -- yes --> OK[delivered ✓]
    C -- "any turn activity<br/>(tool use, Stop, ...)" --> OK
    C -- no --> R[re-deliver ONCE:<br/>bracketed paste + Enter]
    R --> D{UserPromptSubmit?}
    D -- yes --> OK2[rescued ✓ logged]
    D -- no --> W2[error + SystemWarning:<br/>manual nudge needed]
Loading

Dedup guards: re-checked at fire time; any turn activity cancels (double-submission is worse than a manual nudge); writes refuse stale/replaced PTYs; a confirming event landing mid-paste cancels the Enter and clears the input draft (Ctrl-U). All failure paths surface as SystemWarning notifications in the dashboard panel (new warning badge), not just server logs.

Testing

  • Unit (prompt-delivery.test.ts, 14 cases): full receipt state machine — happy path, exactly-one-retry, dedup/activity guards, dead-PTY abort, compact-SessionStart exclusion, cancel-on-kill, multi-line paste.
  • Unit (startup-watcher.test.ts, 9 cases): scripted fake terminal reproducing the exact stdin-attach race, needle re-render retry, maxAttempts cap, dual-dialog sequencing, ANSI-wrapped needles.
  • Integration (CI-only, agent-spawn-prompt.test.ts): real claude + mock /v1/messages — spawn with prompt must execute with zero manual keystrokes (the old suite drove the terminal WS by hand, working around this exact bug), prompt text must reach the model backend, and the fallback must NOT have fired — so a watcher regression can't hide behind the re-delivery crutch. Server-boot harness extracted to helpers/embedded-server.ts (shared, no duplication).
  • QA on isolated dev server (real spawns vs mock backend, zero API cost):
    • Happy path: prompt executed hands-off, marker reached backend, no fallback.
    • Broken boot (autoTrust off, dialog blocking): 15s warning + SystemWarning fired, no paste.
    • Re-delivery: 20s fallback pasted into a real blocked PTY and rescued the agent end-to-end — trust dialog dismissed, UserPromptSubmit confirmed, turn completed.

Risks

  • The fallback writes to a live PTY; guards (stale-PTY refusal, activity dedup, one-retry cap, fire-time re-check) bound the blast radius to one paste per session lifetime.
  • The watcher's "silent PTY → retry" heuristic can send a harmless extra Enter at an empty input box; capped.
  • SystemWarning now appears in the bulk notification panel — intentional (these alerts exist for the operator), filtered events otherwise unchanged.

🤖 Generated with Claude Code

aterrylu added a commit that referenced this pull request Jun 11, 2026
…210)

Captures the two-layer fix for create_agent silently dropping the starting
prompt (shipped in #209): (1) needle-verified retry replacing blind Enter-burst
in the auto-trust watcher, and (2) hook-stream delivery-receipt tracker with
one-shot PTY fallback re-delivery on SessionStart-without-UserPromptSubmit.

Records the masking-guard (CI asserts fallback did NOT fire on happy path) and
the three rejected alternatives (gate on SessionStart, TUI needle detection,
PTY-only delivery).

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

@nox-0x nox-0x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving — solid two-layer fix for the silently-dropped prompt bug, with a needle-driven auto-trust retry that addresses the root cause and a delivery-receipt fallback that makes the system self-healing for unknown races. Tests are thorough (14 + 9 unit cases plus a CI integration test that explicitly asserts the fallback did NOT fire, so a watcher regression can't hide behind it).

A couple of minor observations, not blockers:

  • Layering inversion: agents/runtime.ts now imports pushSystemNotification from routes/hooks.ts. Routes-from-agents is the normal direction; this works because of how the notifications store happens to live in hooks.ts. If you ever want to clean this up, extracting the notifications store + pushSystemNotification to e.g. notifications.ts would restore the normal layering without behavior change.

  • Bracketed-paste escaping: \x1b[200~${prompt}\x1b[201~ doesn't escape an internal \x1b[201~ in the prompt itself. Vanishingly rare in practice (user prompts are natural language) but worth a comment if you want to future-proof against agent-generated prompts containing ANSI sequences.

  • Theoretical double-submission on a very slow original delivery: 20s is generous, but on a heavily loaded system both the original argv prompt and the re-delivered paste could land. The notification surfaces this, so operator-visible — acceptable trade-off as documented.

Nice work on the test design — the integration test's "the fallback must NOT have fired" assertion is exactly the right shape for keeping the safety net from masking primary-path regressions.

…y receipt

Agents spawned with a starting prompt sometimes sat at an empty input box
forever: the prompt travels only as a CLI arg, and the auto-trust Enter
burst raced CC's TUI stdin attach (100-500ms after first paint), leaving
the trust dialog up and the argv-queued prompt unsubmitted.

Two complementary fixes:

- Delivery receipt (agents/promptDelivery.ts): sessions spawned WITH a
  prompt are tracked through the hook stream (spawn -> SessionStart ->
  UserPromptSubmit). Missing UserPromptSubmit 20s after SessionStart
  triggers ONE re-delivery via PTY bracketed paste + Enter, with dedup
  guards (any turn activity cancels; re-check at fire time). No
  SessionStart within 15s warns without pasting. All failure paths
  surface as SystemWarning notifications in the dashboard panel.

- Needle-driven auto-trust (attachStartupWatcherCore): each Enter is
  verified against fresh PTY output — retry if the dialog re-renders or
  the PTY stays silent (write swallowed pre-attach), capped at 5
  attempts. Replaces the blind 5-burst that caused the race.

Tests: unit coverage for the receipt state machine and the watcher
(scripted fake terminal reproducing the stdin-attach race), plus a
CI-only integration test (agent-spawn-prompt.test.ts) asserting a real
spawn executes its prompt with ZERO manual keystrokes — and that the
fallback did NOT fire, so a watcher regression can't hide behind the
re-delivery crutch. Shared server-boot harness extracted to
helpers/embedded-server.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@aterrylu
aterrylu marked this pull request as ready for review June 11, 2026 17:28
@aterrylu
aterrylu force-pushed the terry/prompt-delivery-receipt branch from 74f2575 to 9b99fcb Compare June 11, 2026 17:28

@nox-0x nox-0x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving — solid surgical fix for the silently-dropped starting prompt with two complementary layers and well-bounded blast radius.

What I verified

  • State machine in promptDelivery.ts — phases (awaiting_session_startawaiting_prompt_submitawaiting_redelivery_confirm → done) are exhaustive; every termination path goes through finish() which clears both timers and removes the tracker.
  • Re-entrancy / replacementtrackPromptDelivery calls finish() first so re-tracking the same id is safe; live.get(persisted.id)?.pty !== pty in the write closure refuses paste into a replaced PTY.
  • No double-submission — the DELIVERY_CONFIRMING_EVENTS set plus the enterTimer !== null mid-paste guard (Ctrl-U to kill the input draft) prevent double-submit in every order I traced (event-then-Enter, Enter-then-event, fast-Stop, compact-SessionStart filtered, late SessionStart no-op).
  • Resource hygienecancelPromptTracking is wired in killAttachment, deleteAgent, pty.onExit (only on canonical PTY), shutdownAllAttachments, and restartAllAttachments. No leak paths I can spot.
  • Auto-trust watcher — needle-driven retry with capped attempts + hard timeout; silent-PTY heuristic correctly treats no-fresh-output as "write swallowed pre-attach" rather than "dialog dismissed". disposed flag plus per-dialog checkTimer cleanup keeps it tight.
  • Notification plumbingpushSystemNotification appends SystemWarning events, and the bulk endpoint + NotificationPanel filter now include them with a distinct warning badge. The per-session cap of 50 still applies.
  • Test coverage — 14 unit cases on the receipt FSM (including the activity-during-gap edge case, dead-PTY abort, compact-SessionStart exclusion) and 9 on the watcher race (silent pre-attach, needle re-render, ANSI-wrapped detection, dual-dialog sequencing). The CI integration test explicitly asserts NO SystemWarning fired — so a watcher regression cannot hide behind the 20s fallback.

Minor observations (not blockers)

  • The bracketed-paste end marker \x1b[201~ won't protect against a prompt that literally contains that substring (extreme edge case; not worth complicating for).
  • Stop is in DELIVERY_CONFIRMING_EVENTS — a hypothetical fast Stop without UserPromptSubmit would mark delivered, but in that scenario re-delivering into a dying session would be worse, so the current behavior is the right call.

Mergeable as-is.

@aterrylu
aterrylu merged commit 290fe5c into main Jun 11, 2026
11 checks passed
@aterrylu
aterrylu deleted the terry/prompt-delivery-receipt branch June 11, 2026 17:35
aterrylu added a commit that referenced this pull request Jun 26, 2026
The test is flaking consistently in CI with the documented auto-trust ↔
TUI-stdin-attachment race (ADR-036 / PR #209): "trust dismissed after N
attempts" → marker never reaches the model. The signature reproduces locally
against real `claude 2.1.193` + the mock backend.

Quarantining (skip: true) with an explanatory header comment + clear exit
criteria (fix the race upstream or route this test through the
prompt-delivery receipt path, then unquarantine). Other CI signal is
preserved; one PR is currently blocked by this flake (#258 cookie-scanner)
and the quarantine unblocks it.

Co-authored-by: Claude Opus 4.7 <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.

2 participants