fix(pi): preserve history cleanup across current resume behavior

Keep the original session identity and missing-folder cleanup when releasing
history-only runtimes. Use an existing directory in the regression so the
newer resume fallback does not try to copy a nonexistent session fixture.
Cover interrupted and failed replay, interactive residency, and automatic
cleanup of a missing-folder history copy.

Co-authored-by: Ryan Winograd <ryan@thewinograds.com>
This commit is contained in:
paseo-bot[bot]
2026-11-02 08:03:40 +00:00
co-authored by Ryan Winograd
parent beda7180c0
commit 5038999e81
3 changed files with 60 additions and 4 deletions
@@ -1615,13 +1615,14 @@ describe("PiRpcAgentSession", () => {
provider: "pi",
sessionId: "pi-session-1",
nativeHandle: "/tmp/native-pi-session",
metadata: { cwd: "/workspace/project" },
metadata: { cwd: tmpdir() },
},
{},
undefined,
{ purpose: "history" },
);
onTestFinished(() => session.close());
const history: AgentStreamEvent[] = [];
for await (const event of session.streamHistory()) history.push(event);
@@ -1640,6 +1641,57 @@ describe("PiRpcAgentSession", () => {
expect(pi.latestSession().closeCalls).toBe(1);
});
test.each(["stopped", "failed"] as const)(
"closes a history-only Pi runtime when replay is %s",
async (outcome) => {
const pi = new FakePi();
pi.queueSessionSetup((runtimeSession) => {
runtimeSession.messages = [
{ role: "user", content: "Old question" },
{ role: "assistant", content: [{ type: "text", text: "Old answer" }] },
];
if (outcome === "failed")
runtimeSession.getMessagesError = new Error("History unavailable");
});
const session = await createClient(pi).resumeSession(
{
provider: "pi",
sessionId: "pi-session-1",
nativeHandle: "/tmp/native-pi-session",
metadata: { cwd: tmpdir() },
},
{},
undefined,
{ purpose: "history" },
);
onTestFinished(() => session.close());
const history = session.streamHistory();
if (outcome === "failed") {
await expect(history.next()).rejects.toThrow("History unavailable");
} else {
expect((await history.next()).done).toBe(false);
await history.return();
}
expect(pi.latestSession().closeCalls).toBe(1);
},
);
test("keeps an interactive Pi runtime open after replaying history", async () => {
const pi = new FakePi();
const session = await createClient(pi).resumeSession({
provider: "pi",
sessionId: "pi-session-1",
nativeHandle: "/tmp/native-pi-session",
metadata: { cwd: tmpdir() },
});
onTestFinished(() => session.close());
for await (const _event of session.streamHistory()) {
// Hydrate the session before its next interactive prompt.
}
expect(pi.latestSession().closeCalls).toBe(0);
await expect(session.getRuntimeInfo()).resolves.toMatchObject({ provider: "pi" });
});
test("resumes a session whose model was removed on the model Pi falls back to", async () => {
const pi = new FakePi();
pi.removeModel("9router/deepseek-v4-flash");
@@ -3780,14 +3832,17 @@ readline.createInterface({ input: process.stdin }).on("line", (line) => {
},
);
test("a history resume of an agent whose folder is gone removes its session copy on close and leaves the original unchanged", async () => {
test("a history resume of an agent whose folder is gone removes its session copy after replay and leaves the original unchanged", async () => {
const archived = writeArchivedSession(createDeletedFolder());
const pi = new FakePi();
const session = await resumeArchived(createClient(pi), archived, "history");
const sessionCopy = pi.recordedLaunches[0]!.session!;
expect(existsSync(sessionCopy)).toBe(true);
await session.close();
for await (const _event of session.streamHistory()) {
// Completion releases both the runtime and its temporary session copy.
}
expect(pi.latestSession().closeCalls).toBe(1);
expect(existsSync(path.dirname(sessionCopy))).toBe(false);
expect(readFileSync(archived.sessionFile)).toEqual(archived.originalBytes);
@@ -26,7 +26,6 @@ import {
type AgentRunOptions,
type AgentRunResult,
type AgentRuntimeInfo,
type AgentResumeSessionOptions,
type AgentSelectOption,
type AgentSession,
type AgentSessionConfig,
@@ -162,6 +162,7 @@ export class FakePiSession implements PiRuntimeSession {
setModelResult: PiModel | null = null;
models: PiModel[] = [];
messages: PiAgentMessage[] = [];
getMessagesError: Error | null = null;
stats: PiSessionStats = {
tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
cost: 0,
@@ -304,6 +305,7 @@ export class FakePiSession implements PiRuntimeSession {
}
async getMessages(): Promise<PiAgentMessage[]> {
if (this.getMessagesError) throw this.getMessagesError;
return this.messages;
}