Account pinned Pi compaction receipts and progress

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-28 15:14:57 -05:00
1 parent 742f9bef15
commit 60294a71ac
11 files changed
+242 -52

No files matched your search

@@ -214,3 +214,17 @@ The closed snapshot has been exercised against actual pinned Pi through ACP
initialization and the missing-credential admission path without a model call.
The exact reported `currentModelId` and billable receipt still need credentialed
local and Linux/Daytona qualification.
Profile version 2 declaration digest: `sha256:b18deb44976b92e5e6b4f874f6e28dbbf23b34c917879ee3f7cdc03472919476`. It hashes the versioned
profile domain, patched wrapper source and platform closure pins. Every native
closure remains independently checked at launch. Version 1 warm sessions cannot
be reused with this integration.
Pi 0.84.2 emits `compaction_start`/`compaction_end`; the wrapper maps those
events, includes terminal `result.usage` receipts once, and retains bounded
summarization retry progress. Manual compaction requires an idle session and has
a 120-second RPC deadline; deadline expiry terminates Pi so a timed-out operation
cannot continue invisibly. Successful compaction reports
`assistant_message_and_compaction_receipts` provenance. Pi's summarization retry
helper discards failed-attempt usage, so any retry or missing compaction receipt
invalidates complete-turn token/cost totals instead of inventing complete coverage.
@@ -170,7 +170,7 @@ impl AcpxProviderDescriptor {
"0.0.33",
Some("@earendil-works/pi-coding-agent"),
Some("0.84.2"),
"sha256:8c696f38296d53d0061fa11534570c5ddd951b63532aed30e0f1fcc676dc169f",
"sha256:b18deb44976b92e5e6b4f874f6e28dbbf23b34c917879ee3f7cdc03472919476",
),
"cursor" => (
self.model.as_str(),
@@ -2461,7 +2461,7 @@ mod tests {
"pi",
"pi-acp",
"0.0.33",
"sha256:8c696f38296d53d0061fa11534570c5ddd951b63532aed30e0f1fcc676dc169f",
"sha256:b18deb44976b92e5e6b4f874f6e28dbbf23b34c917879ee3f7cdc03472919476",
Some("@earendil-works/pi-coding-agent"),
Some("0.84.2"),
"openrouter/deepseek/deepseek-v4-flash-0731",
@@ -18,8 +18,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: "0.84.2", sdk: "0.26.0", zod: "3.25.76", nodeVersion: PI_NODE_VERSION,
wrapperSha256: "18375d30e7eca9fddf6c024f49899d6da38c064d8587aa1678fd4c2d70308fbc",
helperSha256: "57dd03e3d1e59d0191f0dd66f5a809e6c825de3c4b6660bff697686c160a659f",
wrapperSha256: "80c0e302e75b39d71404c69afb2d5182c468ccdee68bfe8b09f1f40e20a47fb4",
helperSha256: "4333d284ff06ab1195e49630041f0b03a486a92fc678100525ad107e0982dba1",
});
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
@@ -91,6 +91,16 @@ describe("Pi ACP bridge", () => {
} });
});
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 } } });
const compacted = { firstKeptEntryId: "entry-2", tokensBefore: 100, summary: "Context summary", usage: { input: 10, output: 4, cacheRead: 2, cacheWrite: 0, cost: { total: 0.02 } } };
usage.acceptCompaction(compacted); usage.acceptCompaction(compacted);
expect(usage.response()).toMatchObject({ usage: { inputTokens: 11, outputTokens: 6, totalTokens: 19, _meta: { paperclipPi: { provenance: "assistant_message_and_compaction_receipts", costUsd: 0.03 } } }, _meta: { jetbrains: { air: { sessionFailure: { severity: "error" } } } } });
usage.acceptCompaction({ summary: "Missing usage" });
expect(usage.response().usage).toEqual({ _meta: { paperclipPi: { provenance: "assistant_message_and_compaction_receipts" } } });
});
it("launches verified paths only and rejects session escapes", async () => {
const root = await mkdtemp(join(tmpdir(), "paperclip-pi-launch-")); temporary.push(root);
await mkdir(join(root, "sessions"));
@@ -210,7 +210,7 @@ export class PiUiBridge {
}
}
/** Sum actual assistant receipts, never context occupancy or estimated cost. */
/** Sum native usage receipts; preserve missing fields and label catalog pricing. */
export class PiTurnUsage {
private seen = new Set<string>();
private total = { inputTokens: 0, outputTokens: 0, cachedReadTokens: 0, cachedWriteTokens: 0, totalTokens: 0 };
@@ -218,9 +218,10 @@ export class PiTurnUsage {
private unknown = new Set<keyof typeof this.total>();
private costObserved = false;
private costIncomplete = false;
private compactionObserved = false;
private failed = false;
private observed = false;
reset(): void { this.seen.clear(); this.total = { inputTokens: 0, outputTokens: 0, cachedReadTokens: 0, cachedWriteTokens: 0, totalTokens: 0 }; this.cost = 0; this.unknown.clear(); this.costObserved = false; this.costIncomplete = false; this.failed = false; this.observed = false; }
reset(): void { this.seen.clear(); this.total = { inputTokens: 0, outputTokens: 0, cachedReadTokens: 0, cachedWriteTokens: 0, totalTokens: 0 }; this.cost = 0; this.unknown.clear(); this.costObserved = false; this.costIncomplete = false; this.compactionObserved = false; this.failed = false; this.observed = false; }
accept(value: unknown): void {
const message = record(value);
if (message.role !== "assistant") return;
@@ -252,9 +253,30 @@ export class PiTurnUsage {
} else this.costIncomplete = true;
}
markIncomplete(): void {
for (const name of Object.keys(this.total)) this.unknown.add(name as keyof typeof this.total);
this.costIncomplete = true;
}
acceptCompaction(value: unknown): void {
this.compactionObserved = true;
const result = value && typeof value === "object" && !Array.isArray(value) ? record(value) : {};
if (!result.usage || typeof result.usage !== "object") {
// A compaction may consume tokens even if its terminal receipt is absent.
// Known assistant counters alone cannot establish the complete turn.
this.markIncomplete();
return;
}
const previousFailure = this.failed;
this.accept({ role: "assistant", id: "compaction", timestamp: result.firstKeptEntryId,
content: [result.tokensBefore, result.summary], usage: result.usage });
// A successful summary is not proof that an earlier failed model turn recovered.
this.failed = previousFailure;
}
response(): 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: "assistant_message_receipts", ...(this.costObserved && !this.costIncomplete ? { costUsd: this.cost, costSource: "pi_pricing_estimate" } : {}) } } } } : {}),
...(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" } } } } } : {}),
};
}
@@ -6,7 +6,7 @@
* three pins. Never accept a digest supplied only by an installed manifest.
*/
export const PI_DISTRIBUTION_CLOSURE_SHA256 = Object.freeze({
"darwin-arm64": "f2c95f532e4b56a57e938ceb62b5ea1a86bc721cba29b41de6270afe3fe0041f",
"darwin-x64": "93f3c06c91fc9e06084dff083f3b43ac100dee3ac2c5f5981e3d281f1e561a20",
"linux-x64": "99707d2c78656bebdf5df4b85e4674a72d5d30a3a0c730536246251a2889d0a8",
"darwin-arm64": "2fde022b3714c497f352e628789c4dd47b6446351dc69779293b3a0eafcbc17f",
"darwin-x64": "049fe566c4844db05187cc7e3e94b813f9a3fa8600d37c344f3b425b8776f743",
"linux-x64": "12ff126d91bc296f39bd6c82b52b95dbf8075076e6257bb73256b043f614f391",
});
@@ -1,5 +1,6 @@
import { cp, mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
import { cp, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
import { once } from "node:events";
import { createHash } from "node:crypto";
import { StringDecoder } from "node:string_decoder";
import { tmpdir } from "node:os";
import { join } from "node:path";
@@ -22,6 +23,13 @@ async function fixture() {
}
describe("Pi installation factory", () => {
it("binds the profile declaration to the reviewed patch and platform closure pins", async () => {
const digest = createHash("sha256").update("paperclip.pi.rich-acp.profile.v2\0")
.update(await readFile(new URL("../../../../../patches/pi-acp@0.0.33.patch", import.meta.url)))
.update("\0").update(await readFile(new URL("./pi-closure-pins.ts", import.meta.url))).digest("hex");
expect(QUALIFIED_ACPX_PROFILES.pi.commandDigest).toBe(`sha256:${digest}`);
});
it("rejects legacy profiles and caller-selected identities", () => {
expect(() => assertPiInstallationProfile(candidate())).not.toThrow();
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 1 })).toThrow("version 2");
@@ -47,13 +47,14 @@ export const QUALIFIED_ACPX_PROFILES: Readonly<
protocolVersion: ACPX_DRIVER_PROTOCOL_VERSION,
acpxVersion: QUALIFIED_ACPX_VERSION,
agent: "pi",
agentProfileVersion: 1,
agentProfileVersion: 2,
qualificationStatus: "pending",
agentServerPackage: "pi-acp",
agentServerVersion: "0.0.33",
agentRuntimePackage: "@earendil-works/pi-coding-agent",
agentRuntimeVersion: "0.84.2",
commandDigest:
"sha256:8c696f38296d53d0061fa11534570c5ddd951b63532aed30e0f1fcc676dc169f",
"sha256:b18deb44976b92e5e6b4f874f6e28dbbf23b34c917879ee3f7cdc03472919476",
qualificationModel: "openrouter/deepseek/deepseek-v4-flash-0731",
reportedModelId: "openrouter/deepseek/deepseek-v4-flash-0731",
permissionPolicy: "interactive",
@@ -9,6 +9,7 @@ writeFileSync(sessionFile, JSON.stringify({ type: "session", cwd: process.cwd()
const output = (value) => process.stdout.write(JSON.stringify(value) + "\n");
let active = false;
let model = "fixture-model";
const compaction = () => ({ firstKeptEntryId: "fixture-entry", tokensBefore: 50, summary: "Fixture compacted", usage: { input: 5, output: 2, cacheRead: 1, cacheWrite: 0, cost: { total: 0.02 } } });
const finish = (answer = "done") => {
output({ type: "message_update", assistantMessageEvent: { type: "text_delta", delta: answer } });
output({ type: "message_end", message: { role: "assistant", timestamp: Date.now(), stopReason: "stop", usage: { input: 11, output: 3, cacheRead: 2, cacheWrite: 0, cost: { total: 0.01 } } } });
@@ -27,11 +28,13 @@ createInterface({ input: process.stdin }).on("line", (line) => {
if (request.type === "get_commands") { response({ commands: process.env.PI_FIXTURE_EXTENSION_FAIL ? [] : [{ name: "paperclip-runtime-ready-v1", description: "Paperclip runtime gate v1", source: "extension" }] }); return; }
if (request.type === "steer") { response(); output({ type: "message_update", assistantMessageEvent: { type: "text_delta", delta: `steered:${request.message}` } }); return; }
if (request.type === "follow_up") { response(); 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; output({ type: "agent_settled" }); } return; }
if (request.type === "prompt") {
response({ disposition: "started" }); active = true; output({ type: "agent_start" });
if (request.message === "die") { process.exit(4); }
if (request.message === "long") return;
if (request.message === "auto-compact" || request.message === "retry-compact") { output({ type: "compaction_start", reason: "threshold" }); if (request.message === "retry-compact") output({ type: "summarization_retry_scheduled" }); output({ type: "compaction_end", reason: "threshold", result: compaction(), aborted: false, willRetry: true }); finish("compacted"); return; }
if (request.message === "question") { output({ type: "extension_ui_request", id: "question-id", method: "input", title: "Project name", placeholder: "Name" }); return; }
if (request.message === "permission") { output({ type: "extension_ui_request", id: "permission-id", method: "select", title: 'paperclip.pi.permission.v1:{"toolCallId":"tool-1","toolName":"bash","input":{"command":"pwd"}}', options: ["Allow once", "Allow for this session", "Deny"] }); return; }
if (request.message === "failure") {
@@ -77,7 +77,7 @@ async function fixture(t, extra = {}) {
await rm(root, { recursive: true, force: true });
});
const initialized = await call("initialize", { protocolVersion: 1, clientCapabilities: { elicitation: { form: {} } } });
return { call, initialized, root, notifications, requests, setAnswer(value) { answer = value; } };
return { call, notify(method, params) { send({ jsonrpc: "2.0", method, params }); }, initialized, root, notifications, requests, setAnswer(value) { answer = value; } };
}
test("actual patched ACP process streams thinking and waits for settlement with usage", async (t) => {
@@ -122,3 +122,23 @@ test("provider failure is typed and unadmitted slash commands cannot bypass cont
assert.equal(result._meta.jetbrains.air.sessionFailure.severity, "error");
await assert.rejects(f.call("session/prompt", { sessionId: session.sessionId, prompt: [{ type: "text", text: "/export /outside/report.html" }] }), /not admitted/);
});
test("manual and automatic compaction retain Pi 0.84.2 progress and usage", async (t) => {
const f = await fixture(t); const session = await f.call("session/new", { cwd: join(f.root, "workspace"), mcpServers: [] });
const prompt = (text) => f.call("session/prompt", { sessionId: session.sessionId, prompt: [{ type: "text", text }] });
const manual = await prompt("/compact");
assert.equal(manual.usage.inputTokens, 5); assert.equal(manual.usage.totalTokens, 8);
assert.equal(manual.usage._meta.paperclipPi.provenance, "assistant_message_and_compaction_receipts");
const automatic = await prompt("auto-compact");
assert.equal(automatic.usage.inputTokens, 16); assert.equal(automatic.usage.totalTokens, 24);
assert.equal(automatic.usage._meta.paperclipPi.costUsd, 0.03);
assert.ok(f.notifications.some((event) => event.params?.update?.content?.text === "Context compaction finished."));
const retried = await prompt("retry-compact");
assert.equal(retried.usage.inputTokens, undefined);
assert.equal(retried.usage._meta.paperclipPi.costUsd, undefined);
assert.ok(f.notifications.some((event) => event.params?.update?.content?.text === "Context summarization is retrying a provider request."));
const active = prompt("long"); await new Promise((resolve) => setTimeout(resolve, 25));
await assert.rejects(prompt("/compact"), /requires an idle session/);
f.notify("session/cancel", { sessionId: session.sessionId }); await active;
assert.match(packageSource, /this\.request\(\{ type: "compact", customInstructions \}, 120000\)/);
});
+150 -38
View File
@@ -1,4 +1,3 @@
diff --git a/dist/index.js b/dist/index.js
--- a/dist/index.js
+++ b/dist/index.js
@@ -11,6 +11,7 @@
@@ -127,7 +126,23 @@ diff --git a/dist/index.js b/dist/index.js
}
}
/**
@@ -288,12 +287,17 @@
@@ -242,7 +241,7 @@
if (!res.success) throw new Error(`pi set_steering_mode failed: ${res.error ?? JSON.stringify(res.data)}`);
}
async compact(customInstructions) {
- const res = await this.request({ type: "compact", customInstructions });
+ const res = await this.request({ type: "compact", customInstructions }, 120000);
if (!res.success) throw new Error(`pi compact failed: ${res.error ?? JSON.stringify(res.data)}`);
return res.data;
}
@@ -283,17 +282,23 @@
await this.writeLine(`${JSON.stringify({ type: "extension_ui_response", ...response })}
`);
}
- request(cmd) {
+ request(cmd, timeoutMs = 30000) {
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 120000) throw new Error("Invalid Pi RPC deadline");
const id = crypto.randomUUID();
const withId = { ...cmd, id };
const line = `${JSON.stringify(withId)}
`;
@@ -138,7 +153,7 @@ diff --git a/dist/index.js b/dist/index.js
- this.pending.delete(id);
- reject(error);
- });
+ const timer = setTimeout(() => finish(new Error("Pi RPC request timed out")), 30000);
+ const timer = setTimeout(() => { finish(new Error("Pi RPC request timed out")); this.dispose("SIGKILL"); }, timeoutMs);
+ timer.unref();
+ const finish = (error, value) => {
+ if (!this.pending.delete(id)) return;
@@ -150,7 +165,7 @@ diff --git a/dist/index.js b/dist/index.js
});
}
writeLine(line) {
@@ -337,7 +341,7 @@
@@ -337,7 +342,7 @@
{
authMethods: getAuthMethods()
},
@@ -159,7 +174,7 @@ diff --git a/dist/index.js b/dist/index.js
);
}
@@ -714,6 +718,7 @@
@@ -714,6 +719,7 @@
try {
proc = await PiRpcProcess.spawn({
cwd: params.cwd,
@@ -167,7 +182,7 @@ diff --git a/dist/index.js b/dist/index.js
piCommand: params.piCommand
});
} catch (e) {
@@ -808,6 +813,9 @@
@@ -808,6 +814,9 @@
this.proc = opts.proc;
this.conn = opts.conn;
this.fileCommands = opts.fileCommands ?? [];
@@ -177,7 +192,7 @@ diff --git a/dist/index.js b/dist/index.js
this.proc.onEvent((ev) => this.handlePiEvent(ev));
}
setStartupInfo(text) {
@@ -852,6 +860,7 @@
@@ -852,6 +861,7 @@
}
async cancel() {
this.cancelRequested = true;
@@ -185,7 +200,7 @@ diff --git a/dist/index.js b/dist/index.js
if (this.turnQueue.length) {
const queued = this.turnQueue.splice(0, this.turnQueue.length);
for (const t of queued) t.resolve("cancelled");
@@ -920,12 +929,16 @@
@@ -920,12 +930,16 @@
this.cancelRequested = false;
this.inAgentLoop = false;
this.pendingTurn = { resolve: t.resolve, reject: t.reject };
@@ -202,7 +217,7 @@ diff --git a/dist/index.js b/dist/index.js
const authErr = maybeAuthRequiredError(err);
if (authErr) {
this.pendingTurn?.reject(authErr);
@@ -946,6 +959,23 @@
@@ -946,6 +960,23 @@
handlePiEvent(ev) {
const type = String(ev.type ?? "");
switch (type) {
@@ -226,7 +241,7 @@ diff --git a/dist/index.js b/dist/index.js
case "message_update": {
const ame = ev.assistantMessageEvent;
if (ame?.type === "text_delta" && typeof ame.delta === "string") {
@@ -1047,7 +1077,7 @@
@@ -1047,7 +1078,7 @@
if (p) {
try {
const abs = isAbsolute(p) ? p : resolvePath(this.cwd, p);
@@ -235,7 +250,7 @@ diff --git a/dist/index.js b/dist/index.js
this.fileSnapshots.set(toolCallId, { path: p, oldText: snapshotOldText });
if (toolName === "edit") {
for (const needle of getEditOldTexts(args)) {
@@ -1125,7 +1155,7 @@
@@ -1125,7 +1156,7 @@
if (!isError && snapshot) {
try {
const abs = isAbsolute(snapshot.path) ? snapshot.path : resolvePath(this.cwd, snapshot.path);
@@ -244,7 +259,7 @@ diff --git a/dist/index.js b/dist/index.js
if (snapshot.oldText === null || newText !== snapshot.oldText) {
hasStructuredDiff = true;
content = [
@@ -1154,14 +1184,11 @@
@@ -1154,14 +1185,11 @@
break;
}
case "extension_ui_request": {
@@ -263,7 +278,46 @@ diff --git a/dist/index.js b/dist/index.js
break;
}
case "auto_retry_start": {
@@ -1210,9 +1237,12 @@
@@ -1178,24 +1206,22 @@
});
break;
}
- case "auto_compaction_start": {
- this.emit({
- sessionUpdate: "agent_message_chunk",
- content: {
- type: "text",
- text: "Context nearing limit, running automatic compaction..."
- }
- });
+ case "compaction_start": {
+ this.emit({ sessionUpdate: "agent_message_chunk", content: {
+ type: "text", text: ev.reason === "manual" ? "Compacting context..." : "Context nearing limit, running automatic compaction..."
+ } });
break;
}
- case "auto_compaction_end": {
- this.emit({
- sessionUpdate: "agent_message_chunk",
- content: {
- type: "text",
- text: "Automatic compaction finished; context was summarized to continue the session."
- }
- });
+ case "compaction_end": {
+ (this.pendingTurn ? this.turnUsage : this.manualCompactionUsage)?.acceptCompaction(ev.result);
+ this.emit({ sessionUpdate: "agent_message_chunk", content: {
+ type: "text", text: ev.aborted ? "Context compaction cancelled." : ev.errorMessage ? "Context compaction failed." : "Context compaction finished."
+ } });
+ break;
+ }
+ case "summarization_retry_scheduled": {
+ (this.pendingTurn ? this.turnUsage : this.manualCompactionUsage)?.markIncomplete();
+ this.emit({ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "Context summarization is retrying a provider request." } });
break;
}
case "agent_start": {
@@ -1210,9 +1236,12 @@
break;
}
case "agent_settled": {
@@ -277,7 +331,7 @@ diff --git a/dist/index.js b/dist/index.js
this.pendingTurn = null;
this.inAgentLoop = false;
const next = this.turnQueue.shift();
@@ -1725,6 +1755,7 @@
@@ -1725,6 +1754,7 @@
return process.env.PI_CODING_AGENT_DIR ? resolve3(process.env.PI_CODING_AGENT_DIR) : join4(homedir4(), ".pi", "agent");
}
function getEnableSkillCommands(cwd) {
@@ -285,7 +339,7 @@ diff --git a/dist/index.js b/dist/index.js
const merged = getMergedSettings(cwd);
const direct = merged.enableSkillCommands;
if (typeof direct === "boolean") return direct;
@@ -1733,6 +1764,7 @@
@@ -1733,6 +1763,7 @@
return true;
}
function getQuietStartup(cwd) {
@@ -293,7 +347,7 @@ diff --git a/dist/index.js b/dist/index.js
const merged = getMergedSettings(cwd);
const direct = merged.quietStartup;
if (typeof direct === "boolean") return direct;
@@ -1824,6 +1856,7 @@
@@ -1824,6 +1855,7 @@
const out = [];
const seen = /* @__PURE__ */ new Set();
for (const c of [...a, ...b]) {
@@ -301,7 +355,7 @@ diff --git a/dist/index.js b/dist/index.js
if (seen.has(c.name)) continue;
seen.add(c.name);
out.push(c);
@@ -1884,11 +1917,13 @@
@@ -1884,11 +1916,13 @@
throw RequestError3.invalidParams(`Unknown sessionId: ${sessionId}`);
}
const cwd = opts?.cwd ?? stored.cwd;
@@ -315,7 +369,7 @@ diff --git a/dist/index.js b/dist/index.js
piCommand: process.env.PI_ACP_PI_COMMAND
});
} catch (e) {
@@ -1897,7 +1932,7 @@
@@ -1897,7 +1931,7 @@
}
throw e;
}
@@ -324,7 +378,7 @@ diff --git a/dist/index.js b/dist/index.js
const session = this.sessions.getOrCreate(sessionId, {
cwd,
mcpServers: opts?.mcpServers ?? [],
@@ -1917,6 +1952,7 @@
@@ -1917,6 +1951,7 @@
}
}
async initialize(params) {
@@ -332,7 +386,7 @@ diff --git a/dist/index.js b/dist/index.js
const supportedVersion = 1;
const requested = params.protocolVersion;
return {
@@ -1933,7 +1969,8 @@
@@ -1933,7 +1968,8 @@
}),
agentCapabilities: {
loadSession: true,
@@ -342,7 +396,7 @@ diff --git a/dist/index.js b/dist/index.js
promptCapabilities: {
image: true,
audio: false,
@@ -1942,8 +1979,7 @@
@@ -1942,8 +1978,7 @@
sessionCapabilities: {
// **UNSTABLE** ACP capability used by Zed's codex-acp adapter.
// Enables a native session picker in clients that support it.
@@ -352,7 +406,7 @@ diff --git a/dist/index.js b/dist/index.js
}
}
};
@@ -1953,7 +1989,7 @@
@@ -1953,7 +1988,7 @@
throw RequestError3.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
}
this.lastSessionCwd = params.cwd;
@@ -361,7 +415,7 @@ diff --git a/dist/index.js b/dist/index.js
const enableSkillCommands = getEnableSkillCommands(params.cwd);
const session = await this.sessions.create({
cwd: params.cwd,
@@ -1994,14 +2030,14 @@
@@ -1994,14 +2029,14 @@
this.cleanupFailedNewSession(session.sessionId, state);
throw RequestError3.authRequired(
{ authMethods: getAuthMethods() },
@@ -378,8 +432,11 @@ diff --git a/dist/index.js b/dist/index.js
);
}
const { configOptions, models, modes } = await getSessionConfiguration(session.proc, {
@@ -2066,6 +2102,8 @@
@@ -2064,34 +2099,27 @@
}
async prompt(params) {
const session = await this.restoreSession(params.sessionId);
+ if (session.manualCompactionUsage) throw RequestError3.invalidParams("Pi context compaction is active");
const { message, images } = promptToPiMessage(params.prompt);
if (images.length === 0 && message.trimStart().startsWith("/")) {
+ const command = message.trim().split(/\s+/)[0];
@@ -387,7 +444,42 @@ diff --git a/dist/index.js b/dist/index.js
const trimmed = message.trim();
const space = trimmed.indexOf(" ");
const cmd = space === -1 ? trimmed.slice(1) : trimmed.slice(1, space);
@@ -2432,8 +2470,18 @@
const argsString = space === -1 ? "" : trimmed.slice(space + 1);
const args = parseCommandArgs(argsString);
if (cmd === "compact") {
+ if (session.pendingTurn || session.manualCompactionUsage) throw RequestError3.invalidParams("Manual compaction requires an idle session");
const customInstructions = args.join(" ").trim() || void 0;
- const res = await session.proc.compact(customInstructions);
- const r = res && typeof res === "object" ? res : null;
- const tokensBefore = typeof r?.tokensBefore === "number" ? r.tokensBefore : null;
- const summary = typeof r?.summary === "string" ? r.summary : null;
- const headerLines = [
- `Compaction completed.${customInstructions ? " (custom instructions applied)" : ""}`,
- tokensBefore !== null ? `Tokens before: ${tokensBefore}` : null
- ].filter(Boolean);
- const text = headerLines.join("\n") + (summary ? `
-
-${summary}` : "");
- await this.conn.sessionUpdate({
- sessionId: session.sessionId,
- update: {
- sessionUpdate: "agent_message_chunk",
- content: { type: "text", text }
- }
- });
- return { stopReason: "end_turn" };
+ const compactionUsage = new PiTurnUsage();
+ session.manualCompactionUsage = compactionUsage;
+ try {
+ const res = await session.proc.compact(customInstructions);
+ compactionUsage.acceptCompaction(res);
+ await session.flushEmits();
+ return { stopReason: "end_turn", ...compactionUsage.response() };
+ } finally { session.manualCompactionUsage = null; }
}
if (cmd === "session") {
const stats = await session.proc.getSessionStats();
@@ -2432,8 +2460,18 @@
}
}
const result = await session.prompt(message, images);
@@ -408,7 +500,7 @@ diff --git a/dist/index.js b/dist/index.js
}
async cancel(params) {
const session = this.sessions.maybeGet(params.sessionId);
@@ -2473,7 +2521,7 @@
@@ -2473,7 +2511,7 @@
mcpServers: params.mcpServers
});
const proc = session.proc;
@@ -417,7 +509,7 @@ diff --git a/dist/index.js b/dist/index.js
this.sessions.closeAllExcept?.(session.sessionId);
this.store.upsert({
sessionId: params.sessionId,
@@ -2607,6 +2655,7 @@
@@ -2607,6 +2645,7 @@
return response;
}
async deleteSession(params) {
@@ -425,7 +517,7 @@ diff --git a/dist/index.js b/dist/index.js
const stored = this.store.get(params.sessionId);
const piSession = findPiSession(params.sessionId);
if (!stored && !piSession) {
@@ -2828,6 +2877,7 @@
@@ -2828,6 +2867,7 @@
return 0;
}
function buildUpdateNotice() {
@@ -433,7 +525,7 @@ diff --git a/dist/index.js b/dist/index.js
try {
const piVersion = spawnSync("pi", ["--version"], { encoding: "utf-8" });
const installed = (String(piVersion.stdout ?? "").trim() || String(piVersion.stderr ?? "").trim()).replace(
@@ -2848,6 +2898,7 @@
@@ -2848,6 +2888,7 @@
}
}
function buildStartupInfo(opts) {
@@ -441,7 +533,7 @@ diff --git a/dist/index.js b/dist/index.js
void opts.fileCommands;
const md = [];
try {
@@ -2980,6 +3031,7 @@
@@ -2980,6 +3021,7 @@
// src/index.ts
if (process.argv.includes("--terminal-login")) {
@@ -449,7 +541,7 @@ diff --git a/dist/index.js b/dist/index.js
const { spawnSync: spawnSync2 } = await import("child_process");
const cmd = getPiCommand(process.env.PI_ACP_PI_COMMAND);
const res = spawnSync2(cmd, [], {
@@ -3019,11 +3071,12 @@
@@ -3019,11 +3061,12 @@
}
});
var stream = ndJsonStream(input, output);
@@ -464,11 +556,9 @@ diff --git a/dist/index.js b/dist/index.js
} catch {
}
try {
diff --git a/dist/paperclip-runtime.js b/dist/paperclip-runtime.js
new file mode 100644
--- /dev/null
--- a/dist/paperclip-runtime.js
+++ b/dist/paperclip-runtime.js
@@ -0,0 +1,261 @@
@@ -0,0 +1,283 @@
+/** Source for the helper embedded in patches/pi-acp@0.0.33.patch. */
+import { StringDecoder } from "node:string_decoder";
+import { isAbsolute, relative, resolve, sep } from "node:path";
@@ -681,7 +771,7 @@ new file mode 100644
+ }
+}
+
+/** Sum actual assistant receipts, never context occupancy or estimated cost. */
+/** Sum native usage receipts; preserve missing fields and label catalog pricing. */
+export class PiTurnUsage {
+ seen = new Set ();
+ total = { inputTokens: 0, outputTokens: 0, cachedReadTokens: 0, cachedWriteTokens: 0, totalTokens: 0 };
@@ -689,9 +779,10 @@ new file mode 100644
+ unknown = new Set ();
+ costObserved = false;
+ costIncomplete = false;
+ compactionObserved = false;
+ failed = false;
+ observed = false;
+ reset() { this.seen.clear(); this.total = { inputTokens: 0, outputTokens: 0, cachedReadTokens: 0, cachedWriteTokens: 0, totalTokens: 0 }; this.cost = 0; this.unknown.clear(); this.costObserved = false; this.costIncomplete = false; this.failed = false; this.observed = false; }
+ reset() { this.seen.clear(); this.total = { inputTokens: 0, outputTokens: 0, cachedReadTokens: 0, cachedWriteTokens: 0, totalTokens: 0 }; this.cost = 0; this.unknown.clear(); this.costObserved = false; this.costIncomplete = false; this.compactionObserved = false; this.failed = false; this.observed = false; }
+ accept(value ) {
+ const message = record(value);
+ if (message.role !== "assistant") return;
@@ -723,9 +814,30 @@ new file mode 100644
+ } else this.costIncomplete = true;
+ }
+
+ markIncomplete() {
+ for (const name of Object.keys(this.total)) this.unknown.add(name );
+ this.costIncomplete = true;
+ }
+
+ acceptCompaction(value ) {
+ this.compactionObserved = true;
+ const result = value && typeof value === "object" && !Array.isArray(value) ? record(value) : {};
+ if (!result.usage || typeof result.usage !== "object") {
+ // A compaction may consume tokens even if its terminal receipt is absent.
+ // Known assistant counters alone cannot establish the complete turn.
+ this.markIncomplete();
+ return;
+ }
+ const previousFailure = this.failed;
+ this.accept({ role: "assistant", id: "compaction", timestamp: result.firstKeptEntryId,
+ content: [result.tokensBefore, result.summary], usage: result.usage });
+ // A successful summary is not proof that an earlier failed model turn recovered.
+ this.failed = previousFailure;
+ }
+
+ response() {
+ return {
+ ...(this.observed ? { usage: { ...Object.fromEntries(Object.entries(this.total).filter(([name]) => !this.unknown.has(name ))), _meta: { paperclipPi: { provenance: "assistant_message_receipts", ...(this.costObserved && !this.costIncomplete ? { costUsd: this.cost, costSource: "pi_pricing_estimate" } : {}) } } } } : {}),
+ ...(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" } } } } } : {}),
+ };
+ }