mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 01:41:42 +02:00
Account pinned Pi compaction receipts and progress
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
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
@@ -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" } } } } } : {}),
|
||||
+ };
|
||||
+ }
|
||||
|
||||
Reference in new issue
Block a user