Align warm Pi reopen with native cold admission

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-03 01:18:03 -05:00
1 parent 96a0e329b6
commit 0d65753fe1
5 files changed
+184 -10

No files matched your search

@@ -332,8 +332,13 @@ prices are estimates. Unpriced usage must stay unpriced in the ledger and UI.
- Normal ARM, Intel and Linux public installations pass in 8.043, 37.681 and
5.698 s respectively. Each uses the normal installed daemon, verifies profile
14, submits no prompt and observes clean Runner exit. Workspace build and
typecheck pass. The full local Vitest run remains live; embedded Postgres
integration suites report host skips, so it is not complete integration proof.
typecheck pass. The broad local Vitest attempt completes with 47 failed files,
533 passed files and 163 skipped files. Private Postgres library symlinks are
missing, and the restricted test PATH omits macOS lsof. Both setup causes are
corrected only in the task-owned dependency environment. The focused DB and
process-owner checks pass 95 tests with three existing skips. The full suite
then reruns with the corrected environment. The failed log remains intact; it is not
a passing full local test or complete integration claim.
- The local Product controller-restart cell passes with canonical evidence.
The next cell, warm-three-turn, completes turn 1 but fails before turn 2
provider work: native `run.attach` reaches its 30-second command timeout.
@@ -345,13 +350,18 @@ prices are estimates. Unpriced usage must stay unpriced in the ledger and UI.
of the completed-turn failure and does not qualify warm continuity. Its strict
cleanup receipt fails even though native state reports suspended and Runner
exits cleanly; both observations are retained. No model prompt is submitted.
- Investigate authority rotation and native provider reopen before choosing the
correction. Native ACPX attachment checkpoints the old sidecar and opens a
new one. Pi's native session-open admission bound is 60 s, but warm attachment
currently uses the ordinary 30 s controller command wait. This discrepancy
is a candidate cause, not a measured resolution. Do not widen the 60 s bound,
weaken the three-turn identity checks or retry the paid failure unchanged.
- All 55 CI checks and Greptile 5/5 are terminal at `3d75626bf`, with zero
- A free completed-turn regression proves the warm-admission mismatch. It
finishes turn 1 through the native semantic bridge, checkpoints the sidecar,
and delays the replacement's exact-identity admission by 32 s. The unchanged
controller times out at `run.attach` and strict suspension fails. The corrected
controller uses Pi's existing absolute 60 s cold-process admission budget for
warm attachment and aborts admission on close. The same fixture then completes
turn 2, retains one Runner, starts exactly two sidecars without a retry, and
passes strict cleanup. TypeScript passes; the full transport/eval-session
suite is running. The native 60 s bound and profile-14 bytes are unchanged.
The paid three-turn failure remains failed. Fresh exact-source packaging and
one explicit paid retry are still required; no live resolution is claimed.
- All 55 CI checks and Greptile 5/5 are terminal at `96a0e329b`, with zero
unresolved draft-PR threads. The immutable image job 37099122706 remains
queued. Track it without a duplicate dispatch. Qualification, prerequisites
and image/companion gates still hold production.
+6
View File
@@ -60,6 +60,12 @@ and Daytona qualification passes. Their verified distributions are build-owned;
no candidate accepts an arbitrary executable. See
[the rich ACP capability report](../../doc/architecture/runner-rich-acp-capabilities.md).
Pi cold provider admission has an absolute 60-second budget. Warm run attachment
checkpoints the old ACPX sidecar and starts a new one, so it uses that same budget
while retaining the existing Runner authority. Live adoption and ordinary
commands retain their 30-second bounds. Closing the transport cancels admission;
an acknowledgement received after the admission deadline cannot revive it.
Cursor candidate configuration accepts `acpxSessionMode: "agent" | "plan" | "ask"`
(default `agent`). This selects the native Cursor mode independently of
`acpxPermissionMode` and Paperclip task planning or company approvals. The mode
@@ -0,0 +1,80 @@
// Protocol-only fixture: no credentials, network, or model calls.
const { appendFileSync } = require("node:fs");
const { createInterface } = require("node:readline");
// The test binds configuration into the verified script snapshot before launch.
const config = /* fixture-config */ null;
let identity;
let runId;
let turnId;
let sequence = 1;
const send = (value) => process.stdout.write(`${JSON.stringify({ protocolVersion: 2, ...value })}\n`);
const event = (eventType, payload) => send({ sequence: sequence++, eventType, runId, turnId, payload });
const journal = (value) => appendFileSync(config.journal, `${JSON.stringify({ pid: process.pid, ...value })}\n`);
journal({ event: "spawn" });
async function handle({ id, command, params = {} }) {
journal({ command, resumed: command === "session.open" && !!params.expectedIdentity });
let result;
switch (command) {
case "initialize":
result = { protocolVersion: 2, sidecarPid: process.pid, profile: { agent: "pi" }, capabilities: {
persistentSessions: true, exactModelVerification: true, permissions: "runner_policy",
semanticTools: "runner_bridge", structuredInput: "paperclip.question_set.v1",
} };
break;
case "session.open":
if (params.expectedIdentity) await new Promise((resolve) => setTimeout(resolve, config.resumeDelayMs));
identity = params.expectedIdentity ?? {
kind: "acpx", normalizedSessionId: params.normalizedSessionId,
acpxRecordId: "record-warm", backendSessionId: "backend-warm", agentSessionId: "agent-warm",
profileDigest: "sha256:f35145437eeb355ed37bc5a4fa93d7ede561d9c45daf311979b46890b808ddd4",
workspaceDigest: `sha256:${"2".repeat(64)}`, requestedModel: params.model, effectiveModel: params.model,
permissionMode: params.permissionMode, piThinkingLevel: params.piThinkingLevel,
providerLifetimeFenceCandidates: [61001, 61002, 61003],
};
result = { sidecarPid: process.pid, identity, status: {}, turnControls: { steering: true, queuedFollowUp: true } };
break;
case "run.attach":
runId = params.runId;
result = { runId, catalogRevision: params.catalogRevision };
break;
case "turn.start":
turnId = params.turnId;
result = { turnId, turnControls: { steering: true, queuedFollowUp: true } };
break;
case "tool.resolve":
if (params.error) throw new Error(`Completion rejected: ${JSON.stringify(params.error)}`);
result = { resolved: true };
break;
case "session.snapshot":
result = { identity, runId, turnId: null, pendingRuntimeRequests: [] };
break;
case "session.goal.get":
result = { schema: "paperclip.session_goal.snapshot.v1", goal: null, workingNow: false, sessionGoals: {
availability: "available", actions: ["set", "pause", "resume", "clear"], autonomousUpdates: true,
persistentAcrossResume: true, maxObjectiveChars: 4000, tokenBudgetControl: false, usageReporting: false,
} };
break;
case "session.suspend": result = { suspended: true, identity }; break;
case "session.close": result = { closed: true }; break;
case "turn.cancel": result = { cancelled: true }; break;
default: throw new Error(`Unexpected fixture command ${command}`);
}
send({ id, ok: true, result });
if (command === "turn.start") event("runtime.tool_called", {
callId: `finish-${turnId}`, operationId: "paperclip_finish", input: {
schema: "paperclip.run_result.v1", reportedWorkDisposition: "done", summary: "Fixture turn completed.",
completionClaim: { contractRevision: "warm-pi-contract", objectiveSatisfied: true, criteria: [], remainingWork: [] },
evidence: [], verification: [], attentionRequests: [], artifacts: [],
},
});
if (command === "tool.resolve") event("runtime.turn_terminal", { status: "completed" });
}
let handling = Promise.resolve();
createInterface({ input: process.stdin }).on("line", (line) => {
handling = handling.then(() => handle(JSON.parse(line))).catch((error) => {
process.stderr.write(`${error.message}\n`);
process.exit(1);
});
});
@@ -6294,6 +6294,75 @@ it("rotates PRP authority in place for a warm cross-run attachment", async () =>
}
}, 30_000);
it("reopens Pi after a completed turn within its cold admission budget during warm attachment", async () => {
const root = await mkdtemp(join(tmpdir(), "runnerd-pi-warm-admission-"));
const cliRoot = join(root, "dist/cli");
await mkdir(cliRoot, { recursive: true });
const sidecarPath = join(cliRoot, "acpx-runtime-sidecar.cjs");
const journal = join(root, "commands.ndjson");
const fixture = await readFile(fileURLToPath(new URL("./fixtures/fake-pi-warm-sidecar.cjs", import.meta.url)), "utf8");
await writeFile(sidecarPath, fixture.replace("/* fixture-config */ null", JSON.stringify({ journal, resumeDelayMs: 32_000 })));
const digest = (path: string) => `sha256:${createHash("sha256").update(readFileSync(path)).digest("hex")}`;
const bundle = createCapabilityRunnerdCodexTransport({
provider: "acpx", acpxAgent: "pi", piThinkingLevel: "low", acpxPermissionMode: "deny-all",
runnerBinary: defaultCapabilityRunnerdBinary(), stateDirectory: root, runnerFilesystemRoot: root,
providerNodeCommand: process.execPath, providerNodeCommandSha256: digest(process.execPath),
acpxSidecarPath: sidecarPath, acpxSidecarSha256: digest(sidecarPath),
providerPackAuthorityDigest: `sha256:${"d".repeat(64)}`,
lifecyclePolicy: { mode: "warm", idleTimeoutMs: 60_000 },
environment: { PATH: "/usr/bin:/bin" },
});
bundle.transport.setServerRequestHandler(async () => ({
success: true,
contentItems: [{ type: "inputText", text: "Completion report accepted." }],
}));
let failure: unknown;
try {
await bundle.transport.request("initialize", {});
await bundle.transport.request("thread/start", {
cwd: root, model: "openrouter/deepseek/deepseek-v4-flash-0731",
dynamicTools: codexSemanticToolSpecs(),
completionContract: { revision: "warm-pi-contract", criterionIds: [] },
});
const runnerPid = bundle.evidence().runnerPid;
const notifications = bundle.transport.notifications()[Symbol.asyncIterator]();
const completeTurn = async () => {
await bundle.transport.request("turn/start", { input: [{ type: "text", text: "Complete the fixture turn." }] });
for (let index = 0; index < 64; index += 1) {
const next = await Promise.race([
notifications.next(),
new Promise<never>((_, reject) => setTimeout(() => reject(new Error("Fixture completion timed out")), 5_000)),
]);
if (next.value?.method === "turn/completed") return;
}
throw new Error("Fixture turn did not complete");
};
await completeTurn();
const started = Date.now();
await bundle.transport.attachRun!({ runId: "run-pi-warm-second", turnId: "turn-pi-warm-second", itemId: "item-pi-warm-second" });
expect(Date.now() - started).toBeGreaterThan(30_000);
await completeTurn();
expect(bundle.evidence()).toMatchObject({ runnerPid, runnerExited: false });
const commands = (await readFile(journal, "utf8")).trim().split("\n").map((line) => JSON.parse(line));
expect(commands.filter((entry) => entry.event === "spawn")).toHaveLength(2);
expect(commands.filter((entry) => entry.command === "session.open")).toHaveLength(2);
expect(commands.filter((entry) => entry.resumed)).toHaveLength(1);
expect(commands.filter((entry) => entry.command === "turn.start")).toHaveLength(2);
expect(commands.filter((entry) => entry.command === "session.close")).toHaveLength(1);
} catch (error) {
failure = error;
throw error;
} finally {
try {
await bundle.transport.close();
} catch (cleanupError) {
if (failure) throw new AggregateError([failure, cleanupError], "Warm attachment and strict cleanup failed");
throw cleanupError;
}
await rm(root, { recursive: true, force: true });
}
}, 75_000);
it("waits for a warm runner to re-authenticate before probing attachment readiness", async () => {
const stateDirectory = await mkdtemp(
join(tmpdir(), "runnerd-warm-reattach-before-probe-"),
@@ -4014,7 +4014,16 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
paperclipNextAuthority: { identity: desired, connection },
};
core.queueCommand("run.attach", payload, commandId, true);
await this.#waitCommand("run.attach", commandId);
// ACPX run attachment checkpoints the old sidecar and starts a fresh
// provider process. Pi must use the same absolute cold-admission budget
// as startup/recovery even though its Runner authority remains warm.
// Other providers retain the ordinary command bound.
await this.#waitCommand(
"run.attach",
commandId,
this.#coldAdmissionDeadline(),
true,
);
const attached = core.getCommand(commandId);
if (attached?.status !== "completed") {
throw new Error("native_runner_prp_run_rotation_failed");