mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 00:53:38 +02:00
fix(pi): preserve acknowledged cancellation terminals
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
fcc2e5e735
commit
8e785ac958
19 files changed
+161
-84
No files matched your search
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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: [] });
|
||||
|
||||
@@ -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" } } } } } : {}),
|
||||
+ };
|
||||
+ }
|
||||
+}
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user