diff --git a/doc/plans/2026-10-06-hermes-native-runner.md b/doc/plans/2026-10-06-hermes-native-runner.md index 80b41c5be5..540375e23c 100644 --- a/doc/plans/2026-10-06-hermes-native-runner.md +++ b/doc/plans/2026-10-06-hermes-native-runner.md @@ -1068,3 +1068,44 @@ Codex regressions pass 318 checks. The final result-copy and ordering tests pass 51 checks. Runner TypeScript and generated contracts pass. The pinned runtime and Rust inputs are unchanged from `1f3af78114`. This remains deterministic proof. Clean-head live billing, fresh cloud CI and review remain required. + +### 2026-10-08 governed confirmation correction + +The clean `d54a98895c` Mac question workflow passes in 71.476 seconds. A second +workflow preserves the exact pending interaction across a controller restart +and a fresh browser document, then resumes Hermes to completion in 107.945 +seconds. Both have zero automatic retries and passing cleanup. Each has two +settled, reported OpenRouter receipts with healthy configured budgets. Their +reported totals are $0.002000649 and $0.001891331. Inspected screenshots show +the pending form, Cobalt answer, thought card, one final response, Done state +and available composer. This proves assigned Paperclip human input, not live +native `clarify_callback`. That head has 54 passing cloud checks, two skips, +and fresh Greptile 5/5 with no unresolved threads. Its credential-free Linux +job verifies the recorded closure and passes four native fixtures. + +A separate clean-head planning workflow exposes the same early-stop problem +for `request_confirmation`. The plan is accepted, but its first run remains +pending and unpriced; the company pauses and the wake fails. The owned test +launcher was cancelled after preserving that public state. Its original +243.189-second infrastructure failure and `not_started` cleanup verdict remain +unchanged. Independent checks confirm no owned process group, listener or +temporary instance remains. Unknown spend stays fully reserved. + +The bridge and both event boundaries now accept the three closed canonical +human-input kinds: questions, confirmations and checkbox confirmations. Each +must be an applied pending result from assigned `request_human_input`, with +`wake_assignee` and the original identity. Other kinds, tools, resolved results +and prose cannot stop work. This delays the fact only; the controller retains +wait, approval and accounting authority. The internal launch policy is named +for human input. The native regression covers immediate shutdown for each +kind. The bridge-only closure update retains verified dependencies and the +interpreter; the shared account cache is unchanged. Fresh live confirmation +proof, cloud CI and review remain required before claiming this correction +qualified. Actual Daytona and the broader release gates remain pending. + +The correction passes 57 focused ordering/cancellation checks, 315 shared +Runner regressions and seven distribution fixtures. Runner TypeScript and +generated protocol/sidecar contracts pass. All six credential-free native +fixtures pass in 87.507 seconds, including 38 pinned Python checks and immediate +shutdown with a complete usage delta for each of the three human-input kinds. +These fixtures do not establish live approval, billing or Daytona proof. diff --git a/packages/paperclip-runner/docs/hermes.md b/packages/paperclip-runner/docs/hermes.md index 73cb469224..2f79db6a8b 100644 --- a/packages/paperclip-runner/docs/hermes.md +++ b/packages/paperclip-runner/docs/hermes.md @@ -135,11 +135,12 @@ extend the task's execution deadline. authority. Missing, invalid or timed-out reads do not invent usage or cost. This optional execution-result field carries an existing v1 PRP event; it does not change persisted execution inputs or the Rust wire contract. -- An applied pending `request_human_input` response stops Hermes in its native +- An applied pending `request_human_input` response for a question, confirmation + or checkbox confirmation stops Hermes in its native tool completion callback, before another model request. The bridge publishes that tool's completed result after native finalization. Both Runner event pumps read the prompt usage receipt before forwarding this completion to the - controller. The tool bridge also defers its own completed question fact until + controller. The tool bridge also defers its own completed human-input fact until the native terminal notification, which follows that receipt. The managed Hermes profile selects this launch policy; other profiles retain their existing order. Other tool activity still streams immediately. Tool identities diff --git a/packages/paperclip-runner/scripts/qualify-hermes-runnerd.test.mjs b/packages/paperclip-runner/scripts/qualify-hermes-runnerd.test.mjs index ac6528433d..0a82d35ea3 100644 --- a/packages/paperclip-runner/scripts/qualify-hermes-runnerd.test.mjs +++ b/packages/paperclip-runner/scripts/qualify-hermes-runnerd.test.mjs @@ -23,8 +23,8 @@ const completion = { evidence: [], verification: [{ commandOrCheck: 'native transport fixture', status: 'passed' }], attentionRequests: [], artifacts: [], }; -for (const question of [false, true]) test(question - ? 'pinned Hermes retains prompt usage before a committed question triggers immediate Rust-sidecar shutdown' +for (const interactionKind of [null, "ask_user_questions", "request_confirmation", "request_checkbox_confirmation"]) test(interactionKind + ? `pinned Hermes retains prompt usage before committed ${interactionKind} triggers immediate Rust-sidecar shutdown` : 'pinned Hermes streams images and semantic completion through Rust PRP and the production sidecar', { skip: process.env.PAPERCLIP_HERMES_QUALIFY !== '1', timeout: 180_000, }, async t => { @@ -52,10 +52,10 @@ for (const question of [false, true]) test(question if (!body.messages.some(message => message.role === 'tool')) { send({ role: 'assistant', reasoning_content: 'Checking the native runner path.' }); await new Promise(resolve => setTimeout(resolve, 80)); - const finish = body.tools.find(tool => tool.function.name.endsWith(question ? 'request_human_input' : 'paperclip_finish')); + const finish = body.tools.find(tool => tool.function.name.endsWith(interactionKind ? 'request_human_input' : 'paperclip_finish')); assert.ok(finish, 'Paperclip completion authority was not exposed to the native model'); send({ tool_calls: [{ index: 0, id: 'native-prp-finish', type: 'function', function: { name: finish.function.name, - arguments: JSON.stringify(question ? { marker: 'governed-question' } : completion) } }] }); + arguments: JSON.stringify(interactionKind ? { marker: 'governed-question' } : completion) } }] }); send({}, 'tool_calls'); } else { send({ role: 'assistant', content: 'Native Rust ' }); @@ -109,12 +109,12 @@ for (const question of [false, true]) test(question prpIdentity: { runnerInstanceId: 'hermes-prp-runner', environmentLeaseId: 'hermes-prp-lease', runId: identity.runId, normalizedSessionId: identity.sessionId, turnId: 'hermes-prp-turn', itemId: 'hermes-prp-item' }, }); const backend = createRunnerdNativeSessionBackend(input, { runnerInstanceId: 'hermes-prp-runner', transportFactory: () => bundle.transport, - ...(question ? { - dynamicTools: [{ name: 'request_human_input', description: 'Simulated authenticated question operation', + ...(interactionKind ? { + dynamicTools: [{ name: 'request_human_input', description: 'Simulated authenticated human input operation', inputSchema: { type: 'object', properties: { marker: { type: 'string' } }, required: ['marker'] } }], dynamicToolHandler: async () => ({ disposition: 'applied', interaction: { id: 'fixture-question', companyId: identity.companyId, issueId: identity.issueId, sourceRunId: identity.runId, - kind: 'ask_user_questions', status: 'pending', continuationPolicy: 'wake_assignee', + kind: interactionKind, status: 'pending', continuationPolicy: 'wake_assignee', } }), } : {}), }); @@ -124,16 +124,16 @@ for (const question of [false, true]) test(question let cancellationOutcome; for await (const event of session.events()) { events.push(event); - if (question && event.eventType === 'item.completed' && event.payload.kind === 'dynamicToolCall') { - cancellationOutcome = session.cancel({ reason: 'committed question wait', signal: new AbortController().signal }).cleanup + if (interactionKind && event.eventType === 'item.completed' && event.payload.kind === 'dynamicToolCall') { + cancellationOutcome = session.cancel({ reason: 'committed human input wait', signal: new AbortController().signal }).cleanup .then(() => ({ stopped: true }), error => ({ error })); break; } if (['run.terminal', 'turn.completed', 'turn.failed', 'turn.cancelled', 'turn.interrupted'].includes(event.eventType)) break; } - if (question) { - assert.ok(cancellationOutcome, 'The committed question completion did not reach the native boundary'); - await session.close({ reason: 'immediate question shutdown' }); + if (interactionKind) { + assert.ok(cancellationOutcome, 'The committed human input completion did not reach the native boundary'); + await session.close({ reason: 'immediate human input shutdown' }); const stop = await cancellationOutcome; if (stop.error) assert.equal(stop.error.code, 'already_terminal', 'Provider cancellation failed before terminal settlement'); const usage = await session.accountingUsageEvent?.(); @@ -141,8 +141,8 @@ for (const question of [false, true]) test(question input: usage.payload.usage.runDelta.inputTokens, output: usage.payload.usage.runDelta.outputTokens, complete: usage.payload.usage.runDeltaComplete, }, { input: 10, output: 5, complete: true }, 'Immediate shutdown lost the native prompt receipt'); - assert.ok(events.some(event => event.payload.kind === 'usage'), 'The question completion reached PRP before its usage receipt'); - assert.equal(requests.length, 1, 'Hermes started another model request after the committed question'); + assert.ok(events.some(event => event.payload.kind === 'usage'), 'The human input completion reached PRP before its usage receipt'); + assert.equal(requests.length, 1, 'Hermes started another model request after the committed human input'); return; } assert.ok(events.some(event => event.eventType === 'turn.completed'), JSON.stringify(events)); diff --git a/packages/paperclip-runner/src/backends/codex-native-backend.ts b/packages/paperclip-runner/src/backends/codex-native-backend.ts index f9abfc755a..61c2ac31be 100644 --- a/packages/paperclip-runner/src/backends/codex-native-backend.ts +++ b/packages/paperclip-runner/src/backends/codex-native-backend.ts @@ -181,7 +181,7 @@ function createTransportBackedNativeSessionBackend( transportFactory: options.transportFactory, dynamicTools: options.dynamicTools, dynamicToolHandler: options.dynamicToolHandler, - deferCommittedQuestionResults: input.provider.kind === "acpx" && input.provider.agent === "hermes", + deferCommittedHumanInputResults: input.provider.kind === "acpx" && input.provider.agent === "hermes", completionFeedback: options.completionFeedback, environment: options.environment, workingDirectoryAuthority: options.workingDirectoryAuthority, diff --git a/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts b/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts index fc3596b815..f948ddb182 100644 --- a/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts +++ b/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts @@ -15,7 +15,7 @@ import type { AcpPermissionDecision, } from "acpx/runtime"; -import { acpxProfileClientCapabilities, bindAcpxExtensionTurn, validateAcpxRichEvent, createAcpxProfileExtensionAdapter, isHermesCommittedQuestionCompletion, type AcpxExtensionInput } from "../drivers/acpx/profile-extensions.js"; +import { acpxProfileClientCapabilities, bindAcpxExtensionTurn, validateAcpxRichEvent, createAcpxProfileExtensionAdapter, isHermesCommittedHumanInputCompletion, type AcpxExtensionInput } from "../drivers/acpx/profile-extensions.js"; import type { PaperclipQuestionSet } from "../contracts/question-set.js"; import { readProviderUsageBilling, type ProviderUsageBilling } from "../contracts/usage-billing.js"; import { createAcpxToolEventNormalizer, createGrokMessageNormalizer } from "../provider-events.js"; @@ -635,7 +635,7 @@ async function pumpTurn( toolEvidence?.tool(event); const normalized = normalizeMessage(normalizeToolEvent(boundRuntimeEventForNormalization(event))); if (openParams?.agent === "hermes" && event.type === "tool_call" - && isHermesCommittedQuestionCompletion({ ...normalized, rawOutput: event.rawOutput }, runId ?? "")) { + && isHermesCommittedHumanInputCompletion({ ...normalized, rawOutput: event.rawOutput }, runId ?? "")) { if (committedQuestions.length >= MAX_PENDING_INPUTS) throw new Error("Hermes committed question limit exceeded"); committedQuestions.push(normalized); continue; diff --git a/packages/paperclip-runner/src/drivers/acpx/codex-acpx-driver.ts b/packages/paperclip-runner/src/drivers/acpx/codex-acpx-driver.ts index b51c31aeac..ce2ba7df53 100644 --- a/packages/paperclip-runner/src/drivers/acpx/codex-acpx-driver.ts +++ b/packages/paperclip-runner/src/drivers/acpx/codex-acpx-driver.ts @@ -1,7 +1,7 @@ import { isProviderMode } from "../../contracts/provider-mode.js"; import { acpxProfileActivity, type AcpxActivityAdapter, type AcpxToolEvidence } from "./profile-activity.js"; import { requireAcpxResponseDelivery } from "./response-delivery.js"; -import { acpxProfileClientCapabilities, bindAcpxExtensionTurn, validateAcpxRichEvent, createAcpxProfileExtensionAdapter, isHermesCommittedQuestionCompletion, type AcpxExtensionInput } from "./profile-extensions.js"; +import { acpxProfileClientCapabilities, bindAcpxExtensionTurn, validateAcpxRichEvent, createAcpxProfileExtensionAdapter, isHermesCommittedHumanInputCompletion, type AcpxExtensionInput } from "./profile-extensions.js"; import { createHash, randomBytes } from "node:crypto"; import type { @@ -1467,7 +1467,7 @@ class CodexAcpxSession implements HarnessSession { const projected = activity.toolExecutionId && event.type === "tool_call" && typeof event.toolCallId === "string" ? { ...event, toolCallId: activity.toolExecutionId(event.toolCallId) } : event; const normalized = normalizeMessage(normalizeToolEvent(projected)); - if (this.#agent === "hermes" && isHermesCommittedQuestionCompletion(normalized, this.#input.runId)) { + if (this.#agent === "hermes" && isHermesCommittedHumanInputCompletion(normalized, this.#input.runId)) { if (committedQuestions.length >= 16) throw new Error("Hermes committed question limit exceeded"); committedQuestions.push(normalized); continue; diff --git a/packages/paperclip-runner/src/drivers/acpx/hermes-distributions.ts b/packages/paperclip-runner/src/drivers/acpx/hermes-distributions.ts index a28406bade..a69a2bafbd 100644 --- a/packages/paperclip-runner/src/drivers/acpx/hermes-distributions.ts +++ b/packages/paperclip-runner/src/drivers/acpx/hermes-distributions.ts @@ -1,5 +1,5 @@ /** Reviewed native closures. Setup cannot adopt a digest from downloaded files. */ export const HERMES_CLOSURES: Readonly> = Object.freeze({ - "darwin-arm64": "f443a6867c914f49d7308c7421dbce9ba4a0d024b0c718a41a78845c784a7bd0", - "linux-x64": "c9879c3b69357d17d375d8fd4897f18496cebb21256c1e8bb9220d779d995921", + "darwin-arm64": "2230a296b80cb79079f0e6223449e18affb0e9543a476fb5901fb0524f18a0f5", + "linux-x64": "5c49a3018d6f0875bdbc318f934d05814db9c40add9d91b5e834566b3b8b32b4", }); diff --git a/packages/paperclip-runner/src/drivers/acpx/hermes-extensions.test.ts b/packages/paperclip-runner/src/drivers/acpx/hermes-extensions.test.ts index 5a0341a224..876c9d77b9 100644 --- a/packages/paperclip-runner/src/drivers/acpx/hermes-extensions.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/hermes-extensions.test.ts @@ -1,14 +1,14 @@ import { describe, expect, it } from "vitest"; -import { createAcpxProfileExtensionAdapter, isHermesCommittedQuestionCompletion, validateAcpxRichEvent } from "./profile-extensions.js"; +import { createAcpxProfileExtensionAdapter, isHermesCommittedHumanInputCompletion, validateAcpxRichEvent } from "./profile-extensions.js"; describe("Hermes native extensions", () => { - const committed = { disposition: "applied", interaction: { id: "question", companyId: "company", issueId: "issue", - sourceRunId: "run", kind: "ask_user_questions", status: "pending", continuationPolicy: "wake_assignee" } }; const completion = { type: "tool_call", tag: "tool_call_update", status: "completed", title: "mcp__paperclip__request_human_input" }; - it("delays only the current run's structured committed question completion", () => { + it.each(["ask_user_questions", "request_confirmation", "request_checkbox_confirmation"])("delays only the current run's structured committed human input completion (%s)", kind => { + const committed = { disposition: "applied", interaction: { id: "input", companyId: "company", issueId: "issue", + sourceRunId: "run", kind, status: "pending", continuationPolicy: "wake_assignee" } }; for (const rawOutput of [committed, JSON.stringify(committed), { result: committed }, { result: JSON.stringify(committed) }, { result: "Saved", structuredContent: committed }]) { - expect(isHermesCommittedQuestionCompletion({ ...completion, rawOutput }, "run")).toBe(true); + expect(isHermesCommittedHumanInputCompletion({ ...completion, rawOutput }, "run")).toBe(true); } for (const event of [{ ...completion, title: "terminal", rawOutput: committed }, { ...completion, status: "failed", rawOutput: committed }, { ...completion, tag: "tool_call", rawOutput: committed }, @@ -17,10 +17,12 @@ describe("Hermes native extensions", () => { { ...completion, rawOutput: { ...committed, disposition: "rejected" } }, ...["status", "kind", "continuationPolicy", "sourceRunId", "id", "companyId", "issueId"].map(key => ({ ...completion, rawOutput: { ...committed, interaction: { ...committed.interaction, [key]: "" } }, - }))]) expect(isHermesCommittedQuestionCompletion(event, "run")).toBe(false); - expect(isHermesCommittedQuestionCompletion({ ...completion, rawOutput: committed }, "other-run")).toBe(false); - expect(isHermesCommittedQuestionCompletion({ ...completion, title: completion.title + ": Choose the color", rawOutput: committed }, "run")).toBe(true); - expect(isHermesCommittedQuestionCompletion({ ...completion, title: completion.title + "_other", rawOutput: committed }, "run")).toBe(false); + }))]) expect(isHermesCommittedHumanInputCompletion(event, "run")).toBe(false); + expect(isHermesCommittedHumanInputCompletion({ ...completion, rawOutput: committed }, "other-run")).toBe(false); + expect(isHermesCommittedHumanInputCompletion({ ...completion, title: completion.title + ": Choose the color", rawOutput: committed }, "run")).toBe(true); + expect(isHermesCommittedHumanInputCompletion({ ...completion, title: completion.title + "_other", rawOutput: committed }, "run")).toBe(false); + expect(isHermesCommittedHumanInputCompletion({ ...completion, rawOutput: { ...committed, + interaction: { ...committed.interaction, kind: "future_interaction" } } }, "run")).toBe(false); }); const adapter = () => createAcpxProfileExtensionAdapter("hermes", { sessionId: "session", turnId: "turn", workspacePath: "/workspace" })!; it("keeps native child identities stable across start, progress and completion", async () => { diff --git a/packages/paperclip-runner/src/drivers/acpx/profile-extensions.ts b/packages/paperclip-runner/src/drivers/acpx/profile-extensions.ts index c9840f79ee..8024c3cfd0 100644 --- a/packages/paperclip-runner/src/drivers/acpx/profile-extensions.ts +++ b/packages/paperclip-runner/src/drivers/acpx/profile-extensions.ts @@ -115,7 +115,7 @@ export function acpxProfileClientCapabilities(agent: QualifiedAcpxAgent): Record /** Delay this tool's display completion until its native prompt receipt is read. * This grants no wait or accounting authority; the controller validates both. */ -export function isHermesCommittedQuestionCompletion(event: AcpRuntimeEventShape, runId: string): boolean { +export function isHermesCommittedHumanInputCompletion(event: AcpRuntimeEventShape, runId: string): boolean { if (event.type !== "tool_call" || event.tag !== "tool_call_update" || event.status !== "completed" || !(event.title === "mcp__paperclip__request_human_input" || event.title?.startsWith("mcp__paperclip__request_human_input: "))) return false; @@ -134,7 +134,9 @@ export function isHermesCommittedQuestionCompletion(event: AcpRuntimeEventShape, } if (!result || "error" in result || result.disposition !== "applied") return false; const interaction = object(result.interaction); - return interaction !== null && interaction.kind === "ask_user_questions" && interaction.status === "pending" + return interaction !== null && typeof interaction.kind === "string" + && ["ask_user_questions", "request_confirmation", "request_checkbox_confirmation"].includes(interaction.kind) + && interaction.status === "pending" && interaction.continuationPolicy === "wake_assignee" && interaction.sourceRunId === runId && ["id", "companyId", "issueId"].every(key => typeof interaction[key] === "string" && Boolean(interaction[key])); } diff --git a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver-impl.ts b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver-impl.ts index e66ea06592..287737a76f 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver-impl.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver-impl.ts @@ -948,7 +948,7 @@ export class CodexAppServerDriver implements HarnessDriver { skillInputs: this.#options.skillInputs, reasoningEffort: this.#options.reasoningEffort, dynamicToolHandler: this.#options.dynamicToolHandler, - deferCommittedQuestionResults: this.#options.deferCommittedQuestionResults, + deferCommittedHumanInputResults: this.#options.deferCommittedHumanInputResults, completionFeedback: this.#options.completionFeedback, }); } diff --git a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.events.test.ts b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.events.test.ts index 8adbb59990..4a00f9eb83 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.events.test.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.events.test.ts @@ -44,11 +44,11 @@ import { } from "./codex-app-server-driver.test-support.js"; describe("Codex app-server Codex driver", () => { - it.each([true, false])("orders committed bridge question results after native usage only for the managed policy (%s)", async defer => { + it.each([true, false].flatMap(defer => ["ask_user_questions", "request_confirmation", "request_checkbox_confirmation"].map(kind => ({ defer, kind }))))("orders committed bridge human input results after native usage only for the managed policy ($kind, $defer)", async ({ defer, kind }) => { const transport = new FakeCodexTransport(); const committed = { disposition: "applied", interaction: { id: "question", companyId: "company", issueId: "issue", - sourceRunId: "run-question", kind: "ask_user_questions", status: "pending", continuationPolicy: "wake_assignee" } }; - const session = await makeDriver([transport], { deferCommittedQuestionResults: defer, + sourceRunId: "run-question", kind, status: "pending", continuationPolicy: "wake_assignee" } }; + const session = await makeDriver([transport], { deferCommittedHumanInputResults: defer, dynamicTools: [{ name: "request_human_input", inputSchema: { type: "object" } }], dynamicToolHandler: async () => committed, }).openSession({ runId: "run-question", normalizedSessionId: "normalized-question", workingDirectory: WORKSPACE }); diff --git a/packages/paperclip-runner/src/drivers/codex/codex-driver-types.ts b/packages/paperclip-runner/src/drivers/codex/codex-driver-types.ts index 42edacd7b8..d4d5ca45bb 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-driver-types.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-driver-types.ts @@ -48,8 +48,8 @@ export interface CodexAppServerDriverOptions { }) => CodexAppServerTransport; /** Additional control-plane tools exposed to the provider for this run. */ dynamicTools?: readonly Readonly>[]; - /** Native Hermes stops at a committed question and sends final usage first. */ - deferCommittedQuestionResults?: boolean; + /** Native Hermes stops at committed human input and sends final usage first. */ + deferCommittedHumanInputResults?: boolean; /** Executes an admitted additional tool call. Completion tools remain driver-owned. */ dynamicToolHandler?: (call: { tool: string; diff --git a/packages/paperclip-runner/src/drivers/codex/codex-harness-session.ts b/packages/paperclip-runner/src/drivers/codex/codex-harness-session.ts index 78251299c6..ae0b5e397f 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-harness-session.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-harness-session.ts @@ -123,7 +123,7 @@ export class CodexHarnessSession } this.runId = input.runId; this.lastAccountingUsageEvent = null; - this.committedQuestionResults.clear(); + this.committedHumanInputResults.clear(); this.result = null; this.resultFingerprint = null; this.resultCallId = null; diff --git a/packages/paperclip-runner/src/drivers/codex/codex-session-server-requests.ts b/packages/paperclip-runner/src/drivers/codex/codex-session-server-requests.ts index d544a9238d..0dc4e28fc3 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-session-server-requests.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-session-server-requests.ts @@ -1,4 +1,4 @@ -import { isAcpxCanonicalInputMethod, isHermesCommittedQuestionCompletion } from "../acpx/profile-extensions.js"; +import { isAcpxCanonicalInputMethod, isHermesCommittedHumanInputCompletion } from "../acpx/profile-extensions.js"; import { isSemanticToolOutcomeUnknownError } from "../../contracts/native-session-backend.js"; import type { HarnessRuntimeRequest, PaperclipQuestionSet } from "../../contracts/harness-driver.js"; import { @@ -142,11 +142,11 @@ async function handleServerRequestBody( result, }, }; - if (state.deferCommittedQuestionResults && tool === "request_human_input" - && isHermesCommittedQuestionCompletion({ type: "tool_call", tag: "tool_call_update", status: "completed", + if (state.deferCommittedHumanInputResults && tool === "request_human_input" + && isHermesCommittedHumanInputCompletion({ type: "tool_call", tag: "tool_call_update", status: "completed", title: "mcp__paperclip__request_human_input", rawOutput: result }, state.runId)) { - if (state.committedQuestionResults.size >= 16) throw new Error("Hermes committed question result limit exceeded"); - state.committedQuestionResults.set(callId, { payload: structuredClone(completed), turnId, itemId: callId }); + if (state.committedHumanInputResults.size >= 16) throw new Error("Hermes committed human input result limit exceeded"); + state.committedHumanInputResults.set(callId, { payload: structuredClone(completed), turnId, itemId: callId }); } else { state.emit("item.completed", completed, { turnId, itemId: callId }); } diff --git a/packages/paperclip-runner/src/drivers/codex/codex-session-state.ts b/packages/paperclip-runner/src/drivers/codex/codex-session-state.ts index 7e2ab8d3ea..d38dd5b031 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-session-state.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-session-state.ts @@ -105,8 +105,8 @@ export class CodexSessionState { readonly dynamicTools: readonly Readonly>[]; readonly completionFeedback: CodexAppServerDriverOptions["completionFeedback"]; readonly dynamicToolHandler: CodexAppServerDriverOptions["dynamicToolHandler"]; - readonly deferCommittedQuestionResults: boolean; - readonly committedQuestionResults = new Map; turnId: string; itemId: string }>(); + readonly deferCommittedHumanInputResults: boolean; + readonly committedHumanInputResults = new Map; turnId: string; itemId: string }>(); readonly eventQueue = new AsyncQueue(); sourceSequence: number; activeTurnId: string | null; @@ -182,7 +182,7 @@ export class CodexSessionState { dynamicTools: readonly Readonly>[]; completionFeedback?: CodexAppServerDriverOptions["completionFeedback"]; dynamicToolHandler?: CodexAppServerDriverOptions["dynamicToolHandler"]; - deferCommittedQuestionResults?: boolean; + deferCommittedHumanInputResults?: boolean; }) { this.codexUsageBaseline = input.codexUsageBaseline ?? null; if (this.codexUsageBaseline) this.usageSnapshot = codexRunUsage(this.codexUsageBaseline); @@ -207,7 +207,7 @@ export class CodexSessionState { this.reasoningEffort = input.reasoningEffort; this.dynamicTools = input.dynamicTools; this.dynamicToolHandler = input.dynamicToolHandler; - this.deferCommittedQuestionResults = input.deferCommittedQuestionResults === true; + this.deferCommittedHumanInputResults = input.deferCommittedHumanInputResults === true; this.completionFeedback = input.completionFeedback; this.currentGoal = input.goal === undefined ? null : structuredClone(input.goal); for (const entry of input.lineage ?? [input.opened.lineage]) { @@ -443,9 +443,9 @@ export class CodexSessionState { // Native terminal notification follows the final prompt receipt. Release // the bridge's own completed tool facts here, before publishing terminal, // so controller parking cannot interrupt delivery of that receipt. - for (const [id, completed] of this.committedQuestionResults) { + for (const [id, completed] of this.committedHumanInputResults) { if (completed.turnId !== refs.turnId) continue; - this.committedQuestionResults.delete(id); + this.committedHumanInputResults.delete(id); this.emit("item.completed", completed.payload, { turnId: completed.turnId, itemId: completed.itemId }); } } diff --git a/packages/paperclip-runner/src/providers/hermes/bridge.py b/packages/paperclip-runner/src/providers/hermes/bridge.py index 82f04c7a4e..8e8d9b662c 100644 --- a/packages/paperclip-runner/src/providers/hermes/bridge.py +++ b/packages/paperclip-runner/src/providers/hermes/bridge.py @@ -58,7 +58,7 @@ def committed_human_input_wait(name, result): return False interaction = result.get("interaction") return (result.get("disposition") == "applied" and isinstance(interaction, dict) - and interaction.get("kind") == "ask_user_questions" + and interaction.get("kind") in ("ask_user_questions", "request_confirmation", "request_checkbox_confirmation") and interaction.get("status") == "pending" and interaction.get("continuationPolicy") == "wake_assignee" and all(isinstance(interaction.get(key), str) and bool(interaction[key]) diff --git a/packages/paperclip-runner/src/providers/hermes/test_bridge.py b/packages/paperclip-runner/src/providers/hermes/test_bridge.py index 2ee3507f3e..e3e1533299 100644 --- a/packages/paperclip-runner/src/providers/hermes/test_bridge.py +++ b/packages/paperclip-runner/src/providers/hermes/test_bridge.py @@ -268,8 +268,12 @@ class Controls(unittest.IsolatedAsyncioTestCase): "status": "pending", "continuationPolicy": "wake_assignee"}} name = "mcp__paperclip__request_human_input" updates = [] - for carrier in [result, {"result": result}, {"result": json.dumps(result)}, - {"result": "Question saved", "structuredContent": result}]: + carriers = [] + for kind in ("ask_user_questions", "request_confirmation", "request_checkbox_confirmation"): + committed = {**result, "interaction": {**result["interaction"], "kind": kind}} + carriers.extend([committed, {"result": committed}, {"result": json.dumps(committed)}, + {"result": "Human input saved", "structuredContent": committed}]) + for carrier in carriers: with self.subTest(carrier=carrier): self.state.cancel_event.clear() self.bridge._committed_wait_updates.clear() @@ -313,7 +317,7 @@ class Controls(unittest.IsolatedAsyncioTestCase): ("mcp__paperclip__request_human_input", {"result": "Saved a pending question"}), ("mcp__paperclip__request_human_input", {"result": original, "error": "denied"}), ("mcp__paperclip__request_human_input", {**original, "disposition": "rejected"})] - for key, value in [("status", "resolved"), ("kind", "request_confirmation"), + for key, value in [("status", "resolved"), ("kind", "future_interaction"), ("kind", []), ("continuationPolicy", "none"), ("sourceRunId", None), ("id", "")]: cases.append(("mcp__paperclip__request_human_input", {**original, "interaction": {**original["interaction"], key: value}}))