diff --git a/.dockerignore b/.dockerignore index 97575dc3cc4..df7fc23d6b6 100644 --- a/.dockerignore +++ b/.dockerignore @@ -47,7 +47,7 @@ packages/paperclip-runner/test/fixtures/** packages/paperclip-runner/test-fixtures/** !packages/paperclip-runner/test-fixtures/pi-acp packages/paperclip-runner/test-fixtures/pi-acp/** -!packages/paperclip-runner/test-fixtures/pi-acp/profile-v17-identity.json +!packages/paperclip-runner/test-fixtures/pi-acp/profile-v18-identity.json !packages/paperclip-runner/docs/capability-contract.md # check:runner-workflow-traceability access()es every regression test the # stress-traceability spec names — those are src/**/*.test.ts files, so diff --git a/doc/plans/2026-10-02-pi-production-readiness.md b/doc/plans/2026-10-02-pi-production-readiness.md index 581dd8cd7de..e57f3f27ac2 100644 --- a/doc/plans/2026-10-02-pi-production-readiness.md +++ b/doc/plans/2026-10-02-pi-production-readiness.md @@ -7,56 +7,46 @@ widgets, images, Cursor or Copilot qualification. The existing draft stack must ## Production qualification state — 2026-10-07 -The shipping runtime is frozen at `15af2eefbc2dc9f270875836e9bb404f4ed3d135` -with Pi profile 17. The final target is native Linux. All 26 accepted Product -cells and seven Runner cases require passes on the final installed artifacts -and recorded harness definitions. Mac coverage is deferred. Historical passes -do not transfer. The caller chooses the production model; the accepted model -and low thinking remain fixture inputs. +The candidate now uses Pi profile 18. Its production cancellation fix requires a +new frozen source revision, normal Linux packages, cloud image and all 33 accepted +live cases. Historical profile-17 passes do not transfer. Mac qualification stays +deferred. The caller chooses the production model; the accepted model and low +thinking remain fixture inputs. -Both credential boundaries reserve the same 40 loader, shell and process-control -names while permitting custom names such as `LD_API_KEY` and `DYLD_API_KEY`. -Shared negative inputs and actual child-process tests cover explicit bound-key -forwarding and unrelated-secret exclusion. Current generated release profiles -also decode automatically, with separate exact launch admission. Pi's native -1.0.0 distributions, wrapper, extension and closure hashes remain unchanged. +The corrected Stop fixture reached the real pending native-write permission and +received the board Stop acknowledgement. Pi then returned `cancelled` alongside +service-failure metadata, so ACPX reported a failed turn. The original failed +attempt, unchanged strict assertions, no-write observations and runtime-retirement +evidence remain retained. The Pi wrapper now omits service-failure metadata only +for its authoritative cancelled terminal, preserving partial usage and ordinary +provider failures. ACPX and server error handling remain unchanged. + +Profile 18 binds the corrected wrapper/helper and regenerated closure declarations. +Upstream pinned pi-acp patch application reproduces both declared file hashes. +The same 40 reserved credential names, explicit key forwarding, arbitrary caller +model support, native Pi 1.0.0 distributions and Node pins remain unchanged. | Release gate | Current evidence | | --- | --- | -| Normal native Linux build | Workspace and standalone public builds pass at the frozen runtime. | -| Normal public installation | CLI/server and separate Daytona plugin npm installs and normal hooks pass; lockfiles stay unchanged. | -| Installed package integrity and Pi admission | Full archive graph and managed-asset verification, normal Pi runtime setup, credential-free public admission and actual server/UI startup pass. Installed daemon SHA-256 is `905949ca03c1859974ce9868aea6262a5338f98dace66bd029a57e11272996be`. | -| Free runtime checks | Full release Rust: 689 pass, two ignored. Runner TypeScript typecheck passes; provider/configuration/compatibility checks: 58 pass, one skip. Actual server native-input boundary: 68 pass. | -| Final Product acceptance | 1/26. Corrected definition-4 file-edit passes on the frozen installation and harness `92cf55a7a87903b6bd8603f4d7cac6cc510418c0`, with independent process/scratch/credential-input cleanup and retained canonical evidence. Preserve the failed definition-3 attempt. The next Stop attempt failed because its title-based lookup missed the composer-generated title before Stop could run; its original timeout and cleanup failure stay retained. Definitions 9/16 now bind explicit creation-response IDs, with 176 free checks passing. Resume only on the corrected harness, with no automatic retries. | -| Final Runner acceptance | 0/7. Definitions `a29b289cb3a80731c6f8195159d263e5336b4d4a` validate seven cases and pass 139 free tests; final live runs remain pending. | -| Final Daytona image | The user-approved hosted Linux workflow is applied. The first cloud build exposed two ignored release-declaration inputs; the exact build-context correction and 20 focused checks pass. [Corrected cloud build](https://github.com/paperclipai/paperclip/actions/runs/37653872385) passed. Anonymous OCI admission verifies Linux amd64 and source `f4bb181daeaa201c2528f774182cb5a25edfc4c6`; immutable image digest is `sha256:622efa468df3218a77c1302189f20a629fd45a8c816dca3f00e1ec6eed4b3a64`. Guest admission and 13 Daytona cells remain pending. | -| Current PR-head CI and review | Harness head `92cf55a7a87903b6bd8603f4d7cac6cc510418c0` passes CI and review. Cloud build head has 52 passing checks and one Rust-host shutdown during compilation. No Rust test failed; GitHub returned HTTP 500 for the failed-job rerun. Current fixture changes require fresh checks; no unresolved review threads were observed. | +| Offline cancellation proof | New unit and actual subprocess regressions fail on profile-17 bytes and pass on the corrected wrapper. All 49 actual Pi ACP protocol tests pass; 99 focused Runner tests pass, one skipped. | +| Free harness checks | All 2,480 Product E2E unit tests pass; Runner TypeScript typecheck and generated TypeScript/Rust profile parity pass. The broad Runner suite records 3,379 passes and three failures: two timeouts pass on isolated recheck; the warm Pi case passes against the current rebuilt daemon. The first Rust run exposed a stale fixture in the external test checkout; current-source verification is pending. | +| Normal native Linux build and public installation | Historical profile-17 workspace/public builds, normal npm lifecycle hooks, strict installed graph, Pi setup and actual server/UI admission pass. New profile-18 artifacts require rebuilding and full admission. | +| Final Product acceptance | 0/26 on profile 18. Historical profile 17 has one file-edit pass and two retained Stop failures: creation-title lookup, then the production cancellation defect. Resume with one changed Stop attempt after fresh artifact admission; no automatic retries. | +| Final Runner acceptance | 0/7 on profile 18. The seven cases and their assertions stay frozen; roster identity must match the new production profile and source. | +| Cloud image | [Hosted Linux build](https://github.com/paperclipai/paperclip/actions/runs/37653872385) passed for source `f4bb181daeaa201c2528f774182cb5a25edfc4c6`, image `sha256:622efa468df3218a77c1302189f20a629fd45a8c816dca3f00e1ec6eed4b3a64`. Anonymous OCI and actual credential-free Daytona guest admission pass; owned probe deletion is confirmed. Profile 18 requires a new cloud build and guest admission. No local Docker is used. | +| Current PR-head CI and review | New production source requires fresh latest-head checks and review; previous CI results remain historical. No merge, release or rollout is authorized. | -The first final-source file-edit run edited and independently validated the file, -registered its real download and completed the task. Its Pi prompt requested -artifact title `Verified text`, while the unchanged shared registration check -requires the exact filename as title. Extended definition 4 corrects that prompt -instruction and adds a cross-fixture regression check. Native edit, exact bytes, -command/result, registration, run attribution and cleanup grading stay unchanged. -Pi controls definition 9 and native definition 16 also preserve task identity from -the public creation response and use explicit titles for strict native operations. -The original Stop title-lookup failure remains failed; it never reached Stop. -No Stop, permission, native-byte, process-retirement or cleanup assertion is relaxed. +The accepted Product fixtures preserve explicit task titles and creation-response +issue IDs. Native edit, exact bytes, command/result, registration, run attribution, +Stop, permission, process retirement and cleanup assertions stay unchanged. +All original failures retain their canonical results and provenance. Billing +snapshots remain provisional when provider usage has not settled. -The failed definition-3 attempt does not become a pass. Its recorded usage is -39,343 input, 77,312 cached input and 1,359 output tokens; provider cost is unpriced -and key snapshots may settle later. No unchanged paid retry is permitted. - -Earlier installs exhausted the owned host's 10 GiB scratch disk. The final -installation retires its completed npm cache only after strict base archive -verification, then checks every installed file plus normal managed Pi assets. -A nested/top-level npm bin classification error in the external verifier was -corrected on the same installed bytes; the original failed audit is retained. -No grading rule or production artifact was changed for that correction. -Local full database integration remains blocked by host System V shared-memory -capacity. Preserve those observations and require current Linux CI. No merge, -release or rollout is authorized. The dedicated key retains its $5 lifetime cap -inside the approved $100 total token ceiling, with no BYOK or automatic retries. +The previous public installation retired only its completed owned pnpm content +cache after strict base archive verification. Local database integration remains +blocked by System V shared-memory capacity; require current Linux CI. The dedicated +OpenRouter key keeps its $5 lifetime cap inside the approved $100 total token +ceiling, with no reset, BYOK, fallback or unchanged paid retry. ## Final fixture reconciliation — 2026-10-07 diff --git a/packages/paperclip-runner/acpx-profiles.json b/packages/paperclip-runner/acpx-profiles.json index 2f6287283cc..daab3f8ffac 100644 --- a/packages/paperclip-runner/acpx-profiles.json +++ b/packages/paperclip-runner/acpx-profiles.json @@ -14,12 +14,12 @@ "requiresProviderPolicy": false }, "pi": { - "agentProfileVersion": 17, + "agentProfileVersion": 18, "agentServerPackage": "pi-acp", "agentServerVersion": "0.0.33", "agentRuntimePackage": "@earendil-works/pi-coding-agent", "agentRuntimeVersion": "1.0.0", - "commandDigest": "sha256:a1d976c437cb736c9cce8ebe8b8baf59e571761a8d00e8b1885e72dd906d8f21", + "commandDigest": "sha256:9d3e7d8269f1a0af94616552dfc69671932688b81dbbd5bab9ef356b3a9cbccb", "requiresProviderPolicy": true }, "cursor": { diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs index ef4991c2fc0..f6fa38cc0f3 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs @@ -3184,7 +3184,7 @@ mod tests { #[test] fn pi_profile_matches_published_identity_and_rejects_prior_profiles() { let published: Value = serde_json::from_str(include_str!( - "../../../../test-fixtures/pi-acp/profile-v17-identity.json" + "../../../../test-fixtures/pi-acp/profile-v18-identity.json" )) .unwrap(); let mut value = descriptor("codex"); @@ -3203,6 +3203,7 @@ mod tests { for prior in [ include_str!("../../../../test-fixtures/pi-acp/profile-v15-identity.json"), include_str!("../../../../test-fixtures/pi-acp/profile-v16-identity.json"), + include_str!("../../../../test-fixtures/pi-acp/profile-v17-identity.json"), ] { let prior: Value = serde_json::from_str(prior).unwrap(); value["commandDigest"] = prior["commandDigest"].clone(); diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/generated_acpx_profiles.rs b/packages/paperclip-runner/runner/crates/runner-core/src/generated_acpx_profiles.rs index a04489675b6..12c2de52c0e 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/generated_acpx_profiles.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/generated_acpx_profiles.rs @@ -29,7 +29,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option { agent_runtime_package: Some("@earendil-works/pi-coding-agent"), agent_runtime_version: Some("1.0.0"), command_digest: - "sha256:a1d976c437cb736c9cce8ebe8b8baf59e571761a8d00e8b1885e72dd906d8f21", + "sha256:9d3e7d8269f1a0af94616552dfc69671932688b81dbbd5bab9ef356b3a9cbccb", requires_provider_policy: true, }, "cursor" => AcpxReleaseProfile { diff --git a/packages/paperclip-runner/runner/crates/runner-core/tests/native_provider_backend.rs b/packages/paperclip-runner/runner/crates/runner-core/tests/native_provider_backend.rs index 6a5c8a8ef50..7f89276380b 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/tests/native_provider_backend.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/tests/native_provider_backend.rs @@ -17,7 +17,7 @@ use sha2::{Digest, Sha256}; const CODEX_ACPX_DIGEST: &str = "sha256:c4538599d1ab767db5dff50934f13bb5ba313a59d9c4a83e993fac4617ea63d3"; const PI_ACPX_DIGEST: &str = - "sha256:a1d976c437cb736c9cce8ebe8b8baf59e571761a8d00e8b1885e72dd906d8f21"; + "sha256:9d3e7d8269f1a0af94616552dfc69671932688b81dbbd5bab9ef356b3a9cbccb"; fn temporary_directory(label: &str) -> PathBuf { let nonce = SystemTime::now() diff --git a/packages/paperclip-runner/scripts/generate-acpx-profiles.mjs b/packages/paperclip-runner/scripts/generate-acpx-profiles.mjs index 2bdaf1d006a..1130a61c394 100644 --- a/packages/paperclip-runner/scripts/generate-acpx-profiles.mjs +++ b/packages/paperclip-runner/scripts/generate-acpx-profiles.mjs @@ -39,7 +39,7 @@ const { commandDigest, ...attestation } = contract; assert.equal(commandDigest, `sha256:${createHash("sha256").update(canonicalJson(attestation)).digest("hex")}`); assert.equal(contract.acpxPatchSha256, createHash("sha256") .update(await readFile(new URL("../../patches/acpx@0.13.1.patch", root))).digest("hex")); -for (const [agent, path] of [["pi", "test-fixtures/pi-acp/profile-v17-identity.json"], ["copilot", "test/fixtures/copilot-profile-v16-identity.json"]]) { +for (const [agent, path] of [["pi", "test-fixtures/pi-acp/profile-v18-identity.json"], ["copilot", "test/fixtures/copilot-profile-v16-identity.json"]]) { const identity = await readJson(path); assert.equal(manifest.profiles[agent].agentProfileVersion, identity.declaration.agentProfileVersion); assert.equal(manifest.profiles[agent].commandDigest, identity.commandDigest); diff --git a/packages/paperclip-runner/scripts/materialize-pi-distribution.mjs b/packages/paperclip-runner/scripts/materialize-pi-distribution.mjs index 86ac9f94abd..cfcb25feb1a 100644 --- a/packages/paperclip-runner/scripts/materialize-pi-distribution.mjs +++ b/packages/paperclip-runner/scripts/materialize-pi-distribution.mjs @@ -19,8 +19,8 @@ const patchPath = join(workspaceRoot, "patches/pi-acp@0.0.33.patch"); const supportedTargets = new Set(["darwin-arm64", "darwin-x64", "linux-x64"]); export const PI_DISTRIBUTION_PINS = Object.freeze({ wrapper: "0.0.33", runtime: "1.0.0", sdk: "0.26.0", zod: "3.25.76", nodeVersion: PI_NODE_VERSION, undici: "8.10.2", nodeBundledUndici: "7.29.1", - wrapperSha256: "8ef777e9e6a3cef37775bab2b800e4931330c23a0b151ce07fdc5b943f28435d", - helperSha256: "2191b1e5f281d24508ec7eaff39011ee863a8a70bf54f4aa0490ad0172b323da", + wrapperSha256: "9d129b3d38772e93e97080aa6c4e574ac5df4e47ce5bc331a484a9fbf8188b36", + helperSha256: "41e0490b617da0d60e0c8ec58ef311236f945129d99f816cff6897f78da7f91d", }); const hash = (bytes) => createHash("sha256").update(bytes).digest("hex"); diff --git a/packages/paperclip-runner/src/drivers/acpx/generated-profiles.ts b/packages/paperclip-runner/src/drivers/acpx/generated-profiles.ts index 1fa5f162a4d..7dad9a72662 100644 --- a/packages/paperclip-runner/src/drivers/acpx/generated-profiles.ts +++ b/packages/paperclip-runner/src/drivers/acpx/generated-profiles.ts @@ -22,12 +22,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = { "protocolVersion": 1, "acpxVersion": "0.13.1", "agent": "pi", - "agentProfileVersion": 17, + "agentProfileVersion": 18, "agentServerPackage": "pi-acp", "agentServerVersion": "0.0.33", "agentRuntimePackage": "@earendil-works/pi-coding-agent", "agentRuntimeVersion": "1.0.0", - "commandDigest": "sha256:a1d976c437cb736c9cce8ebe8b8baf59e571761a8d00e8b1885e72dd906d8f21", + "commandDigest": "sha256:9d3e7d8269f1a0af94616552dfc69671932688b81dbbd5bab9ef356b3a9cbccb", "permissionPolicy": "interactive" }, "cursor": { diff --git a/packages/paperclip-runner/src/drivers/acpx/pi-acp-runtime.test.ts b/packages/paperclip-runner/src/drivers/acpx/pi-acp-runtime.test.ts index 756774b33e6..92610780975 100644 --- a/packages/paperclip-runner/src/drivers/acpx/pi-acp-runtime.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/pi-acp-runtime.test.ts @@ -259,6 +259,19 @@ describe("Pi ACP bridge", () => { } }); }); + it("preserves usage without reporting an acknowledged cancellation as a service failure", () => { + const usage = new PiTurnUsage(); + usage.accept({ role: "assistant", timestamp: 1, stopReason: "error", usage: { input: 4, output: 2 } }); + expect(usage.response("cancelled")).toEqual({ usage: { + inputTokens: 4, outputTokens: 2, _meta: { paperclipPi: { provenance: "assistant_message_receipts" } }, + } }); + // Ordinary settlement keeps the provider failure and partial accounting. + expect(usage.response("end_turn")).toMatchObject({ + usage: { inputTokens: 4, outputTokens: 2 }, + _meta: { jetbrains: { air: { sessionFailure: { severity: "error" } } } }, + }); + }); + it("accounts compaction receipts once and does not fabricate partial coverage", () => { const usage = new PiTurnUsage(); usage.accept({ role: "assistant", timestamp: 1, stopReason: "error", usage: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0, cost: { total: 0.01 } } }); diff --git a/packages/paperclip-runner/src/drivers/acpx/pi-acp-runtime.ts b/packages/paperclip-runner/src/drivers/acpx/pi-acp-runtime.ts index e05311a41b8..bb2049ff7e7 100644 --- a/packages/paperclip-runner/src/drivers/acpx/pi-acp-runtime.ts +++ b/packages/paperclip-runner/src/drivers/acpx/pi-acp-runtime.ts @@ -568,10 +568,10 @@ export class PiTurnUsage { this.failed = previousFailure; } - response(): RecordValue { + response(stopReason?: "end_turn" | "cancelled"): RecordValue { return { ...(this.observed ? { usage: { ...Object.fromEntries(Object.entries(this.total).filter(([name]) => !this.unknown.has(name as keyof typeof this.total))), _meta: { paperclipPi: { provenance: this.compactionObserved ? "assistant_message_and_compaction_receipts" : "assistant_message_receipts", ...(this.costObserved && !this.costIncomplete ? { costUsd: this.cost, costSource: "pi_pricing_estimate" } : {}) } } } } : {}), - ...(this.failed ? { _meta: { jetbrains: { air: { version: 1, sessionFailure: { severity: "error", category: "service", title: "Pi provider request failed" } } } } } : {}), + ...(this.failed && stopReason !== "cancelled" ? { _meta: { jetbrains: { air: { version: 1, sessionFailure: { severity: "error", category: "service", title: "Pi provider request failed" } } } } } : {}), }; } } diff --git a/packages/paperclip-runner/src/drivers/acpx/pi-closure-pins.ts b/packages/paperclip-runner/src/drivers/acpx/pi-closure-pins.ts index 5bddab5a10c..bd5a9010901 100644 --- a/packages/paperclip-runner/src/drivers/acpx/pi-closure-pins.ts +++ b/packages/paperclip-runner/src/drivers/acpx/pi-closure-pins.ts @@ -7,7 +7,7 @@ * three pins. Never accept a digest supplied only by an installed manifest. */ export const PI_DISTRIBUTION_CLOSURE_SHA256 = Object.freeze({ - "darwin-arm64": "729947513854dadc2d596a34186f7f19beaffead0fc97698d8eeac96b8479533", - "darwin-x64": "80de2913634f83e958792ddebc17e94f6dbbe5aa9aed3cea3ff5b40f44c90a59", - "linux-x64": "5fedea5f04f4f76d0e2ead0637b00322583d6c859f9df4f4dc8b332aca416eda", + "darwin-arm64": "8b85caad950fc720eabd80d7b67146b5b2106c66aecf1797993b90a3568ddc7f", + "darwin-x64": "c66bfff68705627b3c5589e038080b284a04027a0a0a3371a54a657a80787789", + "linux-x64": "a1b2797c5ca8245b441b0cc0e18f09463d9d896bd52631874320d77a09a4736d", }); diff --git a/packages/paperclip-runner/src/drivers/acpx/pi-installation.test.ts b/packages/paperclip-runner/src/drivers/acpx/pi-installation.test.ts index 5fbe331af56..5747c5b442c 100644 --- a/packages/paperclip-runner/src/drivers/acpx/pi-installation.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/pi-installation.test.ts @@ -48,11 +48,19 @@ describe("Pi installation factory", () => { expect(current.commandDigest).not.toBe(prior.commandDigest); }); + it("preserves profile 17 and binds acknowledged cancellation in profile 18", async () => { + const prior = JSON.parse(await readFile(new URL("../../../test-fixtures/pi-acp/profile-v17-identity.json", import.meta.url), "utf8")); + const current = JSON.parse(await readFile(new URL("../../../test-fixtures/pi-acp/profile-v18-identity.json", import.meta.url), "utf8")); + expect(Object.keys(current.declaration).filter(key => JSON.stringify(current.declaration[key]) !== JSON.stringify(prior.declaration[key])).sort()) + .toEqual(["agentProfileVersion", "closure", "helperSha256", "nativeCancellation", "wrapperSha256"]); + expect(current.commandDigest).not.toBe(prior.commandDigest); + }); + it("binds the profile declaration to the reviewed patch and platform closure pins", async () => { const hash = (bytes: string | Uint8Array) => createHash("sha256").update(bytes).digest("hex"); const canonical = (value: any): string => Array.isArray(value) ? `[${value.map(canonical).join(",")}]` : value && typeof value === "object" ? `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}` : JSON.stringify(value); - const identity = JSON.parse(await readFile(new URL("../../../test-fixtures/pi-acp/profile-v17-identity.json", import.meta.url), "utf8")); + const identity = JSON.parse(await readFile(new URL("../../../test-fixtures/pi-acp/profile-v18-identity.json", import.meta.url), "utf8")); const declaration = identity.declaration; expect(declaration.sharedRuntimeContract).toBe("paperclip.acpx-runtime-contract.v1"); expect(identity.commandDigest).toBe(`sha256:${hash(canonical(declaration))}`); @@ -77,25 +85,26 @@ describe("Pi installation factory", () => { expect(materializer).toContain(`helperSha256: "${declaration.helperSha256}"`); // This exact patch produced the reviewed current wrapper/closure. // Pin it separately so wrapper-only edits cannot keep an unchanged declaration. - expect(hash(await readFile(new URL("../../../../../patches/pi-acp@0.0.33.patch", import.meta.url)))).toBe("8b3dbb7e08c6c356e81624b9afcab13827269579cef085db15fdec0ef72aedb3"); + expect(hash(await readFile(new URL("../../../../../patches/pi-acp@0.0.33.patch", import.meta.url)))).toBe("a7c63900b513490cceed9e41bc3bd06f03f2cd56265f434aa2c5dc7925fa76b4"); }); it("rejects legacy profiles and tampered executable identities", () => { expect(() => assertPiInstallationProfile(candidate())).not.toThrow(); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 1 })).toThrow("version 17"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 2 })).toThrow("version 17"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 3 })).toThrow("version 17"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 4 })).toThrow("version 17"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 5 })).toThrow("version 17"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 6 })).toThrow("version 17"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 7 })).toThrow("version 17"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 8 })).toThrow("version 17"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 9 })).toThrow("version 17"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 10 })).toThrow("version 17"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 11 })).toThrow("version 17"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 12 })).toThrow("version 17"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 13 })).toThrow("version 17"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 14 })).toThrow("version 17"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 1 })).toThrow("version 18"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 2 })).toThrow("version 18"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 3 })).toThrow("version 18"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 4 })).toThrow("version 18"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 5 })).toThrow("version 18"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 6 })).toThrow("version 18"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 7 })).toThrow("version 18"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 8 })).toThrow("version 18"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 9 })).toThrow("version 18"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 10 })).toThrow("version 18"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 11 })).toThrow("version 18"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 12 })).toThrow("version 18"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 13 })).toThrow("version 18"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 14 })).toThrow("version 18"); + for (const priorVersion of [15, 16, 17]) expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: priorVersion })).toThrow("version 18"); for (const changed of [{ agentServerVersion: "latest" }, { commandDigest: `sha256:${"0".repeat(64)}` }, { agentRuntimePackage: "ambient-pi" }]) { expect(() => assertPiInstallationProfile({ ...candidate(), ...changed })).toThrow("trusted declaration"); } diff --git a/packages/paperclip-runner/test-fixtures/pi-acp/fake-pi.mjs b/packages/paperclip-runner/test-fixtures/pi-acp/fake-pi.mjs index 68db559d783..7e3209eae4c 100644 --- a/packages/paperclip-runner/test-fixtures/pi-acp/fake-pi.mjs +++ b/packages/paperclip-runner/test-fixtures/pi-acp/fake-pi.mjs @@ -96,7 +96,7 @@ createInterface({ input: process.stdin }).on("line", (line) => { if (request.type === "steer") { response(inputDisposition); if (inputDisposition.disposition !== "queued") return; output({ type: "message_update", assistantMessageEvent: { type: "text_delta", delta: `steered:${request.message}` } }); return; } if (request.type === "follow_up") { response(inputDisposition); if (inputDisposition.disposition !== "queued") return; finish(`follow-up:${request.message}`); return; } if (request.type === "compact") { output({ type: "compaction_start", reason: "manual" }); const result = compaction(); output({ type: "compaction_end", reason: "manual", result, aborted: false, willRetry: false }); response(result); return; } - if (request.type === "abort") { response(); if (active) { active = false; if (assistantActive) endMessage("aborted", {}); output({ type: "turn_end" }); output({ type: "agent_settled" }); } return; } + if (request.type === "abort") { response(); if (active) { active = false; if (assistantActive) endMessage(process.env.PI_FIXTURE_CANCEL_ERROR === "1" ? "error" : "aborted", process.env.PI_FIXTURE_CANCEL_ERROR === "1" ? { input: 11, output: 3 } : {}); output({ type: "turn_end" }); output({ type: "agent_settled" }); } return; } if (request.type === "prompt") { response({ disposition: "started" }); active = true; output({ type: "agent_start" }); if (request.message === "missing-message-start") { modelIteration++; output({ type: "turn_start" }); output({ type: "message_update", assistantMessageEvent: { type: "text_delta", delta: "invalid" } }); return; } diff --git a/packages/paperclip-runner/test-fixtures/pi-acp/profile-v18-identity.json b/packages/paperclip-runner/test-fixtures/pi-acp/profile-v18-identity.json new file mode 100644 index 00000000000..aced3bc0660 --- /dev/null +++ b/packages/paperclip-runner/test-fixtures/pi-acp/profile-v18-identity.json @@ -0,0 +1,49 @@ +{ + "commandDigest": "sha256:9d3e7d8269f1a0af94616552dfc69671932688b81dbbd5bab9ef356b3a9cbccb", + "declaration": { + "schema": "paperclip.acpx_profile_declaration.v1", + "agent": "pi", + "agentProfileVersion": 18, + "acpxVersion": "0.13.1", + "agentServerVersion": "0.0.33", + "agentRuntimeVersion": "1.0.0", + "nodeVersion": "24.21.0", + "closure": { + "darwin-arm64": "8b85caad950fc720eabd80d7b67146b5b2106c66aecf1797993b90a3568ddc7f", + "darwin-x64": "c66bfff68705627b3c5589e038080b284a04027a0a0a3371a54a657a80787789", + "linux-x64": "a1b2797c5ca8245b441b0cc0e18f09463d9d896bd52631874320d77a09a4736d" + }, + "wrapperSha256": "9d129b3d38772e93e97080aa6c4e574ac5df4e47ce5bc331a484a9fbf8188b36", + "helperSha256": "41e0490b617da0d60e0c8ec58ef311236f945129d99f816cff6897f78da7f91d", + "extensionSha256": "71a85bf2e736d4108ea88a4aed01df3f547e8e4ccdeadea8251220e40cb05394", + "agentFilesBinding": "registered-runtime-context-v1", + "nativeQuestions": "select-confirm-input-editor-v3-method-specific-fields", + "pathPolicy": "sdk-normalization-v1", + "mcpToolNames": "provider-safe-exact-source-v1", + "acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1", + "nativeAssistantMessages": "pi1-rpc-deltas-v3-bound-message-and-tool-receipts", + "nativeNotices": "session-extension-v2-bounded-runtime-failure", + "messageProjectionSha256": "17d1e91b948dfa6a56e1905645add52b502041ad843c8d50a13b07956f22aadd", + "noticeProjectionSha256": "014485adc690998e395d334ea3ae67f72be6060a2fc78a1185df7b8d1da28fbf", + "sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1", + "nativeInputDisposition": "queued-only-v1", + "cacheWarming": "disabled-owned-decision-v1", + "thinkingLevel": { + "supported": [ + "off", + "low", + "high", + "max" + ], + "newConfigurationDefault": "low", + "admission": "explicit-native-effective-before-prompt-v1", + "recovery": "mode-bound-v1" + }, + "modelSelection": "caller-selected-native-verified-v1", + "providerConfigurationSourceSha256": "2735ec4b46929f4f59c26cea42e623dfc81016aa7da7bd8399a24caa68a75d6b", + "credentialEnvironmentSourceSha256": "4ee0e9ac56fab8889a4d06b98634bb1797aeda64fcb31e0a404829018c7a12cb", + "runtimeSandboxSourceSha256": "21e1ed2d132886260cf17f956dc21824948bd428b8a0aa05518c508017e66213", + "recoveryIdentitySourceSha256": "f333a24b19ef1b5b5d23ea9778e753f7051690e835ef9e4476a962d8a231c375", + "nativeCancellation": "acknowledged-cancelled-terminal-preserves-usage-v1" + } +} diff --git a/packages/paperclip-runner/test/pi-acp-package-contract.test.mjs b/packages/paperclip-runner/test/pi-acp-package-contract.test.mjs index cc395b8356e..6d1dce690ae 100644 --- a/packages/paperclip-runner/test/pi-acp-package-contract.test.mjs +++ b/packages/paperclip-runner/test/pi-acp-package-contract.test.mjs @@ -149,6 +149,21 @@ test("cancellation closes the iteration before a warm prompt reuses native IDs", assert.equal(new Set(starts.map(update => update.toolCallId)).size, 3); }); +test("acknowledged native cancellation preserves partial usage and refuses service-failure metadata", async t => { + const f = await fixture(t, { PI_FIXTURE_CANCEL_ERROR: "1" }); + const session = await f.call("session/new", { cwd: join(f.root, "workspace"), mcpServers: [] }); + const active = f.call("session/prompt", { sessionId: session.sessionId, prompt: [{ type: "text", text: "long" }] }); + await f.call("pi/steer", { sessionId: session.sessionId, message: "fixture admission fence" }); + f.notify("session/cancel", { sessionId: session.sessionId }); + const result = await active; + assert.equal(result.stopReason, "cancelled"); + assert.equal(result._meta, undefined); + assert.equal(result.usage.inputTokens, 11); + assert.equal(result.usage.outputTokens, 3); + assert.equal(result.usage.cachedReadTokens, undefined); + assert.equal(result.usage._meta.paperclipPi.costUsd, undefined); +}); + test("warm load uses stable display-only history IDs distinct from live execution", async (t) => { const f = await fixture(t, { PI_FIXTURE_HISTORY: "1" }); const session = await f.call("session/new", { cwd: join(f.root, "workspace"), mcpServers: [] }); diff --git a/patches/pi-acp@0.0.33.patch b/patches/pi-acp@0.0.33.patch index 4f11b6493ac..074863bf507 100644 --- a/patches/pi-acp@0.0.33.patch +++ b/patches/pi-acp@0.0.33.patch @@ -477,7 +477,7 @@ + this.uiBridge.cancelAll(); const reason = this.cancelRequested ? "cancelled" : "end_turn"; - this.pendingTurn?.resolve(reason); -+ this.pendingTurn?.resolve({ stopReason: reason, ...this.turnUsage.response() }); ++ this.pendingTurn?.resolve({ stopReason: reason, ...this.turnUsage.response(reason) }); this.pendingTurn = null; this.inAgentLoop = false; const next = this.turnQueue.shift(); @@ -1463,10 +1463,10 @@ + this.failed = previousFailure; + } + -+ response() { ++ response(stopReason ) { + return { + ...(this.observed ? { usage: { ...Object.fromEntries(Object.entries(this.total).filter(([name]) => !this.unknown.has(name ))), _meta: { paperclipPi: { provenance: this.compactionObserved ? "assistant_message_and_compaction_receipts" : "assistant_message_receipts", ...(this.costObserved && !this.costIncomplete ? { costUsd: this.cost, costSource: "pi_pricing_estimate" } : {}) } } } } : {}), -+ ...(this.failed ? { _meta: { jetbrains: { air: { version: 1, sessionFailure: { severity: "error", category: "service", title: "Pi provider request failed" } } } } } : {}), ++ ...(this.failed && stopReason !== "cancelled" ? { _meta: { jetbrains: { air: { version: 1, sessionFailure: { severity: "error", category: "service", title: "Pi provider request failed" } } } } } : {}), + }; + } +} diff --git a/tests/runner-e2e/daytona-image.test.ts b/tests/runner-e2e/daytona-image.test.ts index cffe4380e0b..3668cedc011 100644 --- a/tests/runner-e2e/daytona-image.test.ts +++ b/tests/runner-e2e/daytona-image.test.ts @@ -90,7 +90,7 @@ describe("runner E2E Daytona image contract", () => { expect(dockerignore).toContain("packages/paperclip-runner/runner/target"); expect(await readFile(path.join(repositoryRoot, ".github/docker-context-checks.Dockerfile"), "utf8")).toContain("node packages/paperclip-runner/scripts/generate-acpx-profiles.mjs --check"); for (const declaration of [ - "test-fixtures/pi-acp/profile-v17-identity.json", + "test-fixtures/pi-acp/profile-v18-identity.json", "test/fixtures/copilot-profile-v16-identity.json", ]) { expect(dockerignore).toContain(`!packages/paperclip-runner/${declaration}`); diff --git a/tests/runner-e2e/pi-controls-evidence.test.ts b/tests/runner-e2e/pi-controls-evidence.test.ts index f587c93b253..0656be06b7f 100644 --- a/tests/runner-e2e/pi-controls-evidence.test.ts +++ b/tests/runner-e2e/pi-controls-evidence.test.ts @@ -189,8 +189,8 @@ describe("Pi controls catalog admission", () => { // Extended v3 states the strict file oracle's task-wide Bash limit. const pi = runnerMatrix.find(c => c.profile.qualificationCandidate === "pi")!.profile; expect(pi.modelQualification?.qualificationId).toBe("pi:0.0.33:1.0.0:openrouter"); - expect(runnerSuites.find(s => s.id === "pi-native")!.definitionMetadata).toMatchObject({ version: 16, taskCreation: "explicit-title-and-creation-response-id", agentMemoryParent: "public-managed-file-seed-before-admission", incompleteTerminalCleanup: "retirement-retained-with-failed-watch", profileVersion: 17, agentMemoryContent: "utf8-nonce-plus-final-lf", agentMemoryPrompt: "ordered-native-write-arguments-with-preserved-json-lf", taskPromptTransport: "fenced-markdown-paste-and-multiline-literal-escapes", nativeFinish: "current-contract-objective-evidence-refs", providerFaultExecutable: "stable-preinstalled-runner-link-and-snapshot-node-inode-with-held-bootstrap-fd-3-or-7" }); - expect(runnerSuites.find(s => s.id === "pi-controls")!.definitionMetadata).toMatchObject({ version: 9, taskCreation: "explicit-title-and-creation-response-id", profileVersion: 17, + expect(runnerSuites.find(s => s.id === "pi-native")!.definitionMetadata).toMatchObject({ version: 16, taskCreation: "explicit-title-and-creation-response-id", agentMemoryParent: "public-managed-file-seed-before-admission", incompleteTerminalCleanup: "retirement-retained-with-failed-watch", profileVersion: 18, agentMemoryContent: "utf8-nonce-plus-final-lf", agentMemoryPrompt: "ordered-native-write-arguments-with-preserved-json-lf", taskPromptTransport: "fenced-markdown-paste-and-multiline-literal-escapes", nativeFinish: "current-contract-objective-evidence-refs", providerFaultExecutable: "stable-preinstalled-runner-link-and-snapshot-node-inode-with-held-bootstrap-fd-3-or-7" }); + expect(runnerSuites.find(s => s.id === "pi-controls")!.definitionMetadata).toMatchObject({ version: 9, taskCreation: "explicit-title-and-creation-response-id", profileVersion: 18, remoteProcessIdentity: "observer-pid-startTicks-bootId", controlPlaneSettlement: "required-scoped-result-and-terminal-after-runner" }); expect(runnerSuites.find(s => s.id === "extended-harnesses")!.definitionMetadata).toMatchObject({ version: 4, piFileArtifactTitle: "exact-filename" }); @@ -200,7 +200,7 @@ describe("Pi controls catalog admission", () => { } const hashes = Object.fromEntries(runnerSuites.filter(s => ["pi-native", "native-active-stop", "extended-harnesses", "rich-acp-warm-continuity"].includes(s.id)).map(s => [s.id, suiteDefinitionHash(s)])); expect(hashes).toEqual({ - "pi-native": "82cb1ab814db1e9f4b4ca7877436d5c85b956ed117eff1b809d0f2788c5a440c", + "pi-native": "d548c42dea357803ef4bc652929d1b6f7f1e970a69a96370322d7415c6815b7a", "native-active-stop": "2d4fdeeb75bd531b309bd1b35cfa5680ceefdeee6b7155b068dd011ce9901980", "rich-acp-warm-continuity": "45d0f6a824b061464fa18cb24b1414502b52959d5f47646cfc5079cbb8f2d98b", "extended-harnesses": "77cc19a12c07411bca5f29f48162467bb291d0f47fd567cc1993151cf5c6c989",