Skip to content

fix(memory): support the OpenCode V2 plugin API and session_v2 schema - #655

Closed
kristhianmanue1 wants to merge 1 commit into
MemTensor:mainfrom
kristhianmanue1:fix/opencode-v2-integration
Closed

kristhianmanue1 wants to merge 1 commit into
MemTensor:mainfrom
kristhianmanue1:fix/opencode-v2-integration

Conversation

@kristhianmanue1

Copy link
Copy Markdown

中文摘要(TL;DR)

本 PR 修复 #654:OpenCode V2 下插件无法加载、且两条数据库读取链路(插件捕获 + 桌面历史扫描)都因 V1 表结构(session/message/part)失效的问题。

  • AgentSourceCore 新增 opencode-v2-source 映射器:把 session_v2/session_message 行(内容内联在 data.content[]、无 parentID)投影成共享回合解析器已使用的视图,V1/V2 共用同一条回合管线。
  • db-reader(Memory 与 App/backend 两份):V1 表缺失时回退到 V2 表,扫描与 hook 捕获同时修复。
  • 插件模板:双入口。@opencode-ai/plugin 改为惰性导入(V2 下文件可加载),默认导出同时携带 V2 setup()(ctx.tool.transform 工具、prompt 钩子召回注入、事件流捕获)与 V1 server()(>= 1.18.29)。
  • 本地已跑通:AgentSourceCore 67/67、Memory 集成 3/3、tsc --noEmit、渲染后插件语法校验;并已在真实 OpenCode V2 + Memory 2.1.4 上端到端验证(召回注入、回合捕获、工具配对)。

Problem

Closes #654. Two independent breaks make the Memmy × OpenCode integration dead on OpenCode V2:

  1. Plugin API. The rendered plugin imports { tool } from "@opencode-ai/plugin" statically and exports a V1-style hooks function. OpenCode V2 requires a default-exported { id, setup(ctx) } definition and never invokes V1 entrypoints — the file fails at import resolution (Cannot find package '@opencode-ai/plugin'). Per the official migration guide: "V1 plugin implementations do not run in V2."
  2. Database schema. OpenCode V2 replaced session/message/part with session_v2/session_message (message parts inlined in data.content[], assistant parentID dropped). Both readers — the desktop scan (db-reader.ts) and the plugin hook capture path (readOpencodeHookSourceTurn) — require the V1 tables and silently ignore a V2 database.

Consequence: no recall injection, no turn capture, no memmy_memory_* tools, no /memmy-resume, and nothing from OpenCode V2 reaches the viewer.

Changes

Area File(s) Change
Core AgentSourceCore/src/opencode-v2-source.ts (new) Row mappers: toOpencodeV2Session, toOpencodeV2MessageView (sequential parent grouping replaces parentID, completion timestamp becomes the finish stop signal, bookkeeping row types dropped), toOpencodeV2MessageParts (user data.text → text part; inline content[] → text/tool/compaction parts; reasoning dropped)
Core AgentSourceCore/src/index.ts Re-export the new module
Scan Memory/src/agent-source/adapters/opencode/db-reader.ts, App/backend/.../opencode/db-reader.ts createSource tries V1 tables first, then falls back to a V2 source built from the shared mappers (per-session message query + O(1) message-by-id part lookup)
Bridge both workspace-bridge/runtime.ts readOpencodeHookSourceTurn gains the same V1/V2 dispatch via createOpencodeHookSource, so plugin capture and offline scan keep resolving one turn to one identity
Template both templates/memmy-opencode-plugin.ts Dual entrypoint. The @opencode-ai/plugin import moved inside the V1 function (V1 provides the package at runtime; V2 never runs that path), and a default export carries setup() for V2 plus server() for V1 (object entrypoints supported since OpenCode 1.18.29). V2 setup: three memmy_memory_* tools via ctx.tool.transform, recall injection through the prompt hook (persisted text stays unwrappable by the existing <current_user_request> convention), capture on session.idle with a flush-on-next-prompt fallback, L3 preload, compaction boundaries, session cleanup, /memmy-resume flow. Every Memmy failure degrades to a log line so a turn is never interrupted.
Tests AgentSourceCore/src/tests/opencode-v2-source.test.ts (new), Memory/tests/agent-source-opencode-reader.test.ts Mapper unit tests (grouping, finish synthesis, part mapping, revert JSON) plus integration through the shared parser; SQLite reader cases against a session_v2/session_message database (capture, subagent skip, open-ended turn left incomplete)

The duplicated Memory/App-backend copies of the reader, bridge runtime, and template are kept byte-identical, matching the existing layout.

Validation

  • AgentSourceCore: vitest run src → 67/67 (6 pre-existing files unaffected + new file)
  • Memory: vitest run tests/agent-source-opencode-reader.test.ts → 3/3 (1 pre-existing + 2 new V2 cases)
  • tsc --noEmit clean for AgentSourceCore and Memory (App/backend shows only pre-existing unresolved @memmy/local-api-contracts workspace types in the local sandbox)
  • Rendered dual plugin (1437 lines) passes node --check; template invariants hold (no backticks / interpolation inside the raw string)
  • Live end-to-end on OpenCode V2 + Memory 2.1.4: recall injection on every prompt, turn capture with correct identity (prompt messageID == user session_message.id), stored queries unwrapped, tool calls paired with status, parallel sessions captured, /memmy-resume and the three memory tools working. The POC this PR formalizes is linked in [Bug] OpenCode V2 integration broken: plugin ships the V1 plugin API and both DB readers target the V1 schema #654.

Compatibility

  • OpenCode V1 behavior is unchanged (same hooks, same identity contract); the SDK import is just deferred to invocation time.
  • The V2 path degrades gracefully whenever the memory service is unreachable (log-only), and its capture path reuses the bridge reader, so a turn is never double-stored under different identities.
  • Desktop historical scan now also picks up V2 databases — worth a regression pass on a V1 database (covered by the existing V1 tests here).

Happy to adjust to any house conventions (MEMMY-XXX tracking, commit scoping, test placement).

OpenCode V2 renamed its conversation tables (session/message/part ->
session_v2/session_message, parts inlined into data.content) and replaced
the V1 plugin API, so the installed plugin failed to load and both the
hook capture path and the desktop scan ignored the V2 database entirely.

- AgentSourceCore: new opencode-v2-source mappers project V2 rows onto the
  shared turn parser (sequential parent grouping, inline content parts,
  completion-timestamp finish signal)
- opencode db-readers (Memory + App/backend): fall back to the V2 tables
  when the V1 tables are absent, in both the scan reader and the bridge
  hook reader
- plugin template: dual entrypoint. The V1 helper import is now lazy so
  the file loads under V2, and a default export carries setup() for V2
  (tools via ctx.tool.transform, prompt-hook recall injection, event
  stream capture) plus server() for V1 (>= 1.18.29)
- tests: mapper unit tests plus V2-schema SQLite reader cases

Verified live against OpenCode V2 + Memory 2.1.4 (see MemTensor#654).

Closes MemTensor#654
@kristhianmanue1

Copy link
Copy Markdown
Author

Follow-up from live V2 testing: compaction detection won't fire (event name is V1)

Two findings relevant to this PR:

  1. session.compacted never fires on OpenCode V2. A live tally of the V2 server event stream (2.0.29–2.0.30) shows the emitted name is session.compaction.ended. The template's V2 event loop listens for session.compacted, so after an auto-compaction: notifyRuntimeBoundary(..., "token_compaction") never runs, and L3 is never reloaded for the rest of the session (the first-prompt preload is one-shot) — the world model goes silently stale in long sessions.

  2. The documented V2 mechanism is a session hook, not an event. The official migration guide maps V1 experimental.session.compacting → ctx.session.hook("compaction", ...) (migrate-v1); the template currently registers only the prompt hook.

Suggested adjustment (small): register ctx.session.hook("compaction", ...) as the primary boundary signal, and keep an event listener for both spellings (session.compaction.ended / session.compacted) as a cross-build fallback. Everything else in the V2 path held up in live testing (recall injection, capture identity, tool pairing, /memmy-resume).

Side note: the lazy first-prompt L3 preload (firstTurnSeen) checks out — it lands in the same turn's packet, so no session.created handling is needed.

@kristhianmanue1

Copy link
Copy Markdown
Author

Closing as superseded by the official OpenCode V2 support shipped in v1.2.4 — leaving this record for traceability.

The ideas in this PR converged into the official implementation:

  • SDK-free plugin — the shipped template removes the @opencode-ai/plugin import and bundles the tool schema helper in the bridge (opencodeTool), same motivation as this PR's lazy-import/dual-export approach.
  • Dual-generation entrypoint — the official template exports default { id, server, setup }, so one installed file serves V1 (server()) and V2 (setup(ctx)) hosts.
  • session_v2 database source — reading now lives in AgentSourceCore/src/opencode-database-source.ts (equivalent to this PR's opencode-v2-source.ts), with the bridge runtime updated and a dedicated test (source-turn-opencode-v2.test.ts).
  • Compaction boundary — the follow-up finding here (session.compacted never fires on V2) is addressed upstream by mapping session.compaction.ended and shipping readOpencodeCompactionBoundary (plugin revision opencode-v2-3).

Verified live after upgrading 1.2.2 → 1.2.401 on macOS: the installer regenerated the plugin automatically and recall/capture work end-to-end on OpenCode v2.0.30.

Thanks for the review consideration — happy to rebase any residual deltas if maintainers see value, otherwise this can rest. 🫡

TL;DR(中文):v1.2.4 已官方实现 OpenCode V2 支持,与本 PR 思路一致(免 SDK 插件、双入口、session_v2 读取、compaction 边界);已在 v1.2.401 实测,故关闭本 PR,感谢审阅。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] OpenCode V2 integration broken: plugin ships the V1 plugin API and both DB readers target the V1 schema

1 participant