fix(terminal): retry a two-stage send without repeating paste or Enter

Merged
TE
4d ago
+174-14

What changes

A single cmux terminal <id> send / MCP agents_terminal_send is two daemon mutations: bracketed-paste the text, then Enter. Until now each phase created a fresh random idempotency key. If the daemon applied a write but its acknowledgement was lost, retrying the whole send could paste the text twice or run the shell command twice.

This PR gives that composed operation one stable idempotency_key. The two child mutations receive distinct deterministic SHA-256-derived keys bound to that send ID, resolved terminal and phase. When a write's outcome is uncertain, CLI and MCP return the whole-send key plus details.phase; retrying with the key replays the daemon's previously accepted stage result. The complete send receipt also carries its ID. Callers may provide --idempotency-key in the CLI or idempotency_key in MCP; without one, the CLI generates a key and returns it on an uncertain write.

Rebuilt on feat-cmux-next (2026-11-01): #29207 was closed because base already routes the terminal verbs through status::run_args -> terminal_talk::run, so this PR's diff is only the send change, the CLI key forwarding, the MCP input and schema, and the tests.

Changelog

  • Fixed: Retrying an uncertain terminal-agent send can reuse the same paste and Enter request identities instead of duplicating terminal input.

Tests added

  • MCP descriptor/argument validation for an optional 1–128 byte stable key (test-first commit).
  • A fake daemon that applies the paste or Enter then loses the reply. Replaying with the same key must apply exactly one paste and one Enter.
  • Generated-key recovery after a lost paste acknowledgement.
  • The existing ordinary send tests and default behavior remain unchanged.

Scope and limitations

  • Uses the existing daemon's idempotency store; replay safety is bounded by that owner's key retention and consistency contract. This PR adds no new persistent send journal.
  • The same key is for the same terminal and prompt. A different prompt or a new run needs a new key; daemon payload-fingerprint checks remain authoritative.
  • This controls write duplication after a lost reply, not concurrent human typing or a terminal that has been closed/recreated. Those need an explicit revision/owner guard and a new integration test before claiming a complete atomic submit.
  • The existing special handling for permission dialogs stays unchanged. Do not use --force as implicit consent.

Observed / Not run

Observed: inspected feat-cmux-next@07088897284b6637a4ddb5763e06ca1601d4aae2 and the pushed diff against #29207's head b7a5371a819a0d7fa89191296ee2efa022dc41ff; original research and owner handoff in cmux #23026 and SW-10. The test-only MCP schema commit precedes the implementation.

Not run: Rust compiler, cargo test, real terminal/harness fault injection, hosted verification and independent review. This chat has GitHub write access but no Rust compiler or authorized Testbox. Draft until exact-head tests and review complete.

Suggested verification: ./scripts/verify-cmux-tui-hosted.sh --filter terminal_send_retry_replays_both_writes_after_either_lost_ack; --filter terminal_send_generated_key_is_returned_on_a_lost_reply; --filter terminal_send_exposes (or the MCP descriptor test name). Run a full terminal-talk suite and a live same-key retry against an isolated terminal before landing.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Fixes retrying a terminal send after a lost acknowledgement so it doesn't paste the text twice or run the shell command twice.

A cmux terminal <id> send (and the MCP agents_terminal_send tool) performs two daemon writes: bracketed paste, then Enter. Each write previously used a fresh random idempotency key, so retrying after an uncertain write could apply the same stage twice. Now both writes derive deterministic keys from one stable send identity, and retrying with that identity replays the daemon's previously accepted stage.

  • Callers can pass --idempotency-key/idempotency_key; without one, the CLI generates a key and returns it on an uncertain write.
  • Uncertain failures report the send key and the paste/enter phase, and the send receipt carries the key.
  • Adds tests using a fake daemon that drops acknowledgements, verifying a same-key retry applies exactly one paste and one Enter.