fix: deliver long CLI passthrough prompts intact (#3894)
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
---
|
||||
created: 2026-09-22
|
||||
status: complete
|
||||
requirements:
|
||||
- REQ-CLI-PASSTHROUGH-PROMPT-DELIVERY-001
|
||||
system_design:
|
||||
- ../../specs/cli/system-design/passthrough-prompt-delivery.md
|
||||
legacy_specs: []
|
||||
---
|
||||
|
||||
# Implementation Plan: Passthrough Prompt Delivery Integrity
|
||||
|
||||
## Overview
|
||||
|
||||
A prompt delivered to a CLI passthrough session loses whole leading read-sized
|
||||
blocks of its body. Measured against a live instance, every agent-to-agent
|
||||
message longer than one read arrived at the agent missing exactly N x 1022
|
||||
bytes from the front, with no error on any surface. The planner writes the whole
|
||||
body as one unframed PTY write, so the receiving TUI has to absorb a burst many
|
||||
times larger than one terminal read and silently drops the reads it cannot take.
|
||||
|
||||
Two work orders correct this in the one place that plans PTY writes. The first
|
||||
restores bracketed-paste framing for TUIs that accept it, which a probe against
|
||||
the real CLI showed carries 16 KB in a single write without loss. The second
|
||||
paces the remaining unframed path so a body can never exceed one read per write.
|
||||
|
||||
## Scope
|
||||
|
||||
### In scope
|
||||
|
||||
- Frame passthrough prompt bodies with bracketed paste when the agent accepts
|
||||
it, keeping the submit keystroke a separate delayed write.
|
||||
- Neutralize framing terminators inside the body.
|
||||
- Split unframed bodies into rune-safe writes within the host read budget, with
|
||||
an inter-write delay.
|
||||
- Update the agent capability records that currently disable framing, and the
|
||||
tests that pin the superseded behavior.
|
||||
|
||||
### Out of scope
|
||||
|
||||
- The structured (non-passthrough) prompt path.
|
||||
- Operator keystrokes typed into the mirrored terminal view.
|
||||
- Message wrapping, queueing, delivery mode, and mid-turn steering.
|
||||
- Reading the terminal back to acknowledge delivery.
|
||||
- The local hotfix build and install on the operator's instance, which happens
|
||||
outside the repository and is tracked in the session that requested it.
|
||||
|
||||
## Technical approach
|
||||
|
||||
`PlanPassthroughStdinChunks` stays the single planner and stays pure. It gains
|
||||
three rules: neutralize framing terminators in the body; frame the body when the
|
||||
capability record allows it and the body is multi-line or larger than the
|
||||
raw-safe write size; otherwise emit the body as consecutive rune-safe writes no
|
||||
larger than the raw-safe size, each continuation carrying an inter-write delay.
|
||||
The submit keystroke keeps its own trailing chunk and its existing delay.
|
||||
|
||||
Callers already loop over planned chunks and honor `DelayBefore`, so no caller
|
||||
changes. The raw-safe write size is set below the smallest supported host read
|
||||
capacity (1022 bytes on macOS); 400 bytes with a 30 ms inter-write delay is the
|
||||
combination verified against the real CLI.
|
||||
|
||||
`claude_acp.go` and `tui_agent.go` stop setting `DisableBracketedPaste`. The
|
||||
field stays in the capability record as the lever for a TUI that genuinely
|
||||
cannot accept framing, and work order 02 makes that path safe.
|
||||
|
||||
## Tests
|
||||
|
||||
- `AC-CLI-PASSTHROUGH-PROMPT-DELIVERY-001.1`: planner tests in
|
||||
`apps/backend/internal/agent/agents/passthrough_payload_test.go` assert the
|
||||
concatenated chunk bodies reproduce the prompt exactly, for a body far larger
|
||||
than one read, on both the framed and the unframed path.
|
||||
- `AC-CLI-PASSTHROUGH-PROMPT-DELIVERY-001.2`: a framed body is one chunk
|
||||
carrying start and end markers; an unframed body is consecutive chunks in
|
||||
order.
|
||||
- `AC-CLI-PASSTHROUGH-PROMPT-DELIVERY-001.3`: the submit keystroke remains the
|
||||
final, separate chunk with `DelayBefore` equal to the configured submit delay.
|
||||
- `AC-CLI-PASSTHROUGH-PROMPT-DELIVERY-001.4`: with `DisableBracketedPaste` set,
|
||||
no chunk exceeds the raw-safe size and each continuation carries the
|
||||
inter-write delay.
|
||||
- `AC-CLI-PASSTHROUGH-PROMPT-DELIVERY-001.5`: a body containing a framing
|
||||
terminator is neutralized before framing, and the marker does not survive into
|
||||
a delivered chunk.
|
||||
- `AC-CLI-PASSTHROUGH-PROMPT-DELIVERY-001.6`: covered by the planner being the
|
||||
single source for all callers; existing caller tests in
|
||||
`manager_passthrough_autoinject_test.go` and
|
||||
`executor_passthrough_prompt_test.go` keep that wiring honest.
|
||||
- `AC-CLI-PASSTHROUGH-PROMPT-DELIVERY-001.7`: existing passthrough prompt error
|
||||
handling in `executor_passthrough_prompt_test.go` already asserts a write
|
||||
failure surfaces to the caller; work order 02 keeps it passing with several
|
||||
writes in flight.
|
||||
|
||||
## E2E tests
|
||||
|
||||
No Playwright scenario is added. The defect lives between the Go planner and a
|
||||
vendor TUI's stdin handling, which Playwright does not observe. The behavioral
|
||||
check that matters is the manual TUI probe recorded in work order 01, plus the
|
||||
post-deploy comparison of stored message text against the agent's own
|
||||
transcript.
|
||||
|
||||
## Work orders
|
||||
|
||||
- [x] [Task 01: Frame passthrough prompt body](task-01-frame-passthrough-prompt-body.md) (done)
|
||||
- [x] [Task 02: Pace unframed passthrough writes](task-02-pace-unframed-passthrough-writes.md) (done)
|
||||
|
||||
## Verification results
|
||||
|
||||
Both work orders completed. A probe against the real vendor CLI confirmed a
|
||||
framed 8 KB body written in one PTY write is accepted whole and submitted by the
|
||||
delayed submit keystroke. `go test ./internal/agent/agents/` and
|
||||
`./internal/orchestrator/executor/` pass; `go vet` and `gofmt` are clean and the
|
||||
backend builds. Failures in `./internal/agent/runtime/lifecycle/` (worktree
|
||||
preparer, Kubernetes prepare-script, and SSH identity-agent tests) are
|
||||
environment-dependent: the same twelve tests fail on a pristine checkout of the
|
||||
base branch, with and without this change. `golangci-lint` was not run: it is
|
||||
not installed on the development machine.
|
||||
|
||||
End-to-end on a live instance running the patched `v0.95.0` build, against a
|
||||
Claude CLI passthrough session: the initial prompt injector wrote 10174 bytes
|
||||
and the agent's own transcript recorded 10174 bytes, starting at the first byte,
|
||||
with all 60 line markers present. A `message_task_kandev` message of 9213 bytes,
|
||||
the path that exposed the defect, arrived byte-for-byte identical to the stored
|
||||
message, and the agent reported every marker from first to last. Before the
|
||||
correction, every stored agent message above 5 KB had lost between 1022 and
|
||||
5110 bytes from its start.
|
||||
|
||||
## Risks
|
||||
|
||||
- A TUI that does not enable bracketed-paste mode would render the markers as
|
||||
literal text. Work order 01 confirms the framed body is accepted and submitted
|
||||
by the real CLI before the capability records change.
|
||||
- Custom TUI agents inherit the same change as the built-in Claude record. They
|
||||
share the same TUI family but were not individually probed.
|
||||
- The unframed path becomes slower in proportion to body size, which is
|
||||
intentional: correctness over latency for a path with no acknowledgement.
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
---
|
||||
id: "01-frame-passthrough-prompt-body"
|
||||
title: "Frame passthrough prompt body"
|
||||
status: done
|
||||
wave: 1
|
||||
depends_on: []
|
||||
plan: "plan.md"
|
||||
requirements:
|
||||
- REQ-CLI-PASSTHROUGH-PROMPT-DELIVERY-001
|
||||
acceptance_criteria:
|
||||
- AC-CLI-PASSTHROUGH-PROMPT-DELIVERY-001.1
|
||||
- AC-CLI-PASSTHROUGH-PROMPT-DELIVERY-001.2
|
||||
- AC-CLI-PASSTHROUGH-PROMPT-DELIVERY-001.3
|
||||
- AC-CLI-PASSTHROUGH-PROMPT-DELIVERY-001.5
|
||||
- AC-CLI-PASSTHROUGH-PROMPT-DELIVERY-001.6
|
||||
system_design:
|
||||
- ../../specs/cli/system-design/passthrough-prompt-delivery.md
|
||||
---
|
||||
|
||||
# Task 01: Frame Passthrough Prompt Body
|
||||
|
||||
## Summary
|
||||
|
||||
Restore bracketed-paste framing for passthrough agents that accept it, so a
|
||||
prompt body larger than one terminal read reaches the TUI as one paste instead
|
||||
of a burst it drops reads from. Neutralize framing terminators inside the body
|
||||
first, and keep the submit keystroke a separate delayed write.
|
||||
|
||||
## In scope
|
||||
|
||||
- Add failing planner tests for a body far larger than one read, for framing
|
||||
markers, and for a body containing a framing terminator.
|
||||
- Neutralize framing terminators in the body inside the planner.
|
||||
- Frame the body when the capability record allows framing and the body is
|
||||
multi-line or larger than the raw-safe write size.
|
||||
- Stop setting `DisableBracketedPaste` in the built-in Claude capability record
|
||||
and in the custom TUI agent constructor.
|
||||
- Update the existing tests that pin the superseded unframed behavior.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- The unframed delivery path, which work order 02 paces.
|
||||
- Caller changes: the initial-prompt injector, workflow step handler, and
|
||||
session prompt path already loop over planned chunks.
|
||||
- Detecting bracketed-paste support from the terminal output stream.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- A probe against the real vendor CLI confirms a framed body of at least 16 KB
|
||||
written in one PTY write reaches the input intact and is submitted by the
|
||||
delayed submit keystroke. If submission fails, stop and re-plan: the fallback
|
||||
is the paced path from work order 02 for this agent too.
|
||||
- Concatenating the planned body chunks reproduces the prompt exactly, with
|
||||
framing markers only at the outer boundary and none surviving from the body.
|
||||
- The submit keystroke remains the final separate chunk carrying the configured
|
||||
submit delay, and small single-line bodies keep their current bytes.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
(cd apps/backend && go test ./internal/agent/agents/ -run 'TestPlanPassthroughStdin|TestBuildPassthroughPayload|TestNewTUIAgentDefaultsToInkSafePassthrough|TestClaudeACP' -count=1)
|
||||
(cd apps/backend && go test ./internal/agent/agents/ ./internal/agent/runtime/lifecycle/ ./internal/orchestrator/executor/ -count=1)
|
||||
(cd apps/backend && gofmt -l internal/agent/agents)
|
||||
```
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/backend/internal/agent/agents/passthrough_payload.go`
|
||||
- `apps/backend/internal/agent/agents/passthrough_payload_test.go`
|
||||
- `apps/backend/internal/agent/agents/claude_acp.go`
|
||||
- `apps/backend/internal/agent/agents/tui_agent.go`
|
||||
- `apps/backend/internal/agent/agents/tui_agent_test.go`
|
||||
- `docs/plans/passthrough-prompt-delivery-integrity/plan.md`
|
||||
- `docs/plans/passthrough-prompt-delivery-integrity/task-01-frame-passthrough-prompt-body.md`
|
||||
|
||||
## Dependencies
|
||||
|
||||
None.
|
||||
|
||||
## Risks
|
||||
|
||||
- `TestNewTUIAgentDefaultsToInkSafePassthrough` and the Claude body-verbatim
|
||||
assertion in `passthrough_payload_test.go` encode the behavior being replaced.
|
||||
Rewrite them to assert the new contract rather than deleting the coverage.
|
||||
- The historical reason for disabling framing was a submit keystroke absorbed
|
||||
into the paste. The separate delayed submit chunk already addresses it; the
|
||||
probe in Acceptance is what proves it, not the commit history.
|
||||
|
||||
## Parallelism
|
||||
|
||||
`sequential`
|
||||
|
||||
## Inputs
|
||||
|
||||
- `REQ-CLI-PASSTHROUGH-PROMPT-DELIVERY-001` and its system design.
|
||||
- Diagnosis evidence: delivered bodies lost exactly N x 1022 bytes from the
|
||||
front; a framed 16 KB body written once arrived complete in a probe against
|
||||
the real CLI; an unframed 8 KB body lost roughly half its lines.
|
||||
- Existing planner, capability records, and caller tests.
|
||||
|
||||
## Results
|
||||
|
||||
PROBE: An 8 KB body framed as a bracketed paste and written to the real CLI in
|
||||
one PTY write was accepted whole, and the submit keystroke written 150 ms later
|
||||
started the turn: the input box cleared and the agent moved to its thinking
|
||||
state. Framing therefore does not break submission, so the design holds and the
|
||||
paced fallback stays reserved for agents that refuse framing.
|
||||
|
||||
RED: Five behavioral failures. The Claude body chunk arrived unframed for a
|
||||
multi-line prompt, an 11 KB multi-line body was not bracketed, a single-line
|
||||
body past the raw-safe size was not bracketed, prompt-embedded paste markers
|
||||
survived into the delivered body, and the custom TUI agent still disabled
|
||||
framing.
|
||||
|
||||
GREEN: The planner now strips paste markers from the body, frames it when the
|
||||
agent accepts framing and the body is multi-line or larger than
|
||||
`passthroughRawSafeWriteBytes`, and keeps the submit byte a separate delayed
|
||||
chunk. `claude_acp.go` and `tui_agent.go` no longer set
|
||||
`DisableBracketedPaste`. `go test ./internal/agent/agents/` and
|
||||
`./internal/orchestrator/executor/` pass. Failures in
|
||||
`./internal/agent/runtime/lifecycle/` (worktree preparer and SSH identity-agent
|
||||
tests) reproduce identically on a pristine checkout of the base branch and are
|
||||
unrelated to this work order.
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
---
|
||||
id: "02-pace-unframed-passthrough-writes"
|
||||
title: "Pace unframed passthrough writes"
|
||||
status: done
|
||||
wave: 2
|
||||
depends_on:
|
||||
- "01-frame-passthrough-prompt-body"
|
||||
plan: "plan.md"
|
||||
requirements:
|
||||
- REQ-CLI-PASSTHROUGH-PROMPT-DELIVERY-001
|
||||
acceptance_criteria:
|
||||
- AC-CLI-PASSTHROUGH-PROMPT-DELIVERY-001.1
|
||||
- AC-CLI-PASSTHROUGH-PROMPT-DELIVERY-001.4
|
||||
- AC-CLI-PASSTHROUGH-PROMPT-DELIVERY-001.7
|
||||
system_design:
|
||||
- ../../specs/cli/system-design/passthrough-prompt-delivery.md
|
||||
---
|
||||
|
||||
# Task 02: Pace Unframed Passthrough Writes
|
||||
|
||||
## Summary
|
||||
|
||||
Make the remaining unframed path safe for an agent whose capability record
|
||||
disables framing. Split the body into rune-safe writes no larger than the
|
||||
raw-safe size and delay each continuation, so the receiving TUI never has to
|
||||
absorb more than one terminal read at a time.
|
||||
|
||||
## In scope
|
||||
|
||||
- Add failing planner tests for an unframed body larger than the raw-safe size,
|
||||
including a multi-byte body whose split would otherwise fall mid-character.
|
||||
- Split the unframed body into ordered chunks within the raw-safe size, on rune
|
||||
boundaries, each continuation carrying the inter-write delay.
|
||||
- Name the raw-safe size and inter-write delay as constants with the host read
|
||||
capacity they derive from.
|
||||
- Keep the submit keystroke the final chunk with its own delay, after the last
|
||||
body chunk.
|
||||
- Confirm a write failure mid-body still surfaces to the caller.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- The framed path from work order 01.
|
||||
- Changing the capability records again; this path exists for agents that
|
||||
genuinely cannot accept framing.
|
||||
- Adaptive sizing or feedback from the terminal output stream.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- With framing disabled, no planned body chunk exceeds the raw-safe size, the
|
||||
concatenated chunks reproduce the prompt exactly, and no chunk ends inside a
|
||||
multi-byte character.
|
||||
- Every continuation body chunk carries the inter-write delay; the first body
|
||||
chunk carries none.
|
||||
- A write error on any chunk aborts the remaining writes and returns to the
|
||||
caller as a failed delivery.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
(cd apps/backend && go test ./internal/agent/agents/ -run 'TestPlanPassthroughStdin' -count=1)
|
||||
(cd apps/backend && go test ./internal/agent/agents/ ./internal/orchestrator/executor/ -count=1)
|
||||
(cd apps/backend && gofmt -l internal/agent/agents)
|
||||
```
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/backend/internal/agent/agents/passthrough_payload.go`
|
||||
- `apps/backend/internal/agent/agents/passthrough_payload_test.go`
|
||||
- `docs/plans/passthrough-prompt-delivery-integrity/plan.md`
|
||||
- `docs/plans/passthrough-prompt-delivery-integrity/task-02-pace-unframed-passthrough-writes.md`
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Work order 01 establishes the framed path and the shared raw-safe size
|
||||
constant this work order reuses.
|
||||
|
||||
## Risks
|
||||
|
||||
- Delivery time grows with body size on this path. Keep the constants in one
|
||||
place with a comment naming the host read capacity, so a future change is a
|
||||
deliberate trade rather than a guess.
|
||||
- Splitting by bytes instead of runes would corrupt non-ASCII prompts, which is
|
||||
exactly the traffic that exposed the original defect.
|
||||
|
||||
## Parallelism
|
||||
|
||||
`sequential`
|
||||
|
||||
## Inputs
|
||||
|
||||
- `REQ-CLI-PASSTHROUGH-PROMPT-DELIVERY-001` and its system design.
|
||||
- Probe evidence: unframed writes of 400 bytes spaced 30 ms apart delivered a
|
||||
3 KB body with no loss, while one 8 KB unframed write lost about half of it.
|
||||
- The planner and tests as left by work order 01.
|
||||
|
||||
## Results
|
||||
|
||||
RED: A 3.8 KB unframed body still planned as one 3800-byte write, and the
|
||||
no-submit-delay path planned one 1201-byte write, both far past the raw-safe
|
||||
size.
|
||||
|
||||
GREEN: `paceRawBody` splits an unframed body into writes of at most
|
||||
`passthroughRawSafeWriteBytes`, cutting on rune boundaries, with
|
||||
`passthroughRawWriteInterval` before each continuation. The submit sequence
|
||||
stays its own delayed chunk when the agent sets a submit delay and otherwise
|
||||
rides on the final body write. The rune-boundary test caught a real off-by-one:
|
||||
a body whose length is an exact multiple of the write size indexed one byte past
|
||||
the end. `go test ./internal/agent/agents/` and
|
||||
`./internal/orchestrator/executor/` pass, `go vet ./internal/agent/agents/...`
|
||||
is clean, `gofmt -l internal/agent/agents` is empty, and `go build ./...`
|
||||
succeeds.
|
||||
|
||||
Not run: `golangci-lint`, which is not installed on this machine and was not
|
||||
added. The changed functions stay well inside the documented limits (longest is
|
||||
28 lines, nesting depth 4).
|
||||
@@ -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