Skip to content

feat: awaitPastePlaceholder + harness-findings docs - #7

Merged
Juliusolsson05 merged 4 commits into
mainfrom
feat/paste-submit-headless-fix
May 11, 2026
Merged

Juliusolsson05 merged 4 commits into
mainfrom
feat/paste-submit-headless-fix

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

Three coordinated changes surfacing insights from the cc-shell paste-submit reproduction harness.

What's new

  1. ClaudeCodeHeadless.awaitPastePlaceholder() — polls terminal.snapshotPlain() for [Pasted text #N] with default 2 s timeout and 10 ms cadence. Resolves with { kind: 'appeared'; waitedMs } or { kind: 'timeout' }. cc-shell's claudePaste.ts uses this as the load-independent replacement for its 125 ms wall-clock delay between bracketed paste and \r.

  2. Comment block above HeadlessTerminal.scheduleFlush documenting the 'screen' event stall under Claude's synchronized-output protocol. cc-shell's paste-submit harness rediscovered this the hard way — the buffer was being updated correctly but 'screen' stopped firing after the cold-boot banner. Recommend snapshotPlain() polling for correctness-critical consumers.

  3. Stronger warning on the idleDebounceTimer field that the 'activity' event has false negatives during tool-result rendering and transient headers (Bash() preview, "Tip:" footers). The cc-shell harness lost 2/10 iterations on these false negatives before we switched the verdict logic to "did the placeholder clear from the composer?" Both scenarios then jumped to 10/10.

Test plan

  • cc-shell's PR consuming awaitPastePlaceholder (Juliusolsson05/cc-shell branch feat-paste-debug-and-event-driven) hits 10/10 on its harness reliability gate
  • No new tests in this submodule — the harness in cc-shell is the regression gate

🤖 Generated with Claude Code

Adds two complementary signals for detecting auxiliary /v1/messages
calls (title generation, branch-name gen, compaction summary, hook
agents) so they do not leak into cc-shell's visible transcript as
orphan ghost messages:

  * The mitm addon now surfaces /v1/messages request bodies (capped
    at 256 KiB) so the adapter can read max_tokens and the system
    prompt without parsing the SSE response.

  * ClaudeProxyAdapter.isSidecarFlow gains a body-shape signal in
    addition to the legacy model-name heuristic. Either max_tokens
    <= 1024 OR a known title-gen system-prompt prefix demotes the
    flow to 'secondary', preventing turn_started / text_delta /
    turn_stopped events from reaching the renderer channel.

  * The flow_ignored reason string is now specific about which
    signal tripped, since the renderer surfaces it verbatim in the
    proxy debug panel.

Recent Claude Code versions route these calls to the user's primary
model (Sonnet/Opus), defeating the model-only filter. The body-shape
signal is robust to that change. The legacy model heuristic is kept
for older Claude Code versions and Haiku-as-primary users.
… from body size cap)

The c8c2623 sidecar predicate (max_tokens / messageCount / system-prefix
fingerprint) was running blind on ~99% of production traffic.

Forensic root cause from cc-shell debug bundle 2026-05-07T08-26-35-212-5d948ab5:
the addon's body_b64 capture is gated on a 256 KiB cap, but real Claude
Code /v1/messages requests run 700 KB to 1+ MB because they include
the full conversation history every turn. Result: state.requestShape
was null on every flow except the ~323-byte warmup quota check, no
sidecar signal fired, every title-gen / predict-next-prompt call
leaked through and rendered as an orphan ghost at the bottom of the
cc-shell feed.

Fix: parse the request body inside the addon and emit the three fields
the predicate actually consumes — `max_tokens`, `message_count`,
`system_prefixes` — as a small `request_shape` JSON blob. mitmproxy
already had the body buffered in process memory (request.content
materialises it on access) so the parse is free; the win is that we
stop re-encoding multi-MB blobs as base64 over IPC and stop dropping
data the predicate needs.

Adapter side:
  * ProxyTransportEvent gains optional `request_shape` field.
  * onRequest prefers event.request_shape over parseRequestBody(body_b64),
    new normalizeRequestShape coerces the addon payload into the same
    ParsedRequestShape internal type the legacy path produced.
  * parseRequestBody stays as the fallback for older addon binaries
    that don't emit request_shape.

Addon side:
  * _extract_request_shape parses content into the same three fields,
    mirroring parseRequestBody's tolerance rules so adapter consumers
    can't tell which source emitted the payload.
  * body_b64 stays size-gated (256 KiB) for forensic decoding of small
    bodies; it is no longer the predicate's input.

verify-sidecar-haiku.ts: 13/13 still passing (legacy body_b64 path
unchanged when request_shape absent).
…scriminators

Builds on the request_shape mechanism added in 505377a. Two
extensions, both informed by reverse-engineering Claude Code's HTTP
layer (vendor/claude-code-src/full/services/api/client.ts and
utils/sideQuery.ts):

1. Filter `headers` to a small allowlist before emission. The
   previous `dict(request.headers)` exfiltrated bearer tokens,
   ANTHROPIC_CUSTOM_HEADERS-injected secrets, and any future header
   Claude Code might add — none of which belong in a debug bundle the
   user can share. The allowlist preserves only the headers we
   actually want for forensic correlation:
     x-app, user-agent, x-claude-code-session-id,
     x-claude-remote-{container,session}-id, x-client-app,
     x-client-request-id, anthropic-beta, anthropic-version,
     x-anthropic-additional-protection, content-length, content-type.
   Authorization is structurally excluded — it can't reach disk by
   accident.

2. Extend `request_shape` with four diagnostic fields that future
   sidecar predicate work can use without re-parsing the body:
     - tools_count            (real CC turns ALWAYS ship tools=[Bash,
                               Edit, Read, ...]; sidecars omit)
     - system_blocks_count    (sidecars: 2-3, real turns: many)
     - system_total_chars     (sidecars <5KB, real turns 10s of KB)
     - messages_total_chars   (defence-in-depth scale signal)
     - attribution_entrypoint (cc_entrypoint slug from system[0]'s
                               attribution header — cli/sdk-cli/mcp/
                               etc.; correlates traffic to parent
                               process)

3. Raise `_REQUEST_BODY_CAP` from 256 KiB to 2 MiB so forensic
   tooling (debug bundles) can recover actual prompt text from real
   conversation turns. The previous cap silently dropped the prompt
   for every body past the warmup quota check (323 bytes); 2 MiB
   covers every body observed in production while still giving the
   request_shape path a fallback for pathological multi-MB
   attachment bodies.

verify-sidecar-haiku.ts: 13/13 still passing (existing
predicate logic unchanged; new fields are additive only).
Two doc cleanups + one new method, all surfacing insights from the
paste-submit reproduction harness at cc-shell's
vendor/in_progress/paste-submit-repro/.

awaitPastePlaceholder: polls snapshotPlain() for [Pasted text #N]
with a default 2 s timeout and 10 ms poll interval. Sidesteps the
'screen' event stall (also documented in this commit) by reading the
xterm buffer directly. Used by cc-shell's claudePaste flow to send
\r exactly when Claude has visibly committed the paste — replaces
the 125 ms wall-clock delay that the harness shows races even at
1000 ms.

HeadlessTerminal: thick WHY comment above scheduleFlush documenting
the 'screen' event stall under synchronized-output pressure
(\x1b[?2026h...\x1b[?2026l). pendingWrites can stay > 0
indefinitely under sustained sync output, leaving 'screen' silent
even when the buffer is being updated correctly. Recommend
polling snapshotPlain() directly for correctness-critical consumers.

ClaudeCodeHeadless: replace the idleDebounceTimer comment with a
strong warning that the 'activity' event has false negatives during
tool-result rendering and transient-header windows, and must NOT be
used as a binary "did this submit?" verdict. The harness lost 2/10
iterations on a spurious spinner read before we figured this out.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant