From 02e5ab13cdd2dab5c5bc5cade8fbb472482214a6 Mon Sep 17 00:00:00 2001 From: Khaled Mansour Date: Wed, 8 Jul 2026 20:34:18 -0400 Subject: [PATCH] feat(pi-acp): opt-in persistent sessions via PI_SESSION_DIR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Pi ACP adapter hardcodes `SessionManager.inMemory(params.cwd)` for every new session, so nothing is written to disk and the whole conversation is lost when the adapter process restarts (e.g. an actor sleep/eviction). Pi already ships `SessionManager.continueRecent(cwd, sessionDir)` — persist + resume the most recent session — but there's no way for an embedder to select it without patching the adapter source. Add `resolveSessionManager()`: when the embedder provides a session directory via the `PI_SESSION_DIR` env var, use `continueRecent` (persist under that dir and resume); otherwise keep the current in-memory behavior. Env-driven and additive — no behavior change unless `PI_SESSION_DIR` is set (same pattern as the `SECURE_EXEC_FRAME_TIMEOUT_MS` override in #1641). The helper is exported and unit tested with a fake SessionManager (the real newSession path needs the Pi SDK). Co-authored-by: Khaled Mansour --- registry/agent/pi/src/adapter.ts | 26 +++++++++- .../agent/pi/tests/session-manager.test.mjs | 47 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 registry/agent/pi/tests/session-manager.test.mjs diff --git a/registry/agent/pi/src/adapter.ts b/registry/agent/pi/src/adapter.ts index 6fb8a3bf6d..1eea462bdb 100644 --- a/registry/agent/pi/src/adapter.ts +++ b/registry/agent/pi/src/adapter.ts @@ -103,8 +103,32 @@ Object.defineProperty(process, "stdin", { type SessionManagerLike = { inMemory(cwd?: string): unknown; + continueRecent(cwd: string, sessionDir: string): unknown; }; +/** + * Choose the Pi `SessionManager` for a new session. + * + * By default sessions are in-memory (nothing is written to disk), so a + * conversation is lost when the adapter process restarts. When the embedder + * provides a session directory via the `PI_SESSION_DIR` env var, use pi's + * `continueRecent` instead: it persists the session's `.jsonl` under that + * directory and resumes the most recent one, so conversations survive an adapter + * restart. No behavior change unless `PI_SESSION_DIR` is set. + * + * Exported for unit testing (the real `newSession` path needs the Pi SDK). + */ +export function resolveSessionManager( + SessionManager: SessionManagerLike, + cwd: string, + env: Record = process.env, +): unknown { + const sessionDir = env.PI_SESSION_DIR?.trim(); + return sessionDir + ? SessionManager.continueRecent(cwd, sessionDir) + : SessionManager.inMemory(cwd); +} + type ModelLike = { id: string; provider: string; @@ -873,7 +897,7 @@ export class PiSdkAgent implements Agent { const { session } = await __trace.span("createAgentSession", () => createAgentSession({ cwd: params.cwd, - sessionManager: SessionManager.inMemory(params.cwd), + sessionManager: resolveSessionManager(SessionManager, params.cwd), resourceLoader, tools: this.wrapTools( createCodingTools(params.cwd, { diff --git a/registry/agent/pi/tests/session-manager.test.mjs b/registry/agent/pi/tests/session-manager.test.mjs new file mode 100644 index 0000000000..2dad083450 --- /dev/null +++ b/registry/agent/pi/tests/session-manager.test.mjs @@ -0,0 +1,47 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { resolve as resolvePath } from "node:path"; + +// Unit tests for resolveSessionManager. The real newSession path needs the Pi +// SDK, so we test the SessionManager selection directly with a fake. +const packageDir = resolvePath(import.meta.dirname, ".."); +const { resolveSessionManager } = await import( + resolvePath(packageDir, "dist", "adapter.js") +); + +function fakeSessionManager() { + const calls = []; + return { + calls, + inMemory(cwd) { + calls.push(["inMemory", cwd]); + return { kind: "inMemory", cwd }; + }, + continueRecent(cwd, dir) { + calls.push(["continueRecent", cwd, dir]); + return { kind: "continueRecent", cwd, dir }; + }, + }; +} + +test("PI_SESSION_DIR persists + resumes via continueRecent", () => { + const sm = fakeSessionManager(); + const result = resolveSessionManager(sm, "/workspace", { + PI_SESSION_DIR: "/sessions/a/main", + }); + assert.deepEqual(sm.calls, [["continueRecent", "/workspace", "/sessions/a/main"]]); + assert.equal(result.kind, "continueRecent"); +}); + +test("no PI_SESSION_DIR falls back to in-memory (default, unchanged)", () => { + const sm = fakeSessionManager(); + const result = resolveSessionManager(sm, "/workspace", {}); + assert.deepEqual(sm.calls, [["inMemory", "/workspace"]]); + assert.equal(result.kind, "inMemory"); +}); + +test("blank/whitespace PI_SESSION_DIR is ignored", () => { + const sm = fakeSessionManager(); + resolveSessionManager(sm, "/workspace", { PI_SESSION_DIR: " " }); + assert.deepEqual(sm.calls, [["inMemory", "/workspace"]]); +});