Skip to content

fix(adapters): render the stdin prompt per attempt in claude/codex/cursor/gemini-local (BLO-22497) - #1362

Merged
allyblockcast[bot] merged 1 commit into
masterfrom
blo-22497-port
Aug 15, 2026
Merged

fix(adapters): render the stdin prompt per attempt in claude/codex/cursor/gemini-local (BLO-22497)#1362
allyblockcast[bot] merged 1 commit into
masterfrom
blo-22497-port

Conversation

@allyblockcast

@allyblockcast allyblockcast Bot commented Aug 14, 2026

Copy link
Copy Markdown

Thinking Path

  • Paperclip is the open source app people use to manage AI agents for work
  • Agents run through per-adapter execute() implementations that resume a prior CLI session when one exists, and fall back to starting a fresh session when that resume fails
  • Those adapters built the stdin prompt once, keyed on the original sessionId, and runAttempt() captured it — so the fresh-session fallback runAttempt(null) reused a resume-delta prompt that deliberately omits the bootstrap prompt and the full task context
  • The recovered attempt could therefore lack the very task context it is being told to continue, silently degrading every session-unavailable recovery
  • PR #1103 fixed exactly this in opencode-local, but the identical defect stayed live in four sibling adapters — including claude-local, the family behind claude_k8s that most of this fleet runs on, and the only one with two runAttempt(null) sites
  • This pull request ports that reviewed pattern to claude-local, codex-local, cursor-local and gemini-local: extract the render into buildAttemptPrompt(effectiveSessionId) and call it from inside runAttempt(resumeSessionId)
  • The benefit is that every session recovery now sends the prompt a cold start would have sent, so a recovered agent resumes with its task context intact instead of a context-free delta

Linked Issues or Issue Description

What Changed

  • claude-local — extracted buildAttemptPrompt(effectiveSessionId), called from inside runAttempt. Covers both runAttempt(null) sites: the session-unavailable/poisoned retry and the ccrotate rotation retry.
  • codex-local — same extraction, plus shouldUseResumeDeltaPrompt also drives promptInstructionsPrefix and the commandNotes IIFE, so those become per-attempt (buildCommandNotes(shouldUseResumeDeltaPrompt)). Without that, a fresh-session retry re-injects the instructions while still logging the resumed-session "skipped stdin instruction reinjection" note — logged metadata contradicting the actual prompt. Also removes the outer instructionsChars, whose value was unconditionally overwritten before its only read.
  • cursor-local — same extraction, no other coupling.
  • gemini-local — same extraction, plus buildArgs also captured the outer prompt (gemini passes it as argv, --prompt <value>, not stdin), so it now takes buildArgs(resumeSessionId, attemptPrompt).
  • cursor-cloud — deliberately unchanged; it has no runAttempt indirection. Verified 0 files touched in the diff.
  • Added four execute.session-recovery.test.ts regression suites (13 tests) mirroring the opencode-local original.

isPaperclipRecoveryWakePayload(...) intentionally stays session-id-independent: cold-start parity is the invariant, not "always include the task prompt". Each adapter has a test pinning that.

Verification

Per-adapter suites driving execute() through a simulated session-unavailable first attempt (mocked runChildProcess) and asserting on the second attempt's stdin — argv for gemini:

adapter asserting test tests
claude-local session-unavailable fallback renders a fresh-session prompt on the recovered attempt… + ccrotate rotation fallback also renders a fresh-session prompt on its retry (the second runAttempt(null) site) 4
codex-local renders a fresh-session prompt … while leaving the original resume attempt's prompt unchanged 3
cursor-local same 3
gemini-local same 3

Each file also carries a cold-start guard (initial-attempt prompt unchanged) and a recovery-wake parity case.

Mutation-tested — every test was demonstrated to fail against the unported code, as BLO-22497's verifying signal requires. I re-introduced the defect (buildAttemptPrompt(resumeSessionId)buildAttemptPrompt(sessionId)) per adapter and re-ran:

adapter with defect re-introduced
claude-local 3 failed | 1 passed
codex-local 2 failed | 1 passed
cursor-local 2 failed | 1 passed
gemini-local 2 failed | 1 passed

All failing on expected '## Paperclip Resume Delta…' to contain 'BOOTSTRAP-MARKER-BLO-22497'. In every case the single surviving pass is the cold-start guard — correct, because a cold start has sessionId === null so the defect cannot manifest there. Files restored and re-verified green.

# CI job: Run grouped general test suites → group general-workspaces-b
pnpm test:run:general -- --group general-workspaces-b

general-workspaces-b is confirmed to cover these projects from scripts/run-vitest-stable.mjs:79-80 (every non-server project except @paperclipai/ui / paperclipai).

Full adapter suites (the four ported + opencode-local), this branch vs. stashed-clean master:

  • branch: 1 failed | 385 passed | 1 skipped (387)
  • master: 1 failed | 372 passed | 1 skipped (374)

The one failure is identical on both sides — codex-local/src/server/execute.auth-precedence.test.ts > logs and emits a run event when sandbox login is shadowed by host authpre-existing and unrelated, reproduced on clean master in isolation and in the parallel run. The +13 delta is exactly the new tests. tsc --noEmit clean on all four adapters.

Risks

  • Low risk, and the healthy path is explicitly pinned. The change is a pure code motion: the same expressions, evaluated per attempt instead of once. On the initial attempt resumeSessionId === sessionId, so the rendered prompt is byte-identical to today's for both the resumed-session and cold-start cases — each adapter has a cold-start guard test asserting exactly that.
  • The prompt is now rendered once per attempt rather than once per run. Recovery attempts are rare and renderTemplate is cheap, so this is negligible; promptMetrics is returned data consumed by the already-per-attempt onMeta, so no side effect is double-counted.
  • codex-local carries the only behavioral change beyond the prompt: its command notes now reflect the attempt rather than the run. That is a correctness fix (the notes previously contradicted the prompt on a recovered attempt) but it does alter logged metadata, so log-scraping on that string would see the corrected value.
  • No migration, no schema change, no public API change. cursor-cloud untouched.

Model Used

  • Claude Opus 4.5 (claude-opus-5[1m]), 1M context, extended thinking, with tool use and code execution — run as the Paperclip CTO agent on the claude_k8s adapter.

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used (with version and capability details)
  • I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
  • I have searched GitHub for duplicate or related PRs and linked them above
  • I have either (a) linked existing issues with Fixes: # / Closes # / Refs # OR (b) described the issue in-PR following the relevant issue template
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • If this change affects the UI, I have included before/after screenshots — n/a, no UI surface
  • I have updated relevant documentation to reflect my changes — code comments at each buildAttemptPrompt explain why the render must be per-attempt
  • I have considered and documented any risks above
  • All Paperclip CI gates are green — in progress at time of opening
  • Greptile is 5/5 with no open P2s, recommendations, or follow-ups
  • I will address all Greptile and reviewer comments before requesting merge

🤖 Generated with Claude Code

…rsor/gemini-local (BLO-22497)

PR #1103 fixed the once-computed-prompt defect in opencode-local only. The
identical defect was live in four sibling adapters: each built the stdin
prompt once at outer scope, keyed on the original sessionId, and runAttempt()
captured it. When the session-unavailable fallback calls runAttempt(null) —
a genuinely new session — it reused the resume-only prompt, so the recovered
attempt could lack the task context it was told to continue.

Port the reviewed pattern from #1103: extract the render into
buildAttemptPrompt(effectiveSessionId) and call it from inside
runAttempt(resumeSessionId) with that attempt's own id.

- claude-local: covers BOTH runAttempt(null) sites — the session-unavailable/
  poisoned retry and the ccrotate rotation retry.
- codex-local: shouldUseResumeDeltaPrompt also drives promptInstructionsPrefix
  and the command notes, so those move per-attempt too; a fresh-session retry
  re-injects instructions and must not inherit the resumed-session "skipped
  reinjection" note. Drops the outer instructionsChars, whose value was always
  overwritten before its only read.
- gemini-local: passes the prompt via argv, so buildArgs now takes the
  attempt's prompt rather than capturing the outer one.
- cursor-cloud is deliberately unchanged: it has no runAttempt indirection.

The isPaperclipRecoveryWakePayload term stays session-id-independent, so
recovery-shaped wakes still suppress the task prompt — cold-start parity is
the invariant, not "always include the task prompt".

Co-Authored-By: Claude <noreply@anthropic.com>
@allyblockcast

allyblockcast Bot commented Aug 14, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-22301
🔗 Paperclip issue: BLO-22497

1 similar comment
@allyblockcast

allyblockcast Bot commented Aug 14, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-22301
🔗 Paperclip issue: BLO-22497

@allyblockcast

allyblockcast Bot commented Aug 14, 2026

Copy link
Copy Markdown
Author

Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention:

Missing or incomplete:

  • Missing section: ## Thinking Path
  • Missing section: ## What Changed
  • Missing section: ## Risks
  • Missing section: ## Model Used
  • Add the dedup-search checkbox to your PR description and check it once you have searched the GitHub PR list for similar PRs. See the PR template at .github/PULL_REQUEST_TEMPLATE.md and CONTRIBUTING.md → "Before You Start: Search First".

Once updated, push a new commit and these checks will re-run automatically.

— commitperclip

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 96b5a8b

Looks good. The prompt is now rendered from the session ID actually used by each attempt, and the regression coverage exercises both resumed and recovered attempts across all four adapters. I found no high-confidence correctness, safety, error-handling, or type-design issues in the changed code.

Critical Issues (0)

Important Issues (0)

Suggestions (0)

Strengths

  • Recovery tests assert the actual second-attempt prompt, including Gemini’s argv-based prompt path.
  • Claude coverage includes both independent fresh-session fallback sites.
  • The Codex command-note change keeps operational metadata aligned with the per-attempt prompt semantics.

Recommended Action

  1. No Critical or Important code changes are required from this review.

@allyblockcast
allyblockcast Bot enabled auto-merge August 15, 2026 09:14
@allyblockcast
allyblockcast Bot added this pull request to the merge queue Aug 15, 2026
Merged via the queue into master with commit 4fd90f7 Aug 15, 2026
32 of 38 checks passed
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.

0 participants