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).
|
||||
Reference in New Issue
Block a user