From fe3264fb494ef7d896386fc5c6e2447ca9419be6 Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 26 Aug 2026 20:02:08 +0000 Subject: [PATCH] fix(runner): record the resumed semantic result Require the control plane's canonical result receipt and emit it once after durable completion. The live qualifier now reconciles semantic input, result, provider usage, and the final control-plane effect. Co-Authored-By: Paperclip --- .../runner-core/src/provider_backend.rs | 31 ++++++++++++++++--- .../runner-core/tests/codex_provider.rs | 29 ++++++++++++++++- .../native-codex-runner.integration.test.ts | 6 +++- 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/provider_backend.rs b/packages/paperclip-runner/runner/crates/runner-core/src/provider_backend.rs index c2bae47aff0..a4611ac8205 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/provider_backend.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/provider_backend.rs @@ -807,6 +807,22 @@ impl CodexCommandExecutor { .ok_or_else(|| { DurableRunnerError::invalid("semantic_tool.result has no pending call") })?; + let result_receipt = result + .get("resultReceipt") + .filter(|receipt| { + receipt.get("schema").and_then(Value::as_str) + == Some("paperclip.prp.semantic_tool.v1") + && receipt.get("phase").and_then(Value::as_str) == Some("result") + && receipt.get("callId").and_then(Value::as_str) == Some(call_id) + && receipt.get("operationId").and_then(Value::as_str) + == Some(pending.operation_id.as_str()) + }) + .cloned() + .ok_or_else(|| { + DurableRunnerError::invalid( + "semantic_tool.result requires its matching canonical resultReceipt", + ) + })?; let state = self .state .as_mut() @@ -838,10 +854,17 @@ impl CodexCommandExecutor { } else { false }; - Ok(CommandExecution::result(json!({ - "status": if delivered { "delivered" } else { "deferred" }, - "callId": call_id, - }))) + Ok(CommandExecution { + result: json!({ + "status": if delivered { "delivered" } else { "deferred" }, + "callId": call_id, + }), + events: vec![( + "semantic_tool.result".to_owned(), + EventPriority::P0, + json!({"semantic_tool": result_receipt}), + )], + }) } fn close_session(&mut self) -> Result { diff --git a/packages/paperclip-runner/runner/crates/runner-core/tests/codex_provider.rs b/packages/paperclip-runner/runner/crates/runner-core/tests/codex_provider.rs index 60d787844ca..a09c1b2b3d6 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/tests/codex_provider.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/tests/codex_provider.rs @@ -254,7 +254,34 @@ fn durable_backend_resumes_one_pending_native_tool_call_with_the_same_identity() "semantic_tool.result", json!({ "callId": "semantic-call-1", - "result": {"ok": true, "value": {"task": "NCV-1"}}, + "result": { + "ok": true, + "value": {"task": "NCV-1"}, + "resultReceipt": { + "schema": "paperclip.prp.semantic_tool.v1", + "schemaVersion": 1, + "phase": "result", + "operationId": "report_progress", + "callId": "semantic-call-1", + "correlation": { + "runId": "run-1", + "normalizedSessionId": "session-1", + "turnId": "turn-1", + "itemId": "item-1" + }, + "idempotencyKey": "native-resume-proof-1", + "content": { + "digest": format!("sha256:{}", "a".repeat(64)), + "redactionDisposition": "digest_only", + "references": [] + }, + "outcome": "succeeded", + "code": "ok", + "retryable": false, + "authorizationBoundary": "active_task", + "operationReceiptId": "receipt-1" + } + }, "isError": false, }), )) diff --git a/server/src/services/native-runtime/native-codex-runner.integration.test.ts b/server/src/services/native-runtime/native-codex-runner.integration.test.ts index 6119139e30e..878d62c8dc0 100644 --- a/server/src/services/native-runtime/native-codex-runner.integration.test.ts +++ b/server/src/services/native-runtime/native-codex-runner.integration.test.ts @@ -334,6 +334,10 @@ describeEmbeddedPostgres("native Codex server vertical slice", () => { ]); if (liveCodex) { + const usageReports = nativeEvents + .filter((event) => event.eventType === "usage.reported") + .map((event) => event.payload?.prpEvent) + .filter((event): event is Record => Boolean(event)); console.log("NATIVE_CODEX_RESUME_PROOF", JSON.stringify({ appCommit: process.env.PAPERCLIP_LIVE_CODEX_APP_COMMIT ?? null, runId, @@ -349,7 +353,7 @@ describeEmbeddedPostgres("native Codex server vertical slice", () => { (event) => event.eventType === "semantic_tool.result", ).length, controlPlaneEffectCount: progressEffects.length, - usage: result.usage ?? null, + usageReports, })); return; }