fix(plugins): share one cached handoff runtime and use version 0.3.2

This commit is contained in:
kartik-mem0
2026-09-10 18:52:09 +05:30
parent 0fb924051f
commit 19a71bbacf
97 changed files with 946 additions and 11890 deletions
@@ -0,0 +1,10 @@
# Changelog
## 0.3.2
- Add explicit session handoff to a new Codex task. Host readers supply native active context to one common converter, validator, importer, and recovery implementation; memory extraction is not a transcript source.
- Installable plugins share one pinned runtime cache through a small launcher. First use downloads the exact GitHub commit and verifies SHA-256 digests; subsequent uses verify and reuse the local cache. No transcript is uploaded to GitHub.
- Preserve the session title, project, readable compaction context, supported images, and completed tool calls/results. Hidden reasoning and harness configuration are excluded. Unsupported records, opaque compaction, and unfinished responses fail explicitly.
- Requires Python 3.11+ and a signed-in local Codex CLI supporting external-session import. Pi additionally requires Node.js 22.19+ for its native SDK. Other source formats may require an explicit completed transcript, directory, and title.
- Handoff does not call Mem0. Large imports may invoke Codex’s native model compaction; smaller imports do not generate a summary. Failed creation saves a private recovery bundle under `~/.mem0/handoffs/`.
- Replace strict before-answer and repeated-search instructions with focused optional retrieval. Existing context can answer the question without another search. Search scoping, retrieval limits, and capture scheduling are unchanged.
+5 -3
View File
@@ -53,15 +53,17 @@ For installation, follow the host guides: [Claude Code](../../docs/integrations/
## Session handoff
`python/session_handoff.py` is the common entry point. `python/handoff_sources.py` reads native transcript formats; `python/claude_to_codex.py` retains the Claude parser and the single implementation of validation, private recovery, assets, context limits, and Codex import. It is adapted from [mem0ai/memo](https://github.com/mem0ai/memo/blob/aeeb1593284d1d2fca3b4bcf1e32ea10f71df549/docs/session-handoff.md).
`python/session_handoff.py` is the small common launcher. `python/handoff_sources.py` reads native transcript formats; `python/claude_to_codex.py` retains the Claude parser and the single implementation of validation, private recovery, assets, context limits, and Codex import. It is adapted from [mem0ai/memo](https://github.com/mem0ai/memo/blob/aeeb1593284d1d2fca3b4bcf1e32ea10f71df549/docs/session-handoff.md).
The TypeScript hosts read their native active context and use `typescript/src/handoff.ts` to normalize messages and pass a `mem0.session-handoff.v1` bundle to the same Python engine over stdin. OpenClaw supplies its trusted native transcript path. Native Python and portable plugins generate the shared `handoff` skill, with source-specific instructions.
The common bundle contains `source` (`host`, `session_id`, `title`, `cwd`, optional `path`), `items` (user/assistant messages, paired function calls/results, and supported images), and optional `warnings`. The engine derives counts and validates the bundle. Unknown model-visible content, missing tool results, and opaque compaction state fail rather than disappearing from the imported task. Handoff runs only on explicit user request, independently of memory hooks and the Mem0 API. Creation requires Python 3.11+ and a local Codex CLI with native session import support.
`build/handoff-runtime.json` declares the runtime files once. Every TypeScript build uses `build/package_handoff.mjs` to package them. Conformance checks reject missing or stale copies, as well as generated Python bundle drift. When the importer changes, regenerate the Python bundles and rebuild the TypeScript packages; no per-plugin importer edits are needed. A new host needs only a native context reader or adapter producing the common bundle.
The importer source exists only here. Installable packages contain the small launcher and `build/handoff-runtime.json`, which pins a Git commit and SHA-256 digests. On the first explicit handoff, the launcher downloads those exact two source files from GitHub into `~/.mem0/handoff-runtime/<revision>`. All ten plugins reuse that cache. Every use verifies the files; a valid cache works offline. A missing or invalid cache requires GitHub access, and download or digest failures stop the handoff. No transcript is sent to GitHub.
See the [handoff guide](../../docs/integrations/session-handoff.mdx) for host commands, verified formats, current-session limits, and recovery.
Every TypeScript build uses `build/package_handoff.mjs`; the Python builder uses the same manifest. Builds reject a pin whose digests differ from the canonical source, and conformance checks reject missing or stale launchers/manifests. To update the importer, commit its shared source, update the manifest to that immutable commit and its file digests, then regenerate the Python bundles and rebuild the TypeScript packages. No per-plugin importer edits or duplicated engine files are needed.
See the [0.3.2 changelog](CHANGELOG.md#032) and each plugin’s changelog for invocation details.
## Build and verify
@@ -2,6 +2,7 @@
from __future__ import annotations
import argparse
import hashlib
import json
import re
import shutil
@@ -18,6 +19,7 @@ CORE_ROOT = Path(__file__).resolve().parents[1]
REPOSITORY_ROOT = CORE_ROOT.parents[1]
INTEGRATIONS_ROOT = REPOSITORY_ROOT / "integrations"
SHARED_SKILLS = CORE_ROOT / "skills"
HANDOFF_MANIFEST = CORE_ROOT / "build" / "handoff-runtime.json"
PORTABLE_PLUGIN = "mem0-agent-plugin"
NATIVE_PLUGINS = {
"claude-code": INTEGRATIONS_ROOT / "claude-code-plugin",
@@ -116,7 +118,14 @@ def _bundle_python(
) -> None:
core = staged / "core"
core.mkdir()
handoff = json.loads(HANDOFF_MANIFEST.read_text(encoding="utf-8"))
for name, digest in handoff["files"].items():
if hashlib.sha256((CORE_ROOT / "python" / name).read_bytes()).hexdigest() != digest:
raise ValueError(f"Update the shared handoff runtime pin after changing {name}")
shutil.copy2(HANDOFF_MANIFEST, core / HANDOFF_MANIFEST.name)
for source in sorted((CORE_ROOT / "python").glob("*.py")):
if source.name in handoff["files"]:
continue
if portable and source.name in {"flush_worker.py", "hook_runner.py"}:
continue
shutil.copy2(source, core / source.name)
@@ -1,5 +1,8 @@
[
"claude_to_codex.py",
"session_handoff.py",
"handoff_sources.py"
]
{
"revision": "0fb924051fa876b5d6311fe3378794cc92fb6ff8",
"files": {
"claude_to_codex.py": "48b23958b890836f2c84b74de45a1f5a430c27dc238b80bf1a8fd67e4c7cd705",
"handoff_sources.py": "9ae209949473a215de8424004c7ea296e775e11743b5e7bfb77acc9630346a3b"
},
"artifacts": ["session_handoff.py", "handoff-runtime.json"]
}
@@ -1,12 +1,19 @@
import { copyFile, mkdir, readFile } from "node:fs/promises";
import { createHash } from "node:crypto";
import { copyFile, mkdir, readFile, rm } from "node:fs/promises";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
export async function packageHandoff(output = "dist") {
const files = JSON.parse(await readFile(new URL("./handoff-runtime.json", import.meta.url), "utf8"));
const manifest = JSON.parse(await readFile(new URL("./handoff-runtime.json", import.meta.url), "utf8"));
await mkdir(output, { recursive: true });
for (const name of files) {
await copyFile(new URL(`../python/${name}`, import.meta.url), resolve(output, name));
for (const [name, digest] of Object.entries(manifest.files)) {
const bytes = await readFile(new URL(`../python/${name}`, import.meta.url));
if (createHash("sha256").update(bytes).digest("hex") !== digest) throw new Error(`Update the shared handoff runtime pin after changing ${name}`);
}
for (const name of Object.keys(manifest.files)) await rm(resolve(output, name), { force: true });
for (const name of manifest.artifacts) {
const source = name.endsWith(".py") ? `../python/${name}` : `./${name}`;
await copyFile(new URL(source, import.meta.url), resolve(output, name));
}
}
@@ -21,7 +21,8 @@ TYPESCRIPT_ARTIFACTS = {
),
"deepseek": (INTEGRATIONS_ROOT / "deepseek-plugin", ("dist/index.js", "dist/index.d.ts")),
}
HANDOFF_RUNTIME_FILES = json.loads((CORE_ROOT / "build" / "handoff-runtime.json").read_text(encoding="utf-8"))
HANDOFF_MANIFEST = json.loads((CORE_ROOT / "build" / "handoff-runtime.json").read_text(encoding="utf-8"))
HANDOFF_RUNTIME_FILES = HANDOFF_MANIFEST["artifacts"]
TYPESCRIPT_ARTIFACTS = {
host: (package, (*required, *(f"dist/{name}" for name in HANDOFF_RUNTIME_FILES)))
for host, (package, required) in TYPESCRIPT_ARTIFACTS.items()
@@ -36,9 +37,10 @@ def verify_artifact(group: str, package: Path, required: tuple[str, ...]) -> dic
errors = [f"missing package artifact: {name}" for name in required if not (package / name).is_file()]
dist = package / "dist"
if group in TYPESCRIPT_ARTIFACTS:
errors.extend(f"duplicated handoff engine in package: dist/{name}" for name in HANDOFF_MANIFEST["files"] if (dist / name).exists())
for name in HANDOFF_RUNTIME_FILES:
artifact = dist / name
if artifact.is_file() and artifact.read_bytes() != (CORE_ROOT / "python" / name).read_bytes():
if artifact.is_file() and artifact.read_bytes() != (CORE_ROOT / ("python" if name.endswith(".py") else "build") / name).read_bytes():
errors.append(f"handoff runtime differs from shared source: dist/{name}")
for pattern in ("*.js", "*.mjs", "*.cjs", "*.d.ts"):
for artifact in dist.rglob(pattern) if dist.is_dir() else ():
@@ -29,7 +29,7 @@ from typing import Any, Iterable
import telemetry
DEFAULT_API_URL = "https://api.mem0.ai"
PLUGIN_VERSION = "0.4.0"
PLUGIN_VERSION = "0.3.2"
_harness_name: str = "generic"
_harness_env_prefix: str = "MEM0_PLUGIN"
@@ -1,7 +1,81 @@
#!/usr/bin/env python3
"""Import an explicit native session or neutral handoff bundle into Codex."""
"""Run the shared handoff engine locally or from its verified immutable cache."""
from __future__ import annotations
import hashlib
import json
import os
import re
import sys
import tempfile
from pathlib import Path
from urllib.request import urlopen
ENGINE_FILES = {"claude_to_codex.py", "handoff_sources.py"}
SOURCE_URL = "https://raw.githubusercontent.com/mem0ai/mem0"
def _verified(path: Path, digest: str) -> bool:
try:
return hashlib.sha256(path.read_bytes()).hexdigest() == digest
except FileNotFoundError:
return False
def runtime_root(launcher_dir: Path | None = None) -> Path:
here = launcher_dir or Path(__file__).resolve().parent
if all((here / name).is_file() for name in ENGINE_FILES):
return here # The canonical development checkout already has both engines.
manifest = json.loads((here / "handoff-runtime.json").read_text(encoding="utf-8"))
if not isinstance(manifest, dict):
raise ValueError("invalid handoff runtime manifest")
revision, files = manifest.get("revision"), manifest.get("files")
if not isinstance(revision, str) or not re.fullmatch(r"[0-9a-f]{40}", revision):
raise ValueError("handoff runtime revision must be an immutable commit SHA")
if (
not isinstance(files, dict)
or set(files) != ENGINE_FILES
or any(not isinstance(digest, str) or not re.fullmatch(r"[0-9a-f]{64}", digest) for digest in files.values())
):
raise ValueError("invalid handoff runtime file hashes")
cache = Path.home() / ".mem0" / "handoff-runtime" / revision
missing = {name: digest for name, digest in files.items() if not _verified(cache / name, digest)}
if not missing:
return cache
cache.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
with tempfile.TemporaryDirectory(prefix=f".{revision}-", dir=cache.parent) as temporary:
staged = Path(temporary)
for name, digest in missing.items():
url = f"{SOURCE_URL}/{revision}/integrations/agent-plugin-core/python/{name}"
try:
with urlopen(url, timeout=30) as response:
body = response.read()
except OSError as exc:
raise OSError(f"could not download pinned handoff runtime {name}: {exc}") from exc
if hashlib.sha256(body).hexdigest() != digest:
raise ValueError(f"SHA256 mismatch for pinned handoff runtime {name}; refusing to execute it")
(staged / name).write_bytes(body)
# All downloads are verified before publishing; each replacement is atomic.
cache.mkdir(exist_ok=True, mode=0o700)
for name in missing:
os.replace(staged / name, cache / name)
if not all(_verified(cache / name, digest) for name, digest in files.items()):
raise ValueError("handoff runtime cache changed during installation; refusing to execute it")
return cache
def main(argv: list[str] | None = None) -> int:
try:
root = runtime_root()
except (OSError, ValueError) as exc:
print(f"handoff runtime unavailable: {exc}", file=sys.stderr)
return 1
sys.path.insert(0, str(root))
from claude_to_codex import main as engine_main
return engine_main(argv, default_source=None)
from claude_to_codex import main
if __name__ == "__main__":
raise SystemExit(main(default_source=None))
raise SystemExit(main())
@@ -9,7 +9,9 @@ allowed-tools: Bash(python3 {{PLUGIN_ROOT}}/core/session_handoff.py *)
All hosts share one local import engine. Native readers and SDK adapters supply
complete conversation items; Mem0 memory capture is not a transcript source.
Requires Python 3.11+ and a Codex CLI with native session import support. The
Requires Python 3.11+ and a Codex CLI with native session import support.
First use downloads a pinned, hash-verified runtime from GitHub; later uses share
the verified local cache. No transcript is sent to GitHub. The
supported destination is Codex. This does not transfer files or change branches.
Visible conversation, tool history, and supported source compaction summaries
@@ -127,7 +127,9 @@ def test_handoff_is_bundled_with_host_appropriate_invocation(host: str, tmp_path
root = build(host, kind, tmp_path / host)
skill = (root / "skills" / "handoff" / "SKILL.md").read_text()
assert (root / "core" / "session_handoff.py").is_file()
assert (root / "core" / "handoff_sources.py").is_file()
assert (root / "core" / "handoff-runtime.json").is_file()
assert not (root / "core" / "handoff_sources.py").exists()
assert not (root / "core" / "claude_to_codex.py").exists()
assert "Only run on an explicit user request" in skill
assert "--target codex --create --command-output" in skill
if host == "claude-code":
@@ -115,9 +115,12 @@ def test_handoff_packaging_rejects_missing_and_drifted_runtime(tmp_path: Path) -
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text("", encoding="utf-8")
for name in HANDOFF_RUNTIME_FILES:
(dist / name).write_bytes((PLUGIN_ROOT / "python" / name).read_bytes())
(dist / name).write_bytes((PLUGIN_ROOT / ("python" if name.endswith(".py") else "build") / name).read_bytes())
assert verify_artifact("opencode", tmp_path, required)["status"] == "passed"
(dist / "claude_to_codex.py").write_text("# obsolete bundled engine\n", encoding="utf-8")
assert "duplicated handoff engine" in verify_artifact("opencode", tmp_path, required)["output"]
(dist / "claude_to_codex.py").unlink()
(dist / "session_handoff.py").write_text("# stale importer\n", encoding="utf-8")
assert "differs from shared source" in verify_artifact("opencode", tmp_path, required)["output"]
(dist / "session_handoff.py").unlink()
@@ -0,0 +1,169 @@
from __future__ import annotations
import hashlib
import importlib.util
import io
import json
import shutil
import subprocess
import sys
from pathlib import Path
from urllib.error import URLError
import pytest
CORE = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location("handoff_launcher", CORE / "python" / "session_handoff.py")
assert SPEC and SPEC.loader
launcher = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(launcher)
REVISION = "1" * 40
FILES = {
"claude_to_codex.py": b"def main(argv=None, default_source=None):\n return 0\n",
"handoff_sources.py": b"# reader\n",
}
def installation(tmp_path, monkeypatch):
plugin = tmp_path / "plugin"
plugin.mkdir()
manifest = {"revision": REVISION, "files": {name: hashlib.sha256(body).hexdigest() for name, body in FILES.items()}}
(plugin / "handoff-runtime.json").write_text(json.dumps(manifest))
monkeypatch.setattr(launcher.Path, "home", lambda: tmp_path)
requested = []
def download(url, timeout):
requested.append(url)
assert timeout == 30
return io.BytesIO(FILES[url.rsplit("/", 1)[-1]])
monkeypatch.setattr(launcher, "urlopen", download)
return plugin, tmp_path / ".mem0" / "handoff-runtime" / REVISION, requested
def test_standalone_download_is_pinned_then_works_offline(tmp_path, monkeypatch):
plugin, cache, requested = installation(tmp_path, monkeypatch)
assert launcher.runtime_root(plugin) == cache
assert set(requested) == {
f"https://raw.githubusercontent.com/mem0ai/mem0/{REVISION}/integrations/agent-plugin-core/python/{name}"
for name in FILES
}
assert {name: (cache / name).read_bytes() for name in FILES} == FILES
def offline(*args, **kwargs):
raise AssertionError("verified cached runtime must not access the network")
monkeypatch.setattr(launcher, "urlopen", offline)
assert launcher.runtime_root(plugin) == cache
def test_canonical_checkout_needs_no_manifest_cache_or_network(tmp_path, monkeypatch):
for name, body in FILES.items():
(tmp_path / name).write_bytes(body)
monkeypatch.setattr(launcher, "urlopen", lambda *args, **kwargs: pytest.fail("unexpected network"))
assert launcher.runtime_root(tmp_path) == tmp_path
assert not (tmp_path / ".mem0").exists()
@pytest.mark.parametrize("damage", ["tampered", "missing"])
def test_every_use_detects_and_repairs_damaged_cache(tmp_path, monkeypatch, damage):
plugin, cache, requested = installation(tmp_path, monkeypatch)
launcher.runtime_root(plugin)
target = cache / "handoff_sources.py"
if damage == "tampered":
target.write_bytes(b"untrusted code")
else:
target.unlink()
requested.clear()
assert launcher.runtime_root(plugin) == cache
assert target.read_bytes() == FILES[target.name]
assert len(requested) == 1
assert requested[0].endswith("/handoff_sources.py")
def test_failed_second_download_publishes_no_partial_runtime(tmp_path, monkeypatch):
plugin, cache, _ = installation(tmp_path, monkeypatch)
def download(url, timeout):
if url.endswith("handoff_sources.py"):
raise URLError("offline")
return io.BytesIO(FILES["claude_to_codex.py"])
monkeypatch.setattr(launcher, "urlopen", download)
with pytest.raises(OSError, match="could not download pinned handoff runtime handoff_sources.py"):
launcher.runtime_root(plugin)
assert not cache.exists()
assert list(cache.parent.iterdir()) == []
def test_hash_mismatch_never_replaces_existing_cache(tmp_path, monkeypatch):
plugin, cache, _ = installation(tmp_path, monkeypatch)
launcher.runtime_root(plugin)
(cache / "handoff_sources.py").write_bytes(b"tampered")
monkeypatch.setattr(launcher, "urlopen", lambda *args, **kwargs: io.BytesIO(b"wrong response"))
with pytest.raises(ValueError, match="SHA256 mismatch"):
launcher.runtime_root(plugin)
assert (cache / "claude_to_codex.py").read_bytes() == FILES["claude_to_codex.py"]
assert (cache / "handoff_sources.py").read_bytes() == b"tampered"
assert list(cache.parent.iterdir()) == [cache]
def test_tampered_cache_cannot_run_offline(tmp_path, monkeypatch, capsys):
plugin, cache, _ = installation(tmp_path, monkeypatch)
launcher.runtime_root(plugin)
(cache / "claude_to_codex.py").write_bytes(b"untrusted code")
def offline(*args, **kwargs):
raise URLError("offline")
monkeypatch.setattr(launcher, "urlopen", offline)
resolve = launcher.runtime_root
monkeypatch.setattr(launcher, "runtime_root", lambda: resolve(plugin))
assert launcher.main(["--bundle", "-"]) == 1
assert "handoff runtime unavailable" in capsys.readouterr().err
@pytest.mark.parametrize(
"manifest",
[
{"revision": "main", "files": {}},
{"revision": REVISION, "files": {"../../outside.py": "a" * 64}},
{"revision": REVISION, "files": {name: "not-a-hash" for name in FILES}},
[],
],
)
def test_manifest_rejects_unpinned_or_unexpected_files(tmp_path, monkeypatch, manifest):
plugin, _, requested = installation(tmp_path, monkeypatch)
(plugin / "handoff-runtime.json").write_text(json.dumps(manifest))
with pytest.raises(ValueError):
launcher.runtime_root(plugin)
assert requested == []
def test_missing_manifest_returns_actionable_error(tmp_path, monkeypatch, capsys):
resolve = launcher.runtime_root
monkeypatch.setattr(launcher, "runtime_root", lambda: resolve(tmp_path))
assert launcher.main([]) == 1
assert "handoff-runtime.json" in capsys.readouterr().err
def test_launcher_executes_adjacent_engine_with_generic_cli_contract(tmp_path):
shutil.copy2(CORE / "python" / "session_handoff.py", tmp_path / "session_handoff.py")
(tmp_path / "handoff_sources.py").write_text("# reader\n")
(tmp_path / "claude_to_codex.py").write_text(
"import sys\ndef main(argv=None, default_source='unexpected'):\n"
" assert default_source is None\n assert sys.argv[1:] == ['--bundle', '-']\n return 7\n"
)
result = subprocess.run(
[sys.executable, str(tmp_path / "session_handoff.py"), "--bundle", "-"], capture_output=True
)
assert result.returncode == 7, result.stderr.decode()
def test_manifest_declares_only_launcher_and_hashes_pinned_engines():
manifest = json.loads((CORE / "build" / "handoff-runtime.json").read_text())
assert manifest["artifacts"] == ["session_handoff.py", "handoff-runtime.json"]
assert manifest["revision"] == "0fb924051fa876b5d6311fe3378794cc92fb6ff8"
assert set(manifest["files"]) == launcher.ENGINE_FILES
for name, digest in manifest["files"].items():
assert digest == hashlib.sha256((CORE / "python" / name).read_bytes()).hexdigest()
@@ -129,9 +129,3 @@ export function runHandoff(scriptUrl: URL, bundle: HandoffBundle): Promise<strin
export function runNativeSession(scriptUrl: URL, host: string, session: string): Promise<string> {
return run(scriptUrl, [`--source=${required(host, "Source host")}`, `--session=${required(session, "Native session path")}`]);
}
/** Explicit Claude transcript fallback for legacy callers. */
export async function runClaudeToCodex(scriptUrl: URL, session: string, cwd?: string): Promise<string> {
const args = [`--session=${required(session, "A Claude session ID or JSONL path")}`];
if (cwd !== undefined) args.push(`--cwd=${required(cwd, "Existing project directory")}`);
return run(scriptUrl, args);
}
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { test } from "node:test";
import { buildHandoffBundle, runClaudeToCodex, runHandoff } from "../src/handoff.ts";
import { buildHandoffBundle, runNativeSession, runHandoff } from "../src/handoff.ts";
const source = {host: "test", session_id: "native", title: "Native title", cwd: "/tmp"};
test("native context preserves summary, full tools/images, excludes only triggering call, rejects loss", async () => {
@@ -31,15 +31,15 @@ test("native context preserves summary, full tools/images, excludes only trigger
assert.deepEqual((deepseek.items[2].output as any[])[0], {type: "text", text: "[Tool error]"});
});
test("transport sends native bundle on stdin and legacy arguments literally", async () => {
test("transport sends native bundle on stdin and native arguments literally", async () => {
const dir = await mkdtemp(join(tmpdir(), "mem0-handoff-"));
const script = join(dir, "importer.py");
try {
await writeFile(script, "import json, sys\nprint(json.dumps(sys.argv[1:]))\n");
const session = "--session with spaces; $(touch should-never-exist)";
const args = JSON.parse(await runClaudeToCodex(pathToFileURL(script), session, dir));
assert.deepEqual(args, [`--session=${session}`, `--cwd=${dir}`, "--create", "--command-output", "--target", "codex"]);
await assert.rejects(runClaudeToCodex(pathToFileURL(script), " "), /session ID/);
const args = JSON.parse(await runNativeSession(pathToFileURL(script), "openclaw", session));
assert.deepEqual(args, ["--source=openclaw", `--session=${session}`, "--create", "--command-output", "--target", "codex"]);
assert.throws(() => runNativeSession(pathToFileURL(script), "openclaw", " "), /session path/);
await writeFile(script, "import json, sys\nprint(json.dumps(json.load(sys.stdin)))\n");
const bundle = await buildHandoffBundle(source, [{role: "user", content: "private session"}]);
assert.deepEqual(JSON.parse(await runHandoff(pathToFileURL(script), bundle)), bundle);