From 0fb924051fa876b5d6311fe3378794cc92fb6ff8 Mon Sep 17 00:00:00 2001 From: kartik-mem0 Date: Thu, 10 Sep 2026 18:28:55 +0530 Subject: [PATCH] fix(pi-plugin): load native handoff SDK only on supported runtimes --- docs/integrations/session-handoff.mdx | 1 + integrations/pi-agent-plugin/README.md | 2 ++ .../src/handoff.native.test.ts | 21 ++++++++++++++++++ .../pi-agent-plugin/src/handoff.test.ts | 22 +++++++++++++++++-- integrations/pi-agent-plugin/src/handoff.ts | 7 +++++- 5 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 integrations/pi-agent-plugin/src/handoff.native.test.ts diff --git a/docs/integrations/session-handoff.mdx b/docs/integrations/session-handoff.mdx index fb85e5161..4e745a895 100644 --- a/docs/integrations/session-handoff.mdx +++ b/docs/integrations/session-handoff.mdx @@ -13,6 +13,7 @@ All ten Mem0 plugin packages use the same handoff engine. Host adapters read nat - Python 3.11 or newer available as `python3`. - Codex CLI installed, signed in, and available as `codex` on the same machine. Its app-server must support native external-session import and thread compaction. Open Codex once so its configuration and model metadata exist. - The source conversation and its project directory available locally. +- Pi handoff requires Node.js 22.19 or newer, matching the native Pi SDK requirement. The handoff runtime does not call Mem0 or use its API key. Memory features and host installation may still require their normal configuration. Handoff is separate from automatic memory capture. diff --git a/integrations/pi-agent-plugin/README.md b/integrations/pi-agent-plugin/README.md index 7fcfc5684..7f023434d 100644 --- a/integrations/pi-agent-plugin/README.md +++ b/integrations/pi-agent-plugin/README.md @@ -56,6 +56,8 @@ Environment variables (`MEM0_API_KEY`, `MEM0_USER_ID`) override the config file. Run `/mem0-handoff codex` to continue the current Pi session in Codex. The plugin reads native session context, including readable compaction summaries and completed tool outcomes. +Handoff requires **Node.js 22.19+**, matching the native Pi SDK. The SDK loads only when handoff is invoked; existing memory features remain available on Node.js 20. + Requires **Python 3.11+** available as `python3` and an installed, signed-in Codex CLI with native session import support. Handoff works independently of Mem0 credentials and never sends the transcript through the Mem0 API. Unsupported content, missing results, and unrelated unfinished calls fail explicitly. The shared engine is packaged in `dist/` by [agent-plugin-core](../agent-plugin-core/README.md); it is not maintained separately in this plugin. Failed imports save a private recovery bundle under `~/.mem0/handoffs/`. See the [session handoff guide](../../docs/integrations/session-handoff.mdx) for supported formats, privacy, and recovery. diff --git a/integrations/pi-agent-plugin/src/handoff.native.test.ts b/integrations/pi-agent-plugin/src/handoff.native.test.ts new file mode 100644 index 000000000..244c4f364 --- /dev/null +++ b/integrations/pi-agent-plugin/src/handoff.native.test.ts @@ -0,0 +1,21 @@ +import {expect, it} from "vitest"; + +const [major, minor] = process.versions.node.split(".").map(Number); +// The real Pi 0.79 SDK declares Node >=22.19; Node 20 tests cover startup and the explicit guard. +it.skipIf(major < 22 || (major === 22 && minor < 19))("native Pi resolves the selected leaf and readable compaction context", async () => { + const {buildSessionContext, convertToLlm} = await import("@earendil-works/pi-coding-agent"); + const entries = [ + {type: "message", id: "old", parentId: null, message: {role: "user", content: "old context"}}, + {type: "message", id: "keep", parentId: "old", message: {role: "user", content: "retained tail"}}, + {type: "compaction", id: "compact", parentId: "keep", summary: "Native summary", firstKeptEntryId: "keep", tokensBefore: 100}, + {type: "message", id: "new", parentId: "compact", message: {role: "user", content: "Continue this task"}}, + {type: "message", id: "other", parentId: "old", message: {role: "user", content: "abandoned branch"}}, + ]; + const context = buildSessionContext(entries as Parameters[0], "new"); + const messages = JSON.stringify(convertToLlm(context.messages)); + expect(messages).toContain("Native summary"); + expect(messages).toContain("retained tail"); + expect(messages).toContain("Continue this task"); + expect(messages).not.toContain("old context"); + expect(messages).not.toContain("abandoned branch"); +}); diff --git a/integrations/pi-agent-plugin/src/handoff.test.ts b/integrations/pi-agent-plugin/src/handoff.test.ts index d51ff67aa..1d0644f27 100644 --- a/integrations/pi-agent-plugin/src/handoff.test.ts +++ b/integrations/pi-agent-plugin/src/handoff.test.ts @@ -1,10 +1,18 @@ import {afterEach, beforeEach, describe, expect, it, vi} from "vitest"; import mem0Extension from "./entry.ts"; +import {buildSessionContext, convertToLlm} from "@earendil-works/pi-coding-agent"; import {runHandoff} from "../../agent-plugin-core/typescript/src/handoff.ts"; vi.mock("../../agent-plugin-core/typescript/src/handoff.ts", async (original) => ({...await original(), runHandoff: vi.fn()})); +vi.mock("@earendil-works/pi-coding-agent", () => ({ + buildSessionContext: vi.fn(() => ({messages: [{role: "user", content: "Native summary and retained tail"}]})), + convertToLlm: vi.fn(messages => messages), +})); vi.mock("./config/index.ts", () => ({loadConfig: () => ({apiKey: ""}), CONFIG_DIR: "/tmp/mem0-pi-handoff-tests"})); -beforeEach(() => { vi.clearAllMocks(); }); -afterEach(() => vi.restoreAllMocks()); +beforeEach(() => { + vi.clearAllMocks(); + vi.stubGlobal("process", {...process, versions: {...process.versions, node: "22.19.0"}}); +}); +afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); }); function setup() { const commands = new Map(); const pi = {registerCommand: vi.fn((name, command) => commands.set(name, command)), sendMessage: vi.fn()}; @@ -23,10 +31,20 @@ function setup() { return {pi, ctx, handler: commands.get("mem0-handoff").handler}; } describe("native Pi handoff", () => { + it("keeps startup working on Node 20 and explains the native handoff runtime requirement", async () => { + vi.stubGlobal("process", {...process, versions: {...process.versions, node: "20.20.2"}}); + const {ctx, handler} = setup(); + await handler("codex", ctx); + expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("Node.js 22.19+"), "error"); + expect(buildSessionContext).not.toHaveBeenCalled(); + expect(runHandoff).not.toHaveBeenCalled(); + }); it("uses host compaction/branch selection and the current session without a Mem0 key", async () => { const {pi, ctx, handler} = setup(); vi.mocked(runHandoff).mockResolvedValue("Created Codex task"); await handler("codex", ctx); + expect(buildSessionContext).toHaveBeenCalledWith(ctx.sessionManager.getEntries(), "new"); + expect(convertToLlm).toHaveBeenCalledOnce(); const bundle = vi.mocked(runHandoff).mock.calls[0][1]; expect(bundle.source).toMatchObject({host: "pi-agent", session_id: "pi-native", title: "My Pi task"}); expect(JSON.stringify(bundle.items)).toContain("Native summary"); diff --git a/integrations/pi-agent-plugin/src/handoff.ts b/integrations/pi-agent-plugin/src/handoff.ts index 7369b7969..29a45dc79 100644 --- a/integrations/pi-agent-plugin/src/handoff.ts +++ b/integrations/pi-agent-plugin/src/handoff.ts @@ -1,4 +1,4 @@ -import { buildSessionContext, convertToLlm, type ExtensionAPI } from "@earendil-works/pi-coding-agent"; +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { buildHandoffBundle, runHandoff } from "../../agent-plugin-core/typescript/src/handoff.ts"; export function registerHandoffCommand(pi: ExtensionAPI): void { @@ -11,6 +11,11 @@ export function registerHandoffCommand(pi: ExtensionAPI): void { } try { if (!ctx.isIdle()) throw new Error("Finish the current response before handing off this session."); + const [major, minor] = process.versions.node.split(".").map(Number); + if (major < 22 || (major === 22 && minor < 19)) { + throw new Error("Pi session handoff requires Node.js 22.19+ (the native Pi SDK requirement). Memory features remain available."); + } + const { buildSessionContext, convertToLlm } = await import("@earendil-works/pi-coding-agent"); const session = ctx.sessionManager; const context = buildSessionContext(session.getEntries(), session.getLeafId()); const bundle = await buildHandoffBundle({