Skip to content

feat(opencode): #1484 adapter + #1567 MCP config + daemon-routed integration recipe - #106

Merged
jphein merged 12 commits into
mainfrom
feat/opencode-daemon-integration
May 22, 2026
Merged

feat(opencode): #1484 adapter + #1567 MCP config + daemon-routed integration recipe#106
jphein merged 12 commits into
mainfrom
feat/opencode-daemon-integration

Conversation

@jphein

@jphein jphein commented May 22, 2026

Copy link
Copy Markdown
Collaborator

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:

Direction Mechanism
Read (agent → palace) MCP entry pointing at palace-daemon/clients/mempalace-mcp-wrapper.sh
Push (live capture) option-K's opencode-plugin-mempalace npm package + a re-applicable patch for issue #1
Pull (retrospective backfill) The cherry-picked OpenCodeSourceAdapter via mempalace mine --source opencode

Together 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):

  • 2c368c6 initial adapter (482 LOC + 28 tests + fixture)
  • 3ff7043 gemini-code-assist review fixes
  • 9531532 igorls review fixes
  • 18ab021 + 2ffe652 ruff format

Upstream PR MemPalace#1567 (2 commits):

  • 013ac63 initial .opencode/opencode.json config
  • ba16b82 switch to python -m mempalace.mcp_server per gemini review

New on the fork

  • docs/integrations/opencode.md — full setup recipe
  • examples/opencode/opencode.jsonc.example — copy-paste user config
  • examples/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 --palace which bypasses daemon routing)
  • Three docs/fork-changes.yaml entries (split into two commits to avoid self-referencing-SHA: the recipe entry points at the prior docs commit, not at itself)

Branch lineage

  • 8 commits cherry-picked from upstream + 2 docs commits = 10 fork-ahead commits
  • Branch is up to date with the new origin/main after merging in e281784 (resilience), abab456 (upstream sync), and 3e350b0 (AGE-KG)
  • Merge commit resolves YAML/CHANGELOG conflicts as the union of both sides' entries

Test plan

  • 28/28 OpenCode adapter tests pass in 1.7s
  • 154 tests pass on the merged tree across opencode + power-resilience + AGE-KG + jargon-lint suites (regression-slice; full suite ~3 min CI will run)
  • ruff check . clean
  • ruff format --check . clean
  • Self-referencing SHA fixed per worktree handoff (docs commit 60dc9e6, YAML bump in b1003d0)
  • Smoke test on katana — deferred until merge so examples/opencode/opencode.jsonc.example becomes canonical

🤖 Generated with Claude Code

jphein and others added 10 commits May 21, 2026 16:35
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>
…dc9e6

Companion to 60dc9e6 (the OpenCode integration recipe commit). Split
out per the worktree handoff to avoid the self-referencing-commit-SHA
anti-pattern: the YAML entry now points at the prior docs commit,
not at itself.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…integration

# Conflicts:
#	FORK_CHANGELOG.md
#	docs/fork-changes.yaml
Copilot AI review requested due to automatic review settings May 22, 2026 01:30
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, 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

  • OpenCode Source Adapter: Implemented the OpenCodeSourceAdapter (cherry-picked from upstream PR feat(sources): OpenCode adapter on RFC 002 contract MemPalace/mempalace#1484) to allow retrospective ingestion of OpenCode SQLite session transcripts into the MemPalace.
  • MCP Integration: Added .opencode/opencode.json (cherry-picked from upstream PR feat: add OpenCode MCP integration MemPalace/mempalace#1567) to enable automatic MemPalace MCP server wiring when running OpenCode in the repository root.
  • Integration Documentation: Added a comprehensive integration recipe in docs/integrations/opencode.md detailing the three-direction setup (Read-side MCP, Push-side live capture, and Pull-side retrospective backfill) for daemon-routed environments.
  • Daemon-Routing Fix: Included a patch for the opencode-plugin-mempalace npm package to resolve issue fix(cli): paginate miner.status() to remove 10K drawer truncation #1, ensuring the plugin correctly respects daemon-routed configurations instead of bypassing them with local lookups.
Ignored Files
  • Ignored by pattern: FORK_CHANGELOG.md (1)
    • FORK_CHANGELOG.md
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +141 to +142
continue
pairs.append((role, body))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

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.

Comment on lines +263 to +264
if not sep:
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This transform silently drops lines that do not contain a tab character. Since opencode_extract_text_parts can produce multi-line output (as message bodies naturally contain newlines), this logic causes content loss for any message that spans multiple lines.

Comment on lines +284 to +285
if not sep:
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Similar to opencode_same_role_merge, this function drops any line without a tab separator, leading to content loss for multi-line message bodies produced by earlier steps in the pipeline.

Comment on lines +77 to +79
if local_path:
candidates.append(local_path)
candidates.extend(_DEFAULT_DB_PATHS)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

jphein and others added 2 commits May 21, 2026 19:43
…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>
@jphein
jphein merged commit dfe3cb8 into main May 22, 2026
8 checks passed
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.

2 participants