diff --git a/integrations/agent-plugin-core/CHANGELOG.md b/integrations/agent-plugin-core/CHANGELOG.md index 91d22cb9e..b47e5e51c 100644 --- a/integrations/agent-plugin-core/CHANGELOG.md +++ b/integrations/agent-plugin-core/CHANGELOG.md @@ -7,4 +7,5 @@ - 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+. Pi save additionally requires Node.js 22.19+ for its native SDK; list/resume work on Node.js 20. Other source formats may require an explicit completed transcript, directory, and title. - Handoff needs no destination CLI, model call, or Mem0 credentials. Resources are private, uniquely named files; saving preserves full supported context and resume returns it as historical evidence without replaying tools. +- Live host checks: align the handoff skill’s shell permission with its quoted command; accept Codex harness/usage metadata without importing it as conversation; resolve DeepSeek attachment storage through its native optional-service API. - 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. diff --git a/integrations/agent-plugin-core/build/handoff-runtime.json b/integrations/agent-plugin-core/build/handoff-runtime.json index 3e05b8905..f8d9595a6 100644 --- a/integrations/agent-plugin-core/build/handoff-runtime.json +++ b/integrations/agent-plugin-core/build/handoff-runtime.json @@ -2,7 +2,7 @@ "revision": "2ac8f2f9a9927f1d88e745432a66d66deab1e8d4", "files": { "handoff_engine.py": "5786e4f24e1145ce26867d18c78fafc8e3de4097b5494815073a2e09df15ea11", - "handoff_sources.py": "0eeae1d92ebd8db58400531fba44e950eb5e3d4e7547375d14c1222041d6fe5f" + "handoff_sources.py": "23957113d048179ba014bab39e121818507d61f976eebb6cfb66558f17fde0d1" }, "artifacts": [ "session_handoff.py", diff --git a/integrations/agent-plugin-core/python/handoff_sources.py b/integrations/agent-plugin-core/python/handoff_sources.py index de73ee351..d6e4db264 100644 --- a/integrations/agent-plugin-core/python/handoff_sources.py +++ b/integrations/agent-plugin-core/python/handoff_sources.py @@ -153,7 +153,9 @@ def _codex(records: list[dict], warnings: list[str]) -> tuple[list[dict], dict]: elif kind == "event_msg": if payload.get("type") == "thread_rolled_back": raise engine.HandoffError("Codex rollback requires a native active-context export.") - elif kind != "turn_context": + # Codex 0.153+ persists harness snapshots and accounting beside response_items. + # These records are not conversation history and must not become user context. + elif kind not in {"turn_context", "world_state", "token_usage_record"}: raise engine.HandoffError(f"Unsupported Codex rollout record: {kind!r}.") result = [] for item in items: diff --git a/integrations/agent-plugin-core/skills/handoff/SKILL.md.tmpl b/integrations/agent-plugin-core/skills/handoff/SKILL.md.tmpl index ee93688e6..124008a9f 100644 --- a/integrations/agent-plugin-core/skills/handoff/SKILL.md.tmpl +++ b/integrations/agent-plugin-core/skills/handoff/SKILL.md.tmpl @@ -2,7 +2,7 @@ name: handoff description: Save a native session as a shared Mem0 handoff resource that another plugin can resume. Run only on explicit user request. disable-model-invocation: true -allowed-tools: Bash(python3 {{PLUGIN_ROOT}}/core/session_handoff.py *) +allowed-tools: Bash(python3 "{{PLUGIN_ROOT}}/core/session_handoff.py" *) --- # Save shared session context diff --git a/integrations/agent-plugin-core/tests/test_build.py b/integrations/agent-plugin-core/tests/test_build.py index 68eed90fc..fce24c711 100644 --- a/integrations/agent-plugin-core/tests/test_build.py +++ b/integrations/agent-plugin-core/tests/test_build.py @@ -2,6 +2,7 @@ from __future__ import annotations import json import sys +from fnmatch import fnmatchcase from pathlib import Path import pytest @@ -139,3 +140,14 @@ def test_handoff_is_bundled_with_host_appropriate_invocation(host: str, tmp_path assert "!`" not in skill assert "NATIVE_TRANSCRIPT_PATH" in skill assert "Never guess the latest session" in skill + + +def test_handoff_preprocessor_matches_its_declared_permission(tmp_path: Path) -> None: + root = build("claude-code", "native", tmp_path / "plugin with spaces") + skill = (root / "skills" / "handoff" / "SKILL.md").read_text() + rule = next(line for line in skill.splitlines() if line.startswith("allowed-tools: Bash(")) + pattern = rule.removeprefix("allowed-tools: Bash(").removesuffix(")") + command = next(line for line in skill.splitlines() if line.startswith("!`")).removeprefix("!`").removesuffix("`") + assert fnmatchcase(command, pattern), ( + "Claude's preprocessor command must match its permission rule, including quotes" + ) diff --git a/integrations/agent-plugin-core/tests/test_session_handoff.py b/integrations/agent-plugin-core/tests/test_session_handoff.py index 85641709a..01e27c9b5 100644 --- a/integrations/agent-plugin-core/tests/test_session_handoff.py +++ b/integrations/agent-plugin-core/tests/test_session_handoff.py @@ -103,6 +103,8 @@ def test_codex_native_rollout_keeps_response_items_and_excludes_harness(tmp_path # openai/codex: codex-rs/protocol/src/protocol.rs and persisted response_item payloads. records = [ {"type": "session_meta", "payload": {"id": "codex-session", "cwd": str(tmp_path)}}, + {"type": "world_state", "payload": {"full": True, "state": {"agents_md": {"text": "harness config"}}}}, + {"type": "token_usage_record", "payload": {"input_tokens": 123, "output_tokens": 45}}, {"type": "response_item", "payload": message("developer", "harness config")}, {"type": "response_item", "payload": {"type": "reasoning", "encrypted_content": "opaque"}}, {"type": "response_item", "payload": message("user", "Keep user")}, diff --git a/integrations/antigravity-plugin/core/handoff-runtime.json b/integrations/antigravity-plugin/core/handoff-runtime.json index 3e05b8905..f8d9595a6 100644 --- a/integrations/antigravity-plugin/core/handoff-runtime.json +++ b/integrations/antigravity-plugin/core/handoff-runtime.json @@ -2,7 +2,7 @@ "revision": "2ac8f2f9a9927f1d88e745432a66d66deab1e8d4", "files": { "handoff_engine.py": "5786e4f24e1145ce26867d18c78fafc8e3de4097b5494815073a2e09df15ea11", - "handoff_sources.py": "0eeae1d92ebd8db58400531fba44e950eb5e3d4e7547375d14c1222041d6fe5f" + "handoff_sources.py": "23957113d048179ba014bab39e121818507d61f976eebb6cfb66558f17fde0d1" }, "artifacts": [ "session_handoff.py", diff --git a/integrations/antigravity-plugin/skills/handoff/SKILL.md b/integrations/antigravity-plugin/skills/handoff/SKILL.md index 19f7549c8..75cc293bd 100644 --- a/integrations/antigravity-plugin/skills/handoff/SKILL.md +++ b/integrations/antigravity-plugin/skills/handoff/SKILL.md @@ -2,7 +2,7 @@ name: handoff description: Save a native session as a shared Mem0 handoff resource that another plugin can resume. Run only on explicit user request. disable-model-invocation: true -allowed-tools: Bash(python3 ${ANTIGRAVITY_PLUGIN_ROOT}/core/session_handoff.py *) +allowed-tools: Bash(python3 "${ANTIGRAVITY_PLUGIN_ROOT}/core/session_handoff.py" *) --- # Save shared session context diff --git a/integrations/claude-code-plugin/CHANGELOG.md b/integrations/claude-code-plugin/CHANGELOG.md index 4555eb97a..cf6a39c32 100644 --- a/integrations/claude-code-plugin/CHANGELOG.md +++ b/integrations/claude-code-plugin/CHANGELOG.md @@ -4,4 +4,5 @@ - `/mem0:handoff` reads the current Claude session before model invocation. Uses the [shared handoff logic](../agent-plugin-core/CHANGELOG.md#032). - List and resume shared resources from any supported plugin; the destination is the common local store. +- Match the handoff command’s quoted path in its shell permission rule, including installation paths with spaces. - Lightened search prompts: search when prior work may help; repeat only for a specific gap. diff --git a/integrations/claude-code-plugin/core/handoff-runtime.json b/integrations/claude-code-plugin/core/handoff-runtime.json index 3e05b8905..f8d9595a6 100644 --- a/integrations/claude-code-plugin/core/handoff-runtime.json +++ b/integrations/claude-code-plugin/core/handoff-runtime.json @@ -2,7 +2,7 @@ "revision": "2ac8f2f9a9927f1d88e745432a66d66deab1e8d4", "files": { "handoff_engine.py": "5786e4f24e1145ce26867d18c78fafc8e3de4097b5494815073a2e09df15ea11", - "handoff_sources.py": "0eeae1d92ebd8db58400531fba44e950eb5e3d4e7547375d14c1222041d6fe5f" + "handoff_sources.py": "23957113d048179ba014bab39e121818507d61f976eebb6cfb66558f17fde0d1" }, "artifacts": [ "session_handoff.py", diff --git a/integrations/claude-code-plugin/skills/handoff/SKILL.md b/integrations/claude-code-plugin/skills/handoff/SKILL.md index 069c69ba0..5c592b98b 100644 --- a/integrations/claude-code-plugin/skills/handoff/SKILL.md +++ b/integrations/claude-code-plugin/skills/handoff/SKILL.md @@ -2,7 +2,7 @@ name: handoff description: Save a native session as a shared Mem0 handoff resource that another plugin can resume. Run only on explicit user request. disable-model-invocation: true -allowed-tools: Bash(python3 ${CLAUDE_PLUGIN_ROOT}/core/session_handoff.py *) +allowed-tools: Bash(python3 "${CLAUDE_PLUGIN_ROOT}/core/session_handoff.py" *) --- # Save shared session context diff --git a/integrations/codex-plugin/CHANGELOG.md b/integrations/codex-plugin/CHANGELOG.md index fa6968df9..23d9a38b2 100644 --- a/integrations/codex-plugin/CHANGELOG.md +++ b/integrations/codex-plugin/CHANGELOG.md @@ -4,4 +4,5 @@ - The `handoff` skill accepts a completed Codex rollout with readable active context. Uses the [shared handoff logic](../agent-plugin-core/CHANGELOG.md#032). - List and resume shared resources from any supported plugin; the destination is the common local store. +- Read current Codex rollouts containing harness snapshots and usage records; only conversation records enter the handoff. - Lightened search prompts: search when prior work may help; repeat only for a specific gap. diff --git a/integrations/codex-plugin/core/handoff-runtime.json b/integrations/codex-plugin/core/handoff-runtime.json index 3e05b8905..f8d9595a6 100644 --- a/integrations/codex-plugin/core/handoff-runtime.json +++ b/integrations/codex-plugin/core/handoff-runtime.json @@ -2,7 +2,7 @@ "revision": "2ac8f2f9a9927f1d88e745432a66d66deab1e8d4", "files": { "handoff_engine.py": "5786e4f24e1145ce26867d18c78fafc8e3de4097b5494815073a2e09df15ea11", - "handoff_sources.py": "0eeae1d92ebd8db58400531fba44e950eb5e3d4e7547375d14c1222041d6fe5f" + "handoff_sources.py": "23957113d048179ba014bab39e121818507d61f976eebb6cfb66558f17fde0d1" }, "artifacts": [ "session_handoff.py", diff --git a/integrations/codex-plugin/skills/handoff/SKILL.md b/integrations/codex-plugin/skills/handoff/SKILL.md index b96cafbee..30a209920 100644 --- a/integrations/codex-plugin/skills/handoff/SKILL.md +++ b/integrations/codex-plugin/skills/handoff/SKILL.md @@ -2,7 +2,7 @@ name: handoff description: Save a native session as a shared Mem0 handoff resource that another plugin can resume. Run only on explicit user request. disable-model-invocation: true -allowed-tools: Bash(python3 ${PLUGIN_ROOT}/core/session_handoff.py *) +allowed-tools: Bash(python3 "${PLUGIN_ROOT}/core/session_handoff.py" *) --- # Save shared session context diff --git a/integrations/cursor-plugin/core/handoff-runtime.json b/integrations/cursor-plugin/core/handoff-runtime.json index 3e05b8905..f8d9595a6 100644 --- a/integrations/cursor-plugin/core/handoff-runtime.json +++ b/integrations/cursor-plugin/core/handoff-runtime.json @@ -2,7 +2,7 @@ "revision": "2ac8f2f9a9927f1d88e745432a66d66deab1e8d4", "files": { "handoff_engine.py": "5786e4f24e1145ce26867d18c78fafc8e3de4097b5494815073a2e09df15ea11", - "handoff_sources.py": "0eeae1d92ebd8db58400531fba44e950eb5e3d4e7547375d14c1222041d6fe5f" + "handoff_sources.py": "23957113d048179ba014bab39e121818507d61f976eebb6cfb66558f17fde0d1" }, "artifacts": [ "session_handoff.py", diff --git a/integrations/cursor-plugin/skills/handoff/SKILL.md b/integrations/cursor-plugin/skills/handoff/SKILL.md index 7ea36baee..a2c15d911 100644 --- a/integrations/cursor-plugin/skills/handoff/SKILL.md +++ b/integrations/cursor-plugin/skills/handoff/SKILL.md @@ -2,7 +2,7 @@ name: handoff description: Save a native session as a shared Mem0 handoff resource that another plugin can resume. Run only on explicit user request. disable-model-invocation: true -allowed-tools: Bash(python3 ${CURSOR_PLUGIN_ROOT}/core/session_handoff.py *) +allowed-tools: Bash(python3 "${CURSOR_PLUGIN_ROOT}/core/session_handoff.py" *) --- # Save shared session context diff --git a/integrations/deepseek-plugin/CHANGELOG.md b/integrations/deepseek-plugin/CHANGELOG.md index 236bc8f24..2c7bc8162 100644 --- a/integrations/deepseek-plugin/CHANGELOG.md +++ b/integrations/deepseek-plugin/CHANGELOG.md @@ -4,4 +4,5 @@ - An explicit `mem0_handoff` call uses the current DeepSeek session’s derived messages; invoke it outside nested code mode. Uses the [shared handoff logic](../agent-plugin-core/CHANGELOG.md#032). - List and resume shared resources from any supported plugin; the destination is the common local store. +- Use native optional-service lookup for image attachments so text-only saves also work in the live harness. - Lightened search prompts: search when prior work may help; repeat only for a specific gap. diff --git a/integrations/deepseek-plugin/src/index.ts b/integrations/deepseek-plugin/src/index.ts index 5f54a8abe..05bd64483 100644 --- a/integrations/deepseek-plugin/src/index.ts +++ b/integrations/deepseek-plugin/src/index.ts @@ -109,7 +109,6 @@ export function apply(ctx: Context, config: Config): void { if (!session.header.cwd) throw new Error("The native session project directory is unavailable."); if (action === "list" || action === "resume") return await runHandoffAction(new URL("./session_handoff.py", import.meta.url), action, session.header.cwd, resource); if (action !== "save") throw new Error("Handoff action must be save, list, or resume."); - const attachments = (ctx as unknown as { attachments?: { readImage(ref: unknown): Promise<{ref: {mediaType: string}; data: Uint8Array}> } }).attachments; // dsh-session-title persists user renames and generated titles as last-wins log events. const titleEvent = [...session.events].reverse().find(event => String(event.type) === "session/title"); const nativeTitle = (titleEvent?.data as {title?: unknown} | undefined)?.title; @@ -120,6 +119,8 @@ export function apply(ctx: Context, config: Config): void { }, session.deriveMessages(), { excludeCallId: exec.callId, readImage: async (ref) => { + // Optional services must use Cordis lookup; direct access requires inject. + const attachments = ctx.get("attachments") as { readImage(ref: unknown): Promise<{ref: {mediaType: string}; data: Uint8Array}> } | undefined; if (!attachments) throw new Error("DeepSeek image attachment storage is unavailable."); const image = await attachments.readImage(ref); return { data: image.data, mediaType: image.ref.mediaType }; diff --git a/integrations/deepseek-plugin/tests/apply.test.ts b/integrations/deepseek-plugin/tests/apply.test.ts index ac8f35d6b..3bedc2e08 100644 --- a/integrations/deepseek-plugin/tests/apply.test.ts +++ b/integrations/deepseek-plugin/tests/apply.test.ts @@ -32,10 +32,12 @@ interface RegisteredTool { type HarnessListener = (...args: any[]) => unknown; -function applyAndCollect(config: Config): Map { +function applyAndCollect(config: Config, attachments?: unknown): Map { const tools = new Map(); const ctx = { tools: { register: (t: RegisteredTool) => tools.set(t.name, t) }, + get: (service: string) => service === "attachments" ? attachments : undefined, + get attachments() { throw new Error('cannot get property "attachments" without inject'); }, on: vi.fn(), }; apply(ctx as never, config); @@ -310,6 +312,19 @@ describe("mem0_handoff tool", () => { expect(mockSearch).not.toHaveBeenCalled(); expect(mockAdd).not.toHaveBeenCalled(); }); + it("reads native image bytes through Cordis optional service lookup", async () => { + const readImage = vi.fn(async () => ({ref: {mediaType: "image/png"}, data: new Uint8Array([104,105])})); + const tools = applyAndCollect({apiKey: "k", userId: "u"}, {readImage}); + const imageExec = {...exec, agent: {session: {...exec.agent.session, deriveMessages: () => [ + {role: "user", content: [{type: "text", text: "Describe this image"}, {type: "image", attachment: {id: "native-image"}}]}, + ]}}}; + vi.mocked(runHandoff).mockResolvedValue("Saved image context"); + expect(await tools.get("mem0_handoff")!.execute({}, imageExec)).toBe("Saved image context"); + expect(readImage).toHaveBeenCalledWith({id: "native-image"}); + expect(JSON.stringify(vi.mocked(runHandoff).mock.calls[0][1])).toContain("data:image/png;base64,aGk="); + const unavailable = applyAndCollect({apiKey: "k", userId: "u"}); + expect(await unavailable.get("mem0_handoff")!.execute({}, imageExec)).toContain("attachment storage is unavailable"); + }); it("refuses unfinished sibling tools rather than hiding them with its own invocation", async () => { const tools = applyAndCollect({apiKey: "k", userId: "u"}); const siblingExec = {...exec, agent: {session: {...exec.agent.session, deriveMessages: () => [ diff --git a/integrations/kimi-plugin/core/handoff-runtime.json b/integrations/kimi-plugin/core/handoff-runtime.json index 3e05b8905..f8d9595a6 100644 --- a/integrations/kimi-plugin/core/handoff-runtime.json +++ b/integrations/kimi-plugin/core/handoff-runtime.json @@ -2,7 +2,7 @@ "revision": "2ac8f2f9a9927f1d88e745432a66d66deab1e8d4", "files": { "handoff_engine.py": "5786e4f24e1145ce26867d18c78fafc8e3de4097b5494815073a2e09df15ea11", - "handoff_sources.py": "0eeae1d92ebd8db58400531fba44e950eb5e3d4e7547375d14c1222041d6fe5f" + "handoff_sources.py": "23957113d048179ba014bab39e121818507d61f976eebb6cfb66558f17fde0d1" }, "artifacts": [ "session_handoff.py", diff --git a/integrations/kimi-plugin/skills/handoff/SKILL.md b/integrations/kimi-plugin/skills/handoff/SKILL.md index 42c23ccef..51634b026 100644 --- a/integrations/kimi-plugin/skills/handoff/SKILL.md +++ b/integrations/kimi-plugin/skills/handoff/SKILL.md @@ -2,7 +2,7 @@ name: handoff description: Save a native session as a shared Mem0 handoff resource that another plugin can resume. Run only on explicit user request. disable-model-invocation: true -allowed-tools: Bash(python3 ${KIMI_PLUGIN_ROOT}/core/session_handoff.py *) +allowed-tools: Bash(python3 "${KIMI_PLUGIN_ROOT}/core/session_handoff.py" *) --- # Save shared session context diff --git a/integrations/mem0-agent-plugin/core/handoff-runtime.json b/integrations/mem0-agent-plugin/core/handoff-runtime.json index 3e05b8905..f8d9595a6 100644 --- a/integrations/mem0-agent-plugin/core/handoff-runtime.json +++ b/integrations/mem0-agent-plugin/core/handoff-runtime.json @@ -2,7 +2,7 @@ "revision": "2ac8f2f9a9927f1d88e745432a66d66deab1e8d4", "files": { "handoff_engine.py": "5786e4f24e1145ce26867d18c78fafc8e3de4097b5494815073a2e09df15ea11", - "handoff_sources.py": "0eeae1d92ebd8db58400531fba44e950eb5e3d4e7547375d14c1222041d6fe5f" + "handoff_sources.py": "23957113d048179ba014bab39e121818507d61f976eebb6cfb66558f17fde0d1" }, "artifacts": [ "session_handoff.py", diff --git a/integrations/mem0-agent-plugin/skills/handoff/SKILL.md b/integrations/mem0-agent-plugin/skills/handoff/SKILL.md index ef1ba320d..a4bba6683 100644 --- a/integrations/mem0-agent-plugin/skills/handoff/SKILL.md +++ b/integrations/mem0-agent-plugin/skills/handoff/SKILL.md @@ -1,7 +1,7 @@ --- name: handoff description: Save a native session as a shared Mem0 handoff resource that another plugin can resume. Run only on explicit user request. -allowed-tools: Bash(python3 ${PLUGIN_ROOT}/core/session_handoff.py *) +allowed-tools: Bash(python3 "${PLUGIN_ROOT}/core/session_handoff.py" *) --- # Save shared session context