fix(pi): preserve acknowledged cancellation terminals

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-07 12:34:47 -05:00
1 parent fcc2e5e735
commit 8e785ac958
19 files changed
+161 -84

No files matched your search

+1 -1
View File
@@ -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
+34 -44
View File
@@ -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
+2 -2
View File
@@ -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": {
@@ -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();
@@ -29,7 +29,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
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 {
@@ -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()
@@ -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);
@@ -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");
@@ -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": {
@@ -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 } } });
@@ -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" } } } } } : {}),
};
}
}
@@ -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",
});
@@ -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");
}
@@ -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; }
@@ -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"
}
}
@@ -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: [] });
+3 -3
View File
@@ -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" } } } } } : {}),
+ };
+ }
+}
+1 -1
View File
@@ -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}`);
@@ -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",