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; }