feat(opencode): #1484 adapter + #1567 MCP config + daemon-routed integration recipe - #106
Conversation
Adds mempalace/sources/opencode.py — an OpenCodeSourceAdapter
subclass of BaseSourceAdapter that ingests OpenCode AI-coding-CLI
session transcripts from OpenCode's local SQLite store
(~/.local/share/opencode/opencode.db) into the palace as
DrawerRecords formatted to match convo_miner's exchange-pair shape.
The adapter:
* Yields SourceItemMetadata then DrawerRecords per session.
* Each session becomes one source_file shaped as
opencode://<absolute-db-path>#session=<sid>; chunks are
chunked_content exchange-pair drawers.
* Declares 8 transformations (6 opencode-namespaced + 2 reserved);
every name resolves to a reference implementation on
mempalace.sources.transforms per RFC 002 §7.3.
* Implements is_current honoring opencode_session_version when
present, falling back to "metadata exists → assume current"
for append-only safety on older drawers.
* Routes wing from session.directory basename (or explicit
options['wing'] override); room from detect_convo_room on the
rendered transcript; hall from convo_miner._detect_hall_cached.
* Stamps universal §5.1 metadata (wing, room, hall, filed_at,
added_by, ingest_mode, extract_mode, privacy_class) plus the
declared per-adapter schema (session_id, session_title,
project_dir, session_created_at, message_count, opencode_db_path).
* default_privacy_class = "pii_potential" — AI sessions leak
everything; users opt in explicitly to laxer floors.
mempalace/sources/transforms.py: adds 6 opencode-namespaced
transformations (extract_text_parts, skip_tool_echo,
skip_file_injection, role_coerce, same_role_merge, format_exchange).
Each operates on the role-tab-prefixed line stream the adapter's
canonical_source_bytes produces; declared in declaration order so
the conformance round-trip test reproduces drawer content exactly.
pyproject.toml: registers the adapter under the
[project.entry-points."mempalace.sources"] group as
opencode = "mempalace.sources.opencode:OpenCodeSourceAdapter".
tests/test_sources_opencode.py: 28 tests covering
* class identity, capabilities, schema shape
* SourceNotFoundError on missing DB / missing tables
* AdapterClosedError after close()
* source_summary item count + missing-DB path
* ingest yields metadata then drawers per session
* cancelled / single-turn sessions skipped
* universal + schema metadata fields on every drawer (flat-scalar)
* RouteHint carries wing + room
* wing routing groups by session.directory
* explicit options['wing'] wins over directory derivation
* skip_current_item short-circuits drawer emit per RFC 002 §1.2
* is_current with/without opencode_session_version
* tool-input / tool-output / tool-echo / file-injection parts
are stripped from drawer content
* declared-transformation round-trip reproduces chunk content
(RFC 002 §7.3)
* empty DB, single-message session edge cases
* Unicode (BMP + non-BMP) preserved through transcript
* registry resolves the adapter when registered explicitly
* byte_preserving capability is NOT advertised (declared-lossy)
tests/fixtures/opencode/sample_session_2026_05_12/: builder script
and README documenting the live opencode-ai 1.14.39 schema captured
verbatim from JP's local install on 2026-05-12. No recorded .db
ships (real-session content is unsanitizable user-private data);
build_fixture.py reproduces the schema and populates it with
synthetic-but-realistic exchanges the tests consume.
tests/test_corpus_origin_integration.py: extends the §-section
allowlist to include the new test file (existing allowlist already
covers mempalace/sources/).
Reverse-engineering credit: the OpenCode SQLite schema, json_extract
paths, tool-echo / file-injection skip filters, and same-role merge
originated in @JakobSachs's PR #23 (feat: add OpenCode SQLite
session database support, base=develop). This adapter rebuilds those
primitives on the RFC 002 contract so OpenCode support can ship as a
registered adapter rather than as a normalize.py branch — see #23
coordination thread.
Test suite: 1876 passed, 7 skipped, 106 deselected (28 new opencode
tests, no regressions).
Co-authored-by: Jakob Sachs <28728963+JakobSachs@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…#1484 Four issues raised in the automated review (2026-05-13T01:40Z): 1. **opencode_session_version missing from metadata** (high) `is_current()` at opencode.py:391 compares `existing_metadata.get( "opencode_session_version")` against the new `SourceItemMetadata.version`. Without the metadata key being written on first ingest, the comparison always falls back to "exists → current" and incremental ingest can never detect updates to existing sessions. Now populated as `str(time_updated or time_created or 0)` — same value as the version yielded in SourceItemMetadata above. 2. **PalaceContext._skip_requested encapsulation violation** (medium) The adapter was reading and writing the private flag directly. Added `PalaceContext.is_skip_requested()` public method (read-only) so adapters can short-circuit expensive work (SQL query, transcript build, chunking) when core has signaled skip. Core still owns the reset — adapters MUST NOT clear it, per the new docstring. This is a small companion change to the upstream RFC 002 scaffolding (MemPalace#1014); justified because the spec's "core checks between yields" pattern doesn't hold for Python generators (the adapter's code runs between yields, not core's). The check needs to be available to the adapter. 3. **filed_at generated inside chunk loop** (medium) For consistency across chunks of the same session, `filed_at` is now computed once per session and reused for every chunk's metadata. Also pre-computes `session_version` for the same reason. 4. **PEP 8 import placement** (medium) `import json as _json` was mid-file in transforms.py; hoisted to the top with the other imports. Also removed an unused `import json` from opencode.py that ruff caught. Tests: 57 pass (28 opencode + 29 base sources); ruff clean on all three modified files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three blockers + one minor cleanup from the maintainer review at 2026-05-13T02:52Z: 1. **ruff F401 — unused `os` import** in tests/test_sources_opencode.py:17 Dropped. No call sites used it. 2. **ruff E402 — module-level import not at top** in tests The `sys.path.insert(0, FIXTURE_DIR); import build_fixture` pattern tripped E402 (the `# noqa: E402` was suppressing a legitimate complaint). Refactored to `importlib.util.spec_from_file_location` + `module_from_spec` per @igorls's suggestion — keeps the fixture loader at top of file with the other imports, no sys.path mutation at module scope. Also registers the loaded module in `sys.modules` so `dataclasses` and typing introspection inside the fixture builder can resolve `cls.__module__` correctly. 3. **Route-hint wing mismatch** (RFC 002 §2.5 violation) `_route_hint_for()` (lazy-fetch SourceItemMetadata stage) computed wing from `directory` only; `_wing_for()` (eager DrawerRecord stage) honored `source.options["wing"]` first. When a user passed `options={"wing": "Custom Wing"}`, the metadata hint said `"<dirname>"` while the actual drawers said `"custom_wing"` — core could make wrong skip/routing decisions on the gap. Fix: `_route_hint_for(source, directory)` now delegates to `_wing_for` so both stages apply identical precedence. 4. **Unjustified `# noqa: F401` on `AuthRequiredError`** (minor) The import claimed re-export "used in docstrings" but `__all__` only exposes `OpenCodeSourceAdapter` + `session_source_file`. Dropped the import + the noqa. Tests: 57 pass (28 opencode + 29 base sources); ruff clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CI's lint job ran on commit 13353d9 and failed `ruff format --check .` even though local `ruff format --check` was clean. Cause: ruff version mismatch — CI installs `>=0.4.0,<0.5` (per ci.yml lint job), local env has ruff 0.15.12. Different major versions format differently; 0.15-formatted source isn't 0.4.x-format-clean. Reformatted `mempalace/sources/opencode.py` and `tests/test_sources_opencode.py` with `uvx --from "ruff>=0.4.0,<0.5" ruff format` so CI's check passes. Changes are whitespace-only — no semantic diff. Tests still pass 28/28. Lint clean under 0.4.x. The 29 other files that local ruff 0.15.12 wants to reformat are upstream's own files and pass upstream's CI as-is; left untouched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Missed in the previous format pass (f94e3fe) — only touched the two top-level files. CI's `ruff format --check .` scans the whole tree and caught it. Whitespace-only changes.
…anges entries Adds the three-direction OpenCode + MemPalace integration recipe: - ``docs/integrations/opencode.md`` — full setup guide covering the read (MCP), push (live-capture plugin), and pull (retrospective backfill) paths for daemon-routed deployments. - ``examples/opencode/opencode.jsonc.example`` — copy-paste user config pointing at the palace-daemon wrapper. - ``examples/opencode/option-k-plugin-daemon-routing.patch`` — a re-applicable diff for option-K's ``opencode-plugin-mempalace`` v1.2.1 issue #1 (isInitialized passes ``--palace`` which bypasses ``PALACE_DAEMON_URL`` routing). Also adds two fork-changes.yaml entries for the cherry-picked upstream PRs already in this branch: - ``opencode-mcp-config-cherry-pick-1567`` (commit ba16b82) - ``opencode-source-adapter-cherry-pick-1484`` (commit 2ffe652) The recipe's own fork-changes.yaml entry is added in the next commit once this commit's SHA is known (avoids the self-referencing-commit anti-pattern flagged in the worktree handoff). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…integration # Conflicts: # FORK_CHANGELOG.md # docs/fork-changes.yaml
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request integrates OpenCode with the MemPalace ecosystem by cherry-picking upstream source adapter and MCP configuration improvements, while adding fork-specific documentation and patches to support daemon-routed setups. The changes provide a complete workflow for users to ingest historical OpenCode sessions, capture live interactions, and enable agent-based recall via MCP tools. Highlights
Ignored Files
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive integration for OpenCode, including a new RFC 002 source adapter for ingesting SQLite session transcripts, repository-level MCP configuration, and detailed documentation. The adapter implements a transformation pipeline to clean and format transcripts into exchange-pair drawers. Feedback highlights a critical bug in the transformation logic where multi-line message bodies are truncated due to incorrect line-splitting assumptions. Additionally, improvements were suggested regarding SQLite connection modes (read-only), performance optimizations for string concatenation in message merging, and stricter error handling for user-provided database paths.
| continue | ||
| pairs.append((role, body)) |
There was a problem hiding this comment.
This loop drops any line that does not contain a tab character. Because the transformation pipeline (specifically opencode_same_role_merge) produces multi-line message bodies, any content after the first newline of a merged message will be silently discarded here. This results in significant content loss for long transcripts.
| if not sep: | ||
| continue |
| if not sep: | ||
| continue |
| if local_path: | ||
| candidates.append(local_path) | ||
| candidates.extend(_DEFAULT_DB_PATHS) |
There was a problem hiding this comment.
If a local_path is explicitly provided by the user but does not exist, the current logic silently falls back to searching the default paths. This can lead to confusing behavior where the adapter mines a different database than the one specified without any warning. It would be better to raise a SourceNotFoundError immediately if an explicit path is provided but invalid.
| if self._closed: | ||
| raise AdapterClosedError("OpenCodeSourceAdapter is closed") | ||
| db_path = _resolve_db(source.local_path) | ||
| conn = sqlite3.connect(db_path) |
There was a problem hiding this comment.
Connecting to a live application's SQLite database without specifying read-only mode can lead to database locking issues or unintended side effects if the application (OpenCode) is currently writing to it. Using mode=ro via a URI connection is safer for this use case.
| conn = sqlite3.connect(db_path) | |
| conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) |
| continue | ||
| if out and out[-1][0] == role: | ||
| prev_role, prev_body = out[-1] | ||
| out[-1] = (prev_role, prev_body + "\n\n" + body) |
There was a problem hiding this comment.
Repeated string concatenation in a loop (prev_body + "\n\n" + body) has O(N^2) time complexity. For very long sessions with many consecutive messages from the same role (e.g., after role coercion), this can become a performance bottleneck. Consider collecting parts in a list and joining them at the end.
…n-K patches The previously combined option-K patch (`option-k-plugin-daemon-routing.patch`) mixed two unrelated fixes against two different files and was failing `patch --dry-run` once Fix 1 was applied. Split into: - `option-k-plugin-daemon-routing.patch` — Fix 1 only (mempalace-cli.js, isInitialized daemon detection, option-K#1). - `option-k-plugin-message-updated.patch` — Fix 2 (index.js, subscribe to `message.updated` instead of the non-existent `chat.message`, filed upstream as option-K#4). End-to-end testing with both patches applied surfaced a third bug (option-K#5): the plugin's `mempalace mine <dir>` call hits the daemon, which evaluates `<dir>` against ITS OWN filesystem. For remote-daemon setups (palace-daemon on a different host from OpenCode) the path doesn't exist on the daemon's filesystem and the call returns 400. The option-K plugin is architecturally incompatible with multi-host deployments. Ships a self-contained replacement at `examples/opencode/live-capture/`: - `mempalace-live-capture.js` — minimal OpenCode plugin that subscribes to session.idle / session.deleted / session.status[idle] and spawns the Python helper. Detached subprocess, debounced per session, logs to ~/.local/share/opencode/mempalace-live-capture.log. - `capture-session.py` — Python helper that reads OpenCode's local SQLite session DB, extracts the role-pair transcript via the in-tree `OpenCodeSourceAdapter` helpers, and POSTs to the daemon's `/silent-save` endpoint. Stdlib-only, no extra pip deps. Verified end-to-end against the canonical daemon at disks.jphe.in:8085: a fresh opencode session ends with the transcript landing in wing_opencode_<basename>/room=diary, retrievable via mempalace_search. `docs/integrations/opencode.md` now documents both deployment paths (bundled plugin for remote-daemon, option-K + patches for local palaces) and explicitly notes that `experimental.chat.system.transform` does not exist in the OpenCode plugin API (so per-turn system-prompt injection is not available; agents recall memories via explicit MCP tool calls). Filed: - option-K/opencode-plugin-mempalace#4 - option-K/opencode-plugin-mempalace#5 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the YAML→render loop: scripts/check-docs.sh now verifies the commit hash resolves and FORK_CHANGELOG.md matches the manifest. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Lands the OpenCode source adapter (upstream PR MemPalace#1484) and in-tree MCP config (upstream PR MemPalace#1567) on this fork, with a documented integration recipe for daemon-routed setups.
Three-direction architecture:
palace-daemon/clients/mempalace-mcp-wrapper.shopencode-plugin-mempalacenpm package + a re-applicable patch for issue #1OpenCodeSourceAdapterviamempalace mine --source opencodeTogether these match the same shape as MemPalace's Claude Code stop-hook pattern.
Cherry-picks
Upstream PR MemPalace#1484 (5 commits, clean apply onto fork main):
2c368c6initial adapter (482 LOC + 28 tests + fixture)3ff7043gemini-code-assist review fixes9531532igorls review fixes18ab021+2ffe652ruff formatUpstream PR MemPalace#1567 (2 commits):
013ac63initial.opencode/opencode.jsonconfigba16b82switch topython -m mempalace.mcp_serverper gemini reviewNew on the fork
docs/integrations/opencode.md— full setup recipeexamples/opencode/opencode.jsonc.example— copy-paste user configexamples/opencode/option-k-plugin-daemon-routing.patch— fix for option-K plugin v1.2.1 issue fix(cli): paginate miner.status() to remove 10K drawer truncation #1 (isInitialized passes--palacewhich bypasses daemon routing)docs/fork-changes.yamlentries (split into two commits to avoid self-referencing-SHA: the recipe entry points at the prior docs commit, not at itself)Branch lineage
origin/mainafter merging ine281784(resilience),abab456(upstream sync), and3e350b0(AGE-KG)Test plan
ruff check .cleanruff format --check .cleanexamples/opencode/opencode.jsonc.examplebecomes canonical🤖 Generated with Claude Code