From 9009fbdedb886c19185d5f55c2e3d7a8ec8fbc83 Mon Sep 17 00:00:00 2001
From: Dotta <34892728+cryppadotta@users.noreply.github.com>
Date: Tue, 10 Nov 2026 11:54:55 -0600
Subject: [PATCH] fix(runner): accept request_human_input payload version forms
models send (#20588)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents ask people for decisions with the runner's
`request_human_input` tool, which creates a question or confirmation
card
> - The tool schema required `payload.version` to be the number 1
> - The server ignores the version the model sends and always stores
version 1
> - Some models send `"version": "1"` as a string, so every call failed
with "/payload/version must be equal to constant"
> - This pull request accepts `1` or `"1"`, and lets the model omit the
version
> - The benefit is that the model's natural call creates the card on the
first try, and a turn does not end with no card and no reply
## Linked Issues or Issue Description
Refs #19405 (adds invocation examples for the same tool; it does not
change validation). Refs #20134 (aligns the same tool's text limits).
**What happened?**
In a chat eval run on master `f4337bb5fb` (OpenCode runner,
`deepseek-v4.1-flash`), the agent called `request_human_input` up to six
times. Each call failed with `Invalid tool input: /payload/version must
be equal to constant`. The agent's reasoning shows it sent `"version":
"1"` and could not produce the number. In one case it then made the card
with `call_api`. In two other cases the turn lost the card. This
happened in `questions-and-approvals.reject-and-revise`,
`goals.align-existing-work` and
`assistant-connections.revoke-personal-grant`.
**Expected behavior**
A version field that the server overwrites must not block the call. The
tool must still reject an unknown version and a questions card without a
question form.
**Steps to reproduce**
Validate `{ interactionKind: "questions", payload: { version: "1",
questionSet: {...} } }` against the live `request_human_input` input
schema. Before this change, validation fails at `/payload/version`.
## What Changed
- `payload.version` in the tool schema accepts `1` or `"1"` and is
optional. Its description says Paperclip stores version 1.
- A questions card no longer requires `payload.version`. It still
requires `questionSet` or `questions`.
- The payload description no longer tells the model to send `version:1`.
- Regenerated the semantic tool contracts, the seeded eval catalog
digest and the protocol manifest with the existing generator.
- Added a regression test: number, string and missing version pass for
live and scenario schemas; version `2` and a payload without questions
fail.
## Verification
- `npx vitest run src/protocol-actions/` in `packages/paperclip-runner`:
red before the fix with the exact eval error (`/payload/version must be
equal to constant`), green after (47 tests).
- Focused runner tests for the tool bridge, semantic tools, mock control
plane, codex semantic results, catalog and semantic tools: 231 passed.
- `node scripts/generate-semantic-contracts.mjs --check`,
`generate-semantic-action-catalog --check`, `generate-protocol-manifest
--check`, `generate-replay-goldens --check`: current.
- `node tests/runner-e2e/source-sdk-public-inputs.mjs
--check`:
unchanged.
- Paid chat eval on a temporary branch with this change: see the comment
on this PR.
## Risks
- Low risk. The server already writes `version: 1` into every
interaction payload created by this tool
(`paperclip-runner-tool-authority.ts`), so no stored shape changes.
- Previous-release story: no persisted run, session or launch state
changes. Saved tool receipts keep the input they recorded. A retry with
a different version form under the same idempotency key is a different
input, as before.
## Model Used
- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), agentic coding with
tool use in Claude Code.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5.5
---
.../capability/semantic-tool-contracts.json | 2 +-
.../evals/native-execution-seeded.json | 2 +-
.../paperclip-runner/protocol/manifest.json | 2 +-
.../protocol-actions/core-contracts.test.ts | 25 ++++++++++++++++++-
.../question-payload-schema.ts | 8 +++---
5 files changed, 32 insertions(+), 7 deletions(-)
diff --git a/packages/paperclip-runner/generated/capability/semantic-tool-contracts.json b/packages/paperclip-runner/generated/capability/semantic-tool-contracts.json
index fd412bea2ee..c6802c1c4c4 100644
--- a/packages/paperclip-runner/generated/capability/semantic-tool-contracts.json
+++ b/packages/paperclip-runner/generated/capability/semantic-tool-contracts.json
@@ -1 +1 @@
-[{"annotations":{"exposure":"always","operationId":"get_task_context","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the active task and actor, including the exact approved Markdown revision when this issue has an accepted plan.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"get_task_context","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"get_task_history","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded comments on the active task only. Calling get_task does not change the active task. This is not a status or assignment audit. For progress chronology and handoff actors, use search_api to discover the company timeline or issue activity, then call_api.","inputSchema":{"additionalProperties":false,"properties":{"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":[],"type":"object"},"name":"get_task_history","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_documents","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List revisioned documents on the active task.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_documents","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"read_document","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the current revision of one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable document identifier, not a filename or path (for example reconciliation, not reconciliation.md). After trimming whitespace and lowercasing: 1–64 characters, starting with a letter or digit and containing only a–z, 0–9, _ or -. The raw input is limited to 120 characters.","maxLength":120,"minLength":1,"pattern":"^[\\u0009-\\u000d\\u0020\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]*[A-Za-z0-9\\u212a][A-Za-z0-9_\\u212a-]{0,63}[\\u0009-\\u000d\\u0020\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]*$","type":"string"}},"required":["key"],"type":"object"},"name":"read_document","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_document_revisions","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded revision history for one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable document identifier, not a filename or path (for example reconciliation, not reconciliation.md). After trimming whitespace and lowercasing: 1–64 characters, starting with a letter or digit and containing only a–z, 0–9, _ or -. The raw input is limited to 120 characters.","maxLength":120,"minLength":1,"pattern":"^[\\u0009-\\u000d\\u0020\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]*[A-Za-z0-9\\u212a][A-Za-z0-9_\\u212a-]{0,63}[\\u0009-\\u000d\\u0020\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]*$","type":"string"},"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":["key"],"type":"object"},"name":"list_document_revisions","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"report_progress","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append a durable progress comment to the active task.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Multiline progress update.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"report_progress","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"answer_status_question","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append the answer to a status-only wake without changing task disposition.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Concise status answer.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"answer_status_question","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"write_document","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Create or update an active-task document with optimistic revision safety.","inputSchema":{"additionalProperties":false,"properties":{"baseRevisionId":{"description":"Current revision id, or null when creating.","maxLength":20000,"type":["string","null"]},"body":{"description":"Markdown document body.","maxLength":200000,"minLength":1,"type":"string"},"changeSummary":{"description":"Optional revision summary.","maxLength":20000,"type":["string","null"]},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"key":{"description":"Stable document identifier, not a filename or path (for example reconciliation, not reconciliation.md). After trimming whitespace and lowercasing: 1–64 characters, starting with a letter or digit and containing only a–z, 0–9, _ or -. The raw input is limited to 120 characters.","maxLength":120,"minLength":1,"pattern":"^[\\u0009-\\u000d\\u0020\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]*[A-Za-z0-9\\u212a][A-Za-z0-9_\\u212a-]{0,63}[\\u0009-\\u000d\\u0020\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]*$","type":"string"},"title":{"description":"Document title.","maxLength":300,"minLength":1,"type":"string"}},"required":["idempotencyKey","key","title","body","baseRevisionId"],"type":"object"},"name":"write_document","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"request_human_input","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Create a durable human question or approval card on the current Paperclip task bound to this run; Paperclip renders it and authenticates the response. Use questions with continuationPolicy='wake_assignee' when an answer is needed, including otherwise tool-free chat turns. Supply a stable idempotencyKey and reuse it on retries. When you need several inputs that do not depend on each other's answers, ask them together in one questions card (one questionSet) instead of one card per question; ask a follow-up card only when it depends on an earlier answer. If the user asks for one question at a time, ask only the next unanswered question and wait for its real answer. Never fabricate answers or treat ambiguous clarification as approval. For an ordinary confirmation or checkbox card, record a clear user chat answer with call_api POST /api/issues/{id}/interactions/{interactionId}/resolve-from-comment, using the source commentId and decision (accept/reject), plus explicit selectedOptionIds for checkbox acceptance. Existing resolver permissions still apply; governed tool, secret, and connection approvals are excluded. Question forms retain their dedicated answer workflow. Preserve existing review gates. Call this tool before claiming a question was asked; if creation fails, report the failure. Do not fabricate answer links or Markdown buttons, post duplicate cards, or use call_api to create the card. Use one complete payload.questionSet for text and choice questions. Paperclip generates compatibility questions; see the payload schema for formats. Example inputs (the keys shown are illustrative; choose your own stable idempotencyKey for each request and reuse it on retries). Questions: {\"idempotencyKey\":\"preference-v1\",\"interactionKind\":\"questions\",\"title\":\"Choose a preference\",\"prompt\":\"Which preference should I use?\",\"continuationPolicy\":\"wake_assignee\",\"payload\":{\"version\":1,\"questionSet\":{\"schema\":\"paperclip.question_set.v1\",\"questions\":[{\"id\":\"preference\",\"prompt\":\"Your preference?\",\"required\":true,\"answerMode\":\"text\"}]}}} Confirmation: {\"idempotencyKey\":\"confirm-change-v1\",\"interactionKind\":\"confirmation\",\"title\":\"Confirm change\",\"prompt\":\"Proceed with the proposed change?\",\"continuationPolicy\":\"wake_assignee_on_accept\"}","inputSchema":{"additionalProperties":false,"if":{"properties":{"interactionKind":{"const":"questions"}},"required":["interactionKind"]},"properties":{"continuationPolicy":{"enum":["none","wake_assignee","wake_assignee_on_accept"]},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"},"interactionKind":{"enum":["confirmation","checkbox","questions","suggest_tasks","item_verdicts"]},"payload":{"additionalProperties":true,"description":"Kind-specific interaction data. For questions, send version:1 and one complete questionSet containing every text and choice question. Paperclip generates compatibility questions. Each canonical question needs id, prompt, required, and answerMode: text, single_select, or multi_select. Text questions have no options or customAnswer. Choice questions need at least two meaningful options with id/label. Use customAnswer:{enabled:true} for an optional written answer to a choice question. Legacy questions remain supported; if both representations are supplied, they must describe the same complete form. Keep IDs stable across retries. For required output quality review, confirmation payload must contain qualityReview:{key:} and target:{type:issue_document,key: