fix: deliver long CLI passthrough prompts intact (#3894)
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user