Skip to content

dor send prompt arrives truncated in Claude Code (missing 1,022-byte prefix) #679

Closed

Description

@nedtwigg

A long prompt sent through dor send --raw --text ... --key enter reached Claude Code with its beginning missing. The CLI reported success. Claude explicitly confirmed receiving only the second half of the brief and therefore did not run the intended /simplify task until a shorter retry was sent.

Observed September 16, 2026, around 20:27–20:29 America/Los_Angeles, while developing workspace tabs in PR #678. This issue records the evidence only; no fix is included in that PR.

Environment

  • macOS; sending shell: zsh.
  • Repository/worktree: /Users/ntwigg/projects/dormouse.workspace-tab, HEAD 01825050 at the time.
  • Target: surface:7, created using dor ensure --cwd /Users/ntwigg/projects/dormouse.workspace-tab -- claude --model fable.
  • Target UI: Claude Code v2.1.274, Fable 5.1 with high effort, Claude Max; auto mode enabled.
  • A preceding dor read surface:7 --scrollback --lines 80 showed the correct working directory and an idle prompt.

Exact first send

The tool call used exec_command, with workdir set to the worktree above, and this command (the prompt is one line of ASCII):

dor send surface:7 --raw --text '/simplify and commit the complete workspace-tab feature since f933dcbd (current HEAD 01825050) in /Users/ntwigg/projects/dormouse.workspace-tab. Stay on this branch/worktree. Review the entire feature diff, simplify where useful, preserve behavior, update owning specs for any changes, run relevant checks, and commit the result. Important invariants: tabs use shared Door geometry and active/inactive pane header colors, 220ms header palette tween, gradient clear of focus ring; command-mode arrows select workspace tabs/+ and Enter activates/creates and enters terminal immediately; clicking inactive tabs enters command mode, clicking active tabs renames without changing mode; title editing pauses ants and Enter resumes; focus animation retains the selected Door/tab origin; keyboard x reveals and confirms workspaces then focuses the next tab, Door deletion focuses the next minimized Door; closing the final workspace atomically creates a fresh workspace and selects its tab; create/select/close/transfer animate with pane 440ms timing; outgoing workspaces dim LINEARLY only to 50% opacity until incoming workspace expands, keep terminal elements attached through overlap and input exclusively on active Wall, support rapid reversals and reduced motion. Transfer of final workspace still closes its source Window, without creating a replacement. All 3244 lib tests and 42 transfer tests passed before the final fade tuning; final 32 motion/window tests, typecheck and spec lint passed. Do not push or open a PR; report your changes, validation, commit and any remaining concerns when finished.' --key enter

Result, exit code 0:

sent surface:7  [2 inputs]

The sent prompt is 1600 ASCII bytes. Its expected beginning was /simplify and commit the complete workspace-tab feature since f933dcbd ....

Observed reception and submission

A subsequent dor read surface:7 --lines 45 showed the input still at the prompt, beginning with:

❯ th pane 440ms timing; outgoing workspaces dim LINEARLY only to 50% opacity until incoming workspace
  expands, keep terminal elements attached through overlap and input exclusively on active Wall,
  ...

The assistant then sent a separate Enter:

dor send surface:7 --key enter

Result: sent surface:7 [1 input]. The submitted message in the Claude transcript also begins th pane 440ms timing; ....

That starting text corresponds exactly to byte offset 1022 (zero-based) in the original ASCII prompt: the first 1022 bytes were absent, leaving its final 578 bytes, starting in the middle of with.

Claude's response, visible in the reporter's first screenshot:

The work is already committed and workspace-tab is clean at 0182505, so I re-ran validation and changed nothing. Your message arrived cut off at the start ("th pane 440ms timing..."), so I only have the second half of the brief. Nothing was pushed and no PR was opened.

This is evidence that the actual submitted prompt was truncated, not merely clipped in dor read or by the screenshot viewport. The lost prefix included /simplify, the feature baseline, repository scope, and much of the behavioral brief.

Successful retry

A shorter prompt was sent as text, followed by Enter in a separate tool call:

dor send surface:7 --raw --text '/simplify and commit the full workspace-tab feature diff f933dcbd..HEAD in this worktree. Preserve all existing behavior and regression tests, including the latest linear fade to 50% opacity. Read AGENTS.md and relevant specs. Review the entire feature, simplify where useful, test, and commit. Do not push or open a PR. Report completion, changes, tests, and commit.'
# Separate exec_command invocation:
dor send surface:7 --key enter

The retry is 367 ASCII bytes. Claude displayed its complete /simplify and commit ... f933dcbd..HEAD prefix, launched four simplify reviewers, and ultimately committed b73e1ace. The reporter's second screenshot shows the complete retry and the launched review agents.

The successful retry changed both prompt length and submission timing; it does not isolate which factor caused the first failure.

Expected / investigation boundary

  • Deliver the entire text passed to --text, in order, including the initial slash command.
  • Do not silently report successful delivery when the destination receives only a suffix.
  • Inspect the dor send → host/PTY → interactive application input path, including initial-session readiness and the combined text/Enter sequence.

The cutoff offset is measured from the actual tool-call text and the observed received suffix. The responsible layer and reproducibility are still unknown. No controlled length sweep or additional reproduction was attempted, and no implementation changes were made for this report. The two user-provided screenshots are transcribed above; they are not uploaded as attachments here.

Activity

  1. dormouse-bot commented on Sep 17, 2026

    @dormouse-bot
    Collaborator

    No fix yet — I could not reproduce this in CI, and the reason is itself the useful finding: nothing between dor send and the PTY can produce this failure shape, so the discard almost certainly happens in the kernel tty layer, and dor send is not the only caller exposed to it.

    The chain above the PTY is length-blind. dor send concatenates --text and the --key byte and hands over one string (encodeSendInputs). The control socket, the Rust bridge, and the sidecar all frame with newline-delimited JSON and reassemble from a buffer, so a framing fault would corrupt the JSON and fail the call, not yield well-formed ASCII. The host handler type-checks params.input and passes it through unsliced (use-dor-control.ts#L841). The cut also lands mid-word inside with at a pure byte offset, which no JSON, UTF-8, or frame boundary in that chain can land on. I confirmed your arithmetic against the transcribed prompt: 1600 bytes, surviving text at offset 1022, 578 bytes delivered.

    What that leaves is the last hop, which has no flow control at all. write() in pty-core.js is p.write(data) — one unbounded burst into the pty master, no chunking and no pacing, and both hosts share it. A prefix vanishing while the suffix survives intact is the signature of an input-queue flush, not of per-byte drops (those would leave a hole in the middle, since the app is draining concurrently). 1022 sits just under BSD's 1024-byte tty input-queue bound, which is consistent with the flush happening once the queue crossed it.

    The wider consequence, and why this probably isn't a dor send bug. That same unguarded write() is the only path for clipboard paste too — writePasteToPty writes the whole payload, bracketing markers included, in one call. Bracketed paste doesn't help here: the app is still reading the same tty in raw mode, so the same queue bound applies. If the mechanism is what the shape suggests, pasting more than ~1KB into any raw-mode TUI on macOS loses its head just as silently, and the durable fix is chunk-and-pace inside that one shared write() rather than anything in dor. The sent response is unconditional by construction — the handler responds right after writePty and there is no acknowledgement channel from the PTY — so no amount of care in dor can make the success report honest; bounding the burst is what would.

    One run settles it. This isolates the kernel from everything Dormouse does — no node-pty, no sidecar, no webview. On this Linux runner it shows no loss at any size (the master write goes short at 16KB and libuv retries it, which is the correct behavior); if starts_at comes back non-zero on your Mac, the tty layer is confirmed and the fix location is settled.

    tty-input-limit.py — run on the affected Mac
    #!/usr/bin/env python3
    """Isolate whether the kernel tty layer discards a large one-shot write to a
    pty master. No node-pty, no Dormouse: just openpty + raw slave + one write."""
    import os, pty, select, termios, tty
    
    def probe(n, reader_delay=0.05):
        master, slave = pty.openpty()
        tty.setraw(slave, termios.TCSANOW)          # what an Ink/TUI app does
        payload = bytes(48 + (i % 10) for i in range(n))
        written = os.write(master, payload)          # one burst, like pty.write()
        got = b""
        while True:
            r, _, _ = select.select([slave], [], [], reader_delay)
            if not r:
                break
            got += os.read(slave, 65536)
        os.close(master); os.close(slave)
        start = payload.find(got[:32]) if got else -1
        return written, len(got), start
    
    print(f"{'bytes':>7} {'accepted':>9} {'received':>9} {'starts_at':>10}")
    for n in (512, 1024, 1200, 1600, 2048, 4096):
        w, r, start = probe(n)
        print(f"{n:>7} {w:>9} {r:>9} {start:>10}")

    Linux runner, for comparison — starts_at is 0 at every size, meaning nothing was dropped:

      bytes  accepted  received  starts_at
        512       512       512          0
       1024      1024      1024          0
       1200      1200      1200          0
       1600      1600      1600          0
       2048      2048      2048          0
       4096      4096      4096          0
    

    reader_delay stands in for an app that isn't draining instantly. Raising it widens the window; a reader that drains fast enough may mask the loss, which also explains why the 367-byte retry succeeded without isolating length from timing.

    I stopped short of a fix deliberately: choosing a chunk size and pacing without a measured threshold is guesswork, and a wrong bound would look like it worked while still losing data on a slower reader.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions