fix: deliver long CLI passthrough prompts intact (#3894)

Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
This commit is contained in:
Dmitry Koryakov
2026-09-24 15:45:00 +02:00
committed by GitHub
parent d602745281
commit 5cb908e4ad
28 changed files with 1129 additions and 143 deletions
@@ -76,7 +76,7 @@ When a passthrough session starts for a task that has a description:
No per-agent pattern matchers. The existing idle window is the only readiness signal. If an agent's CLI is unusual enough that an idle window misfires (writes a banner, then waits 5 seconds, then prompts), we make the idle window per-agent-configurable in `PassthroughConfig` (already exists as `IdleTimeout`). No new detection machinery.
For the Claude case: `claude_acp.go` retains `AutoInjectPrompt: true` as compatibility metadata, sets `DisableBracketedPaste: true` (Claude Code already enables bracketed-paste *mode* in its Ink TUI — injecting `ESC[200~`…`ESC[201~` delimiters breaks the prompt), and `SubmitViaBackslashEnter: true` (PTY writes: prompt, then `\`, then `\r` per [Claude terminal docs](https://code.claude.com/docs/en/terminal-config)). Ink may still treat programmatic Enter as newline only ([anthropics/claude-code#15553](https://github.com/anthropics/claude-code/issues/15553)) — if auto-submit fails, the user confirms with Enter. Other passthrough-capable agents without a `PromptFlag` use the same idle-based stdin route.
For the Claude case: `claude_acp.go` retains `AutoInjectPrompt: true` as compatibility metadata and uses a 150 ms `SubmitDelay`. The prompt body uses bracketed-paste framing, then the submit carriage return is sent as a separate delayed write. This keeps the submit byte out of the Ink paste burst. Custom terminal agents can set `DisableBracketedPaste: true` when they do not accept bracketed-paste delimiters; Kandev then uses bounded, paced unframed writes. The pacing is an empirical fallback because PTY writes do not provide a receipt from the TUI. Other passthrough-capable agents without a `PromptFlag` use the same idle-based stdin route.
### Follow-up prompts via PTY stdin
@@ -0,0 +1,96 @@
---
status: draft
system: cli
created: 2026-09-22
owners:
- kandev
---
# Passthrough Prompt Delivery Requirements
## Overview
In CLI passthrough mode Kandev runs a vendor CLI inside a PTY and types prompts
into it, because that CLI exposes no structured prompt channel. Every other
delivery surface hands the agent a framed message; passthrough hands it
keystrokes, so the prompt is only as reliable as the terminal path that carries
it. A host terminal delivers input to the reader in fixed-size reads, and a TUI
that cannot absorb a burst that large drops whole reads without reporting an
error. The operator sees a complete message in Kandev, the agent answers a
truncated one, and nothing in either surface marks the difference.
The CLI system owns this contract because it owns how Kandev drives a vendor CLI
through a PTY and the compatibility behavior that path needs. What is delivered
and when it is delivered stay with their own owners: the tasks system owns the
message API and its delivery modes, and the platform system owns delivery into a
generating turn.
## Terminology
- **CLI passthrough session:** An agent session whose profile sets
`cli_passthrough`, where Kandev runs the vendor CLI as a PTY child and mirrors
that terminal to the operator.
- **Prompt body:** The prompt text Kandev delivers, excluding the submit
keystroke.
- **Submit keystroke:** The byte sequence that makes the receiving TUI start a
turn for the delivered body.
- **Bracketed paste:** The terminal convention in which `ESC[200~` and `ESC[201~`
delimit pasted text so a TUI treats the whole burst as one paste rather than a
stream of keystrokes.
- **Single-read capacity:** The number of bytes a host terminal hands its reader
per read. It is a host constant, not a Kandev setting, and is smaller than a
typical prompt.
## Requirements
### REQ-CLI-PASSTHROUGH-PROMPT-DELIVERY-001: Passthrough Prompt Delivery Integrity
**Intent:** A prompt that Kandev accepts for a passthrough session must reach
the agent as the same text the operator or calling agent sent. Silent partial
delivery is worse than a failed delivery: the agent acts on a fragment, the
answer looks coherent, and the loss is invisible in every Kandev surface.
**User story:** As an operator running an agent in CLI passthrough mode, I want
every prompt to arrive whole, so that the agent answers what was actually sent.
#### Acceptance criteria
- **AC-CLI-PASSTHROUGH-PROMPT-DELIVERY-001.1:** When Kandev delivers a prompt to
a CLI passthrough session, the receiving agent shall receive the prompt body
in full and unmodified, at any body length.
- **AC-CLI-PASSTHROUGH-PROMPT-DELIVERY-001.2:** When a prompt body exceeds the
host's single-read capacity, delivery shall preserve body order and shall
present the body to the receiving TUI as one continuous input rather than as
unrelated input events it has to rejoin.
- **AC-CLI-PASSTHROUGH-PROMPT-DELIVERY-001.3:** When the prompt body has been
delivered, the submit keystroke shall arrive as a discrete input event, so the
receiving TUI starts exactly one turn for that body.
- **AC-CLI-PASSTHROUGH-PROMPT-DELIVERY-001.4:** When the receiving agent is
configured as unable to accept bracketed-paste framing, delivery shall keep
AC-CLI-PASSTHROUGH-PROMPT-DELIVERY-001.1 true by pacing the body within the
host's single-read capacity instead of writing one oversized burst.
- **AC-CLI-PASSTHROUGH-PROMPT-DELIVERY-001.5:** When the prompt body contains
byte sequences that would end the delivery framing early, delivery shall
neutralize those sequences so no part of the body reaches the TUI as terminal
input outside the prompt.
- **AC-CLI-PASSTHROUGH-PROMPT-DELIVERY-001.6:** When a prompt originates from the
initial task description, an operator composer message, a workflow step, or an
agent-to-agent message, delivery integrity shall be identical, because all
passthrough prompt sources share one delivery contract.
- **AC-CLI-PASSTHROUGH-PROMPT-DELIVERY-001.7:** When delivery of a body cannot
complete, Kandev shall report the failure to the caller rather than leaving a
partially typed prompt as a successful delivery.
## Out of scope
- Prompt transport for non-passthrough sessions, which uses the structured agent
protocol and not a terminal.
- Operator keystrokes typed directly into the mirrored terminal view, which the
terminal input path owns.
- Which prompt is delivered, its wrapping, its queue position, and its delivery
mode, owned by the tasks system.
- Delivery into an already generating turn, owned by the platform system's
mid-turn steering contract.
- How the receiving agent interprets or answers the prompt once it has it.
- Message storage and rendering in Kandev surfaces, owned by the tasks and UI
systems.
@@ -0,0 +1,120 @@
---
status: draft
system: cli
requirements:
- REQ-CLI-PASSTHROUGH-PROMPT-DELIVERY-001
---
# Passthrough Prompt Delivery System Design
## Purpose and boundaries
This design owns how a prompt that is already selected for a CLI passthrough
session becomes bytes on that session's PTY. It owns the framing of the body,
the size and pacing of the writes, and the separation of the submit keystroke.
It does not own the prompt text, its system wrapper, its queue position, or its
delivery mode; those arrive from the tasks system. It does not own PTY process
startup, terminal mirroring, or operator keystrokes, which belong to the
interactive process runner. It consumes the per-agent passthrough capability
record but does not own the agent catalog.
## Requirement mapping
| Requirement | Design section |
| --- | --- |
| `REQ-CLI-PASSTHROUGH-PROMPT-DELIVERY-001` | [Components and responsibilities](#components-and-responsibilities), [Data and contracts](#data-and-contracts), [Control flow](#control-flow), [Failure and recovery](#failure-and-recovery), [Security](#security) |
## Components and responsibilities
- **Passthrough capability record.** Per-agent configuration that states the
submit sequence, the submit delay, and whether the agent accepts
bracketed-paste framing. It is the only input that varies delivery behavior
between agents; delivery code never branches on an agent identity.
- **Stdin chunk planner.** The single function that converts a prompt and a
capability record into an ordered list of PTY writes. It is pure: no PTY, no
clock, no I/O, so every framing and sizing rule is unit-testable. It is the
only place that knows about framing markers and the host read budget.
- **Delivery callers.** The initial-prompt injector, the workflow step prompt
handler, and the session prompt path each obtain the planned chunks and write
them in order, honoring each chunk's pre-write delay. They add no framing of
their own.
- **PTY writer.** The lifecycle manager's passthrough stdin call and the
interactive runner behind it write one chunk per call to the PTY master and
surface write errors to the caller.
## Data and contracts
- **Planned chunk.** An ordered pair of the bytes to write and the delay to
apply before writing them. The first chunk carries no delay. This type already
exists and is unchanged.
- **Framing markers.** The bracketed-paste start and end sequences. They are
applied to the body only, never to the submit keystroke.
- **Raw-safe write size.** A byte budget for one write on the unframed path,
chosen below the smallest host single-read capacity Kandev supports, so a
receiving TUI never has to absorb more than one read worth of unframed input
at a time. Splits fall on rune boundaries so no write ends mid-character.
- **Inter-chunk delay.** The pause applied before each continuation write on the
unframed path, sized so the receiving TUI drains one read before the next
arrives.
- **Submit delay.** The existing per-agent pause before the submit keystroke,
which keeps that keystroke a discrete input event rather than trailing bytes
of the body.
## Control flow
1. A caller resolves the session's capability record and asks the planner for
the chunks of a prompt.
2. If framing is selected, the planner neutralizes framing terminators inside the body.
3. When the agent accepts bracketed-paste framing and the body is multi-line or
larger than the raw-safe write size, the planner emits the framed body as one
chunk. Small single-line bodies stay unframed, preserving today's bytes.
4. When the agent does not accept bracketed-paste framing, the planner emits the
body as consecutive chunks no larger than the raw-safe write size, each
continuation chunk carrying the inter-chunk delay.
5. When `SubmitDelay > 0`, the planner appends the submit keystroke as its own
chunk carrying the submit delay. When `SubmitDelay == 0`, it appends the
submit sequence to the final body chunk.
6. The caller writes each chunk in order, waiting for the chunk's delay with the
caller context first.
Ordering is total: the PTY is a single stream, and the caller writes
sequentially, so the body writes and submit keystroke are sent in order. The
unframed pacing reduces burst loss but cannot confirm what the TUI consumed.
## Failure and recovery
A failed write aborts the remaining chunks for that prompt. The session prompt
path returns the failure to its caller, which reverts session state and surfaces
the error, so a half-typed prompt is never reported as delivered. The initial
prompt injector logs the failure and stops rather than retrying, because a retry
would duplicate the already-written prefix in the agent's input.
Delivery does not read the terminal back, so it cannot confirm what the TUI
absorbed. Correctness therefore rests on framing and sizing rather than on
acknowledgement. The 30 ms unframed interval is an empirical pacing policy, not
a universal lossless-delivery guarantee. Any change to these rules needs
evidence from a real TUI.
## Security
Prompt bodies are untrusted for terminal framing. A body that contains a framing
terminator would end the paste early and let the remainder reach the TUI as
terminal input outside the prompt, which in a CLI agent means unintended
commands. The planner therefore neutralizes framing terminators in the body
before wrapping it. It performs no other rewriting: the body is agent-visible
content and must otherwise arrive byte-for-byte.
## Observability
Delivery callers log the byte length of what they write: the initial prompt
injector logs the injected description length, and the session prompt path logs
the prompt length at debug level. The planner itself logs nothing, because it is
pure.
The authoritative delivery check is comparison, not logging: the stored message
text against the receiving agent's own transcript of what it was sent. That
comparison is what exposed the original defect, a loss of exactly N x 1022
bytes from the start of every long body. Per-delivery logging of chunk count and
framing is not implemented; it would let an operator distinguish a mis-planned
delivery from one the TUI mishandled without a transcript.