Clarify current-turn agent home and verify Pi resume environment

This commit is contained in:
Dotta committed 2026-10-02 09:34:16 -05:00
1 parent bba63f5120
commit beddbab87d
3 files changed
+88 -2

No files matched your search

@@ -89,6 +89,22 @@ describe("NativeExecutionInputV1", () => {
expect(composeNativeSystemInstructions(parsed.runtimeContext, "Follow sibling.md")).toBe(
`${PAPERCLIP_EXECUTION_PROMPT}\n\nFollow sibling.md\n\nRead-only instruction sibling root: /runtime/instructions`,
);
const agentFilesContext = {
...parsed.runtimeContext,
instructions: {
...parsed.runtimeContext.instructions,
workingCopy: { kind: "agent_files" as const, rootPath: "/runtime/current-agent-copy", entryPath: "AGENTS.md" },
},
};
const agentFilesInstructions = composeNativeSystemInstructions(agentFilesContext, "Preserve my personal notes.");
expect(agentFilesInstructions).toContain("AGENT_HOME) is /runtime/current-agent-copy.");
expect(agentFilesInstructions).toContain("its absolute path may change between turns");
expect(agentFilesInstructions).toContain("use the current $AGENT_HOME environment variable instead of an absolute agent-directory path from an earlier turn");
expect(agentFilesInstructions).toContain("All supported files and subfolders there are restored across tasks and sessions");
expect(agentFilesInstructions).toContain("Write task deliverables in the task working directory");
expect(agentFilesInstructions).toContain("Preserve my personal notes.");
expect(agentFilesInstructions).toMatch(/Read-only instruction sibling root: \/runtime\/instructions$/);
expect(composeNativeSystemInstructions(parsed.runtimeContext, "Follow sibling.md")).not.toContain("$AGENT_HOME");
expect(canonicalNativeRuntimeContextDigest({
...context,
mcp: { ...context.mcp, bindingId: "native-mcp:run-2" },
@@ -158,7 +158,7 @@ export function composeNativeSystemInstructions(context: NativeRuntimeContextSna
context.prompt.text,
entryContent.trim(),
context.instructions.workingCopy?.kind === "agent_files"
? `Your persistent agent directory (AGENT_HOME) is ${context.instructions.workingCopy.rootPath}. Your instruction entry is ${context.instructions.workingCopy.entryPath}, relative to that directory. All supported files and subfolders there are restored across tasks and sessions, and collected after this provider stops. Write task deliverables in the task working directory. Only changed or deleted files synchronize; the last sync wins for the same file. Temporary copies are cleaned up without retaining file history. Check the save receipt before claiming persistence.`
? `Your persistent agent directory (AGENT_HOME) is ${context.instructions.workingCopy.rootPath}. This is the current turn's copy; its absolute path may change between turns. In shell commands, use the current $AGENT_HOME environment variable instead of an absolute agent-directory path from an earlier turn. Your instruction entry is ${context.instructions.workingCopy.entryPath}, relative to that directory. All supported files and subfolders there are restored across tasks and sessions, and collected after this provider stops. Write task deliverables in the task working directory. Only changed or deleted files synchronize; the last sync wins for the same file. Temporary copies are cleaned up without retaining file history. Check the save receipt before claiming persistence.`
: context.instructions.workingCopy
? `Your editable agent instruction file is ${context.instructions.workingCopy.rootPath}/${context.instructions.workingCopy.entryPath}. Edit this registered private copy normally. After this run stops, Paperclip saves changed content as a persistent revision if your responsible user still has permission and the baseline has not changed. Check the run's instruction-save receipt before claiming persistence. Conflicts are preserved for explicit resolution. Repository instruction files, skills, and this run's loaded prompt are separate and are not collected.`
: null,
@@ -1,8 +1,10 @@
import { execFileSync } from "node:child_process";
import { createPiLaunchSpec } from "./pi-acp-runtime.js";
import { createCopilotToolEvidence } from "./copilot-tool-evidence.js";
import { readNativeSemanticReceipt, type SemanticToolResult } from "../semantic-tool-receipt.js";
import { validateAcpxRichEvent } from "./profile-extensions.js";
import type { CanonicalProviderEvent } from "../../provider-events.js";
import { mkdir, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
import { mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
@@ -919,6 +921,74 @@ describe("ACPX runtime host", () => {
}
});
it("reopens Pi with the current registered home in a resumed child shell after prior copies are removed", async () => {
const fixture = await hostFixture();
const root = await realpath(fixture.root);
const copies = await realpath(await mkdtemp(join(tmpdir(), "paperclip-pi-home-probe-")));
temporaryDirectories.push(copies);
const sessionHome = join(root, "pi-session-home");
await mkdir(join(sessionHome, "sessions"), { recursive: true });
const sessionPath = join(sessionHome, "sessions", "retained.jsonl");
await writeFile(sessionPath, "retained provider session");
const entrypoint = join(root, "inspect-launch.cjs");
const extension = join(root, "extension.js");
await writeFile(extension, "");
// The child replaces only the provider/model boundary. Its shell inherits
// the actual production launch spec, including the registered AGENT_HOME.
await writeFile(entrypoint, `
const { execFileSync } = require("node:child_process");
const shell = execFileSync("/bin/bash", ["--noprofile", "--norc", "-c",
'printf "%s\\n" "$AGENT_HOME"; cat "$AGENT_HOME/notes/warm-memory.txt"'],
{ encoding: "utf8", env: process.env });
console.log(JSON.stringify({ shell,
configuration: JSON.parse(process.env.PAPERCLIP_PI_RUNTIME_CONFIGURATION),
arguments: process.argv.slice(2) }));
`);
const model = "openrouter/deepseek/deepseek-v4-flash-0731";
const observed: Array<{ shell: string; configuration: { agentHome: string; instructions: string }; arguments: string[] }> = [];
const dependencies = fixture.dependencies({ openRuntime: async launch => {
const spec = createPiLaunchSpec({ cwd: fixture.options.workingDirectory, sessionPath }, {
...launch.launchEnvironment,
PAPERCLIP_PI_NODE_EXECUTABLE: process.execPath,
PAPERCLIP_PI_ENTRYPOINT: entrypoint,
PAPERCLIP_PI_EXTENSION_PATH: extension,
PI_CODING_AGENT_DIR: sessionHome,
});
observed.push(JSON.parse(execFileSync(spec.command, spec.args, {
cwd: fixture.options.workingDirectory, env: spec.env, encoding: "utf8", timeout: 5_000,
})));
return runtimePort({
getStatus: async () => ({ models: { currentModelId: model } }),
identity: async () => ({ acpxRecordId: "same-record", backendSessionId: "same-session", agentSessionId: "same-session", piThinkingLevel: "low" }),
});
} });
let priorHome = "/unregistered-ambient-home";
for (const turn of [1, 2, 3]) {
const agentHome = join(copies, `turn-${turn}`);
await mkdir(join(agentHome, "notes"), { recursive: true });
await writeFile(join(agentHome, "notes", "warm-memory.txt"), `T${turn}\n`);
const instructions = `For this turn, AGENT_HOME is ${agentHome}.`;
const runtimeContext = { instructions: { workingCopy: { kind: "agent_files", rootPath: agentHome, entryPath: "AGENTS.md" } }, skills: [], mcp: { bindingId: null } } as unknown as NativeRuntimeContextSnapshot;
const host = await AcpxRuntimeHost.open({
...fixture.options, agent: "pi", model, piThinkingLevel: "low", permissionMode: "approve-all",
providerPolicy: { readOnly: false }, runtimeContext, systemInstructions: instructions,
environment: { OPENROUTER_API_KEY: "test-only", AGENT_HOME: priorHome, PAPERCLIP_PI_AGENT_HOME: priorHome },
}, dependencies);
try {
expect(observed.at(-1)).toMatchObject({
shell: `${agentHome}\nT${turn}\n`, configuration: { agentHome, instructions },
arguments: expect.arrayContaining(["--session", sessionPath]),
});
} finally {
await host.close({ reason: "credential-free home probe complete" });
}
await rm(agentHome, { recursive: true });
priorHome = agentHome;
}
expect(observed).toHaveLength(3);
expect(await readFile(sessionPath, "utf8")).toBe("retained provider session");
});
it("selects and verifies Claude's qualified reported model", async () => {
const fixture = await hostFixture();
let selected = false;