fix(pi-plugin): load native handoff SDK only on supported runtimes

This commit is contained in:
kartik-mem0
2026-09-10 18:28:55 +05:30
parent 82c32f993e
commit 0fb924051f
5 changed files with 50 additions and 3 deletions
+1
View File
@@ -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.
+2
View File
@@ -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.
@@ -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<typeof buildSessionContext>[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");
});
@@ -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<typeof import("../../agent-plugin-core/typescript/src/handoff.ts")>(), 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<string, any>();
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");
+6 -1
View File
@@ -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({