Skip to content

feat(sources): OpenCode adapter on RFC 002 contract - #1484

Open
jphein wants to merge 6 commits into
MemPalace:developfrom
techempower-org:pr/opencode-source-adapter-rfc002
Open

jphein wants to merge 6 commits into
MemPalace:developfrom
techempower-org:pr/opencode-source-adapter-rfc002

Conversation

@jphein

@jphein jphein commented May 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds OpenCodeSourceAdapter — an RFC 002 BaseSourceAdapter that ingests OpenCode AI-coding-CLI session transcripts from OpenCode's local SQLite store (~/.local/share/opencode/opencode.db) into the palace, formatted to match convo_miner's exchange-pair drawer shape.

What's included

  • mempalace/sources/opencode.py — 482-line adapter:
    • Yields SourceItemMetadata then DrawerRecords per session
    • One source_file per session shaped as opencode://<absolute-db-path>#session=<sid>
    • Chunks via convo_miner.chunk_exchanges exchange-pair shape
    • Declares 8 transformations (6 opencode-namespaced + 2 reserved); every name resolves to a reference implementation on mempalace.sources.transforms per RFC 002 §7.3
    • Schema fields: session_id, session_title, project_dir, session_created_at, message_count, extract_mode, opencode_db_path
    • supports_incremental + adapter_owns_routing; default_privacy_class = "pii_potential"
    • Routes wing from session.directory basename (or explicit options["wing"]); room from detect_convo_room; hall from _detect_hall_cached
  • mempalace/sources/transforms.py — +141 lines adding the 6 opencode-namespaced reference transformations
  • pyproject.toml — entry-point registration under [project.entry-points."mempalace.sources"] as opencode = "mempalace.sources.opencode:OpenCodeSourceAdapter"
  • tests/test_sources_opencode.py — 28 tests covering identity, capabilities, schema, ingest, route hints, transforms round-trip (RFC 002 §7.3 byte-equivalence), edge cases
  • tests/fixtures/opencode/sample_session_2026_05_12/build_fixture.py — SQLite-schema-verbatim fixture builder (mirrors opencode-ai 1.14.39's schema captured live from ~/.local/share/opencode/opencode.db); no recorded .db ships because real-session content is unsanitizable

Coordination

This work originated from the OpenCode SQLite spadework in @JakobSachs's #23 (DB-schema reverse engineering, session/message/part traversal, tool-input/tool-output stripping primitives) — rebuilt on the RFC 002 contract so OpenCode support can ship as a registered adapter rather than a normalize.py branch. Coordination thread at #23, comment posted offering three paths forward; pushing this PR as the working code per @JakobSachs's choice of how to land (see #23#issuecomment-4436116396). Co-authored credit on the commit.

Complementary to #297 (Milofax) which adds the consumer-side JS plugin (examples/opencode_auto_plugin.js) that auto-initializes MemPalace in OpenCode sessions. Together: this adapter ingests OpenCode sessions into the palace; #297's plugin queries the palace from OpenCode at runtime. Different layers, no overlap.

Built on top of #1014 (RFC 002 scaffolding by @igorls).

Test plan

  • 28/28 OpenCode adapter tests pass in 1.53s
  • Full suite 1876 passed / 7 skipped / 106 deselected — zero regressions (+22 net vs pre-adapter baseline)
  • Adapter conformance via RFC 002 §7.3 declared-transformation round-trip (drawer content reproducible from transformations)
  • Real-OpenCode-session smoke (deferred — fixture is schema-verbatim from a real install but doesn't ship recorded content; configuring an OpenCode provider for a billable real-session capture is non-blocking)

Notes

  • The 7 fork-only-on-jphein/main files (CLAUDE.md, README.md, FORK_CHANGELOG.md, docs/fork-changes.yaml, .claude-plugin/, docs/internal/, docs/research/*) are intentionally NOT in this diff — branch was rebuilt off upstream/develop carrying only commit 9030f06 so no fork-specific narrative leaks.

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings May 13, 2026 01:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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
Contributor

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 adds the OpenCodeSourceAdapter for ingesting SQLite-based CLI session transcripts, including custom text transformations and a synthetic fixture-based test suite. The review feedback recommends adding a missing version field to metadata for functional incremental ingests, respecting encapsulation of the PalaceContext, ensuring consistent timestamps across session chunks, and adhering to PEP 8 import standards.

Comment on lines +340 to +362
metadata = {
# Universal §5.1 fields
"source_file": src_file,
"chunk_index": chunk_index,
"filed_at": datetime.now(timezone.utc)
.replace(microsecond=0)
.isoformat()
.replace("+00:00", "Z"),
"added_by": "opencode-adapter",
"wing": wing,
"room": room,
"hall": _detect_hall(content),
"ingest_mode": "chunked_content",
"extract_mode": "exchange",
"privacy_class": self.default_privacy_class,
# Adapter-declared fields (§5.2)
"session_id": sid,
"session_title": title or "",
"project_dir": directory or "",
"session_created_at": created_iso,
"message_count": len(messages),
"opencode_db_path": db_path,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The metadata dictionary is missing the opencode_session_version field. This field is required by the is_current method (line 391) to perform stable version comparisons for incremental ingests. Without it, is_current will always fall back to returning True if any metadata exists, preventing the adapter from detecting and ingesting updates to existing sessions.

                    metadata = {
                        # Universal §5.1 fields
                        "source_file": src_file,
                        "chunk_index": chunk_index,
                        "filed_at": datetime.now(timezone.utc)
                        .replace(microsecond=0)
                        .isoformat()
                        .replace("+00:00", "Z"),
                        "added_by": "opencode-adapter",
                        "wing": wing,
                        "room": room,
                        "hall": _detect_hall(content),
                        "ingest_mode": "chunked_content",
                        "extract_mode": "exchange",
                        "privacy_class": self.default_privacy_class,
                        # Adapter-declared fields (§5.2)
                        "session_id": sid,
                        "session_title": title or "",
                        "project_dir": directory or "",
                        "session_created_at": created_iso,
                        "message_count": len(messages),
                        "opencode_db_path": db_path,
                        "opencode_session_version": str(time_updated or time_created or 0),
                    }

Comment thread mempalace/sources/opencode.py Outdated
Comment on lines +310 to +311
if palace._skip_requested:
palace._skip_requested = False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Accessing and modifying a private attribute (_skip_requested) of the PalaceContext violates encapsulation. If the adapter needs to check if the current item should be skipped (e.g., after a call to palace.skip_current_item()), the context should expose a public property or method for this purpose.

Comment thread mempalace/sources/opencode.py Outdated
Comment on lines +344 to +347
"filed_at": datetime.now(timezone.utc)
.replace(microsecond=0)
.isoformat()
.replace("+00:00", "Z"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The filed_at timestamp is generated inside the chunk loop. For consistency across all chunks of the same session, it is better to generate this timestamp once per session (outside the for chunk in chunks: loop).

                created_iso = _utc_iso(time_created or 0)
                filed_at = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
                for chunk in chunks:
                    content = chunk["content"]
                    chunk_index = int(chunk["chunk_index"])
                    metadata = {
                        # Universal §5.1 fields
                        "source_file": src_file,
                        "chunk_index": chunk_index,
                        "filed_at": filed_at,

Comment thread mempalace/sources/transforms.py Outdated
# what ``canonical_source_bytes`` returns to the conformance suite, and is
# what the chain of transformations below collapses into the drawer content.

import json as _json

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

According to PEP 8, imports should always be put at the top of the file, just after any module comments and docstrings, and before module globals and constants. Moving this import to the top of the module improves maintainability and follows standard Python conventions.

References
  1. Imports are always put at the top of the file, just after any module comments and docstrings, and before module globals and constants. (link)

jphein added a commit to techempower-org/mempalace that referenced this pull request May 13, 2026
…#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>
@jphein

jphein commented May 13, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed all 4 items from @gemini-code-assist's review in commit faee2b9:

  1. ✅ Added opencode_session_version to the chunk metadata so is_current() can do real version comparisons (was always falling back to "exists → current")
  2. ✅ Removed direct access to PalaceContext._skip_requested; added a small companion change — PalaceContext.is_skip_requested() public method — so adapters can short-circuit without touching the private flag. The spec's "core checks between yields" pattern doesn't hold for Python generators (the adapter's code runs between yields, not core's), so adapters need a way to query the flag. Read-only access only; core still owns the reset
  3. ✅ Hoisted filed_at (and session_version) outside the chunk loop — every chunk of the same session now shares the same filed_at timestamp
  4. ✅ Moved import json as _json to the top of transforms.py per PEP 8

Bonus: removed an unused import json from opencode.py that ruff caught.

Tests: 57 pass (28 opencode + 29 base sources); ruff clean.

@igorls

igorls commented May 13, 2026

Copy link
Copy Markdown
Member

Nice adapter — solid RFC 002 conformance, good test coverage, and the fixture-builder approach (no recorded .db) is the right privacy call.

Two things to address before merge:

1. CI lint is red — two ruff violations in tests/test_sources_opencode.py:

tests/test_sources_opencode.py:17:8: F401 `os` imported but unused
tests/test_sources_opencode.py:43:1: E402 Module level import not at top of file

The E402 comes from the sys.path.insert(0, ...) / import build_fixture pattern. Consider importlib.util.spec_from_file_location + module_from_spec to load the fixture builder without mutating sys.path at module scope (which also fixes the E402).

2. Route-hint wing mismatch_route_hint_for() (called at the SourceItemMetadata stage) computes the wing purely from directory, but _wing_for() (called at the DrawerRecord stage) respects source.options["wing"] first (RFC 002 §2.5 precedence). When a user passes options={"wing": "Custom Wing"}, the metadata hint says "frontend" while the actual drawers say "custom_wing". This could cause core to make wrong skip/routing decisions.

Fix: have _route_hint_for accept the source parameter and delegate to _wing_for, or inline the same precedence logic.

Minor (non-blocking): the AuthRequiredError import on line 62 has a # noqa: F401 claiming re-export, but nothing in __all__ or the module surface actually re-exports it — the suppression is unjustified.

jphein added a commit to techempower-org/mempalace that referenced this pull request May 13, 2026
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>
@jphein

jphein commented May 13, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks @igorls — addressed all four in commit 13353d9:

  1. ✅ Dropped unused os import (F401)
  2. ✅ Refactored the fixture builder loader to importlib.util.spec_from_file_location + module_from_spec per your suggestion. No more sys.path mutation at module scope; E402 gone. Also registers the loaded module in sys.modules so dataclasses/typing introspection inside build_fixture can resolve cls.__module__ (without that registration, dataclass collection fails at test-collection time — found out the hard way).
  3. _route_hint_for(source, directory) now delegates to _wing_for so both stages apply identical RFC 002 §2.5 precedence. The metadata hint and drawer wing will agree when callers pass options={"wing": ...}.
  4. ✅ Dropped the AuthRequiredError import + unjustified # noqa: F401__all__ was never going to re-export it.

Tests: 57 pass (28 opencode + 29 base sources); ruff clean on all touched files.

@jphein jphein added area/mining File and conversation mining enhancement New feature or request labels May 13, 2026
jphein pushed a commit to techempower-org/mempalace that referenced this pull request May 15, 2026
1. corpus_full must hold session text in turn mode (HIGH, hybrid_v2/v3/v4)
   --------------------------------------------------------------
   The turn branch wrote only the single user turn into corpus_full,
   so the assistant-reference two-pass (Pass 2 queries corpus_full
   for quoted/assistant content) had nothing to match against in
   turn granularity. Fix: corpus_full now duplicates the full
   session text per user turn while corpus_user keeps the granular
   per-turn signal. Removed the now-stale "session-level boosts
   compensate" comment — they do not, the two-pass quoted match
   reads corpus_full directly.

2. run_sweep.sh: awk field index for single-digit @k (MEDIUM)
   ----------------------------------------------------------
   "Recall@ 1:" prints with a space, so the value lands in $3, not
   $2. The previous script wrote literal "1:" / "5:" into the CSV.
   rebuild_csv.sh already used $3 (which is why the committed CSV
   is correct), but run_sweep.sh would corrupt any fresh CSV.

3. Absolute paths broke portability (MEDIUM)
   ------------------------------------------
   run_sweep.sh, run_turn_sweep.sh, rebuild_csv.sh all hard-coded
   /Users/macmini/Projects/mempalace. Switched to BASH_SOURCE-based
   repo root resolution. DATA path moved behind an env var with a
   sensible default and a clear error when the file is missing.
jphein added a commit to techempower-org/mempalace that referenced this pull request May 22, 2026
…daemon-routed integration recipe (#106)

* feat(sources): OpenCode adapter on RFC 002 contract

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>

* fix(sources/opencode): address Gemini Code Assist review on MemPalace#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>

* fix(sources/opencode): address @igorls review on MemPalace#1484

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>

* style(sources/opencode): ruff format with CI's ruff 0.4.x

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>

* style(tests/fixtures/opencode): ruff format build_fixture.py with 0.4.x

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.

* feat: add OpenCode MCP integration for MemPalace

* fix: use python -m mempalace.mcp_server for robustness

* docs(integrations): OpenCode integration recipe + cherry-pick fork-changes 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>

* docs(changelog): add opencode-integration-recipe entry pointing at 60dc9e6

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>

* feat(opencode-integration): bundled live-capture plugin + split option-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>

* docs(changelog): add commit ref for opencode-live-capture-plugin entry

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>

---------

Co-authored-by: Jakob Sachs <28728963+JakobSachs@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Dxrk System <dxrk@local>
jphein and others added 5 commits May 22, 2026 08:46
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.
@jphein
jphein force-pushed the pr/opencode-source-adapter-rfc002 branch from 9d6b31a to acc233f Compare May 22, 2026 15:46
Upstream's CI now pins ruff to 0.15.9 (was 0.4.x when this PR was last
pushed). The format rules tightened in between; `ruff format --check .`
flags the file. Run `ruff format` against the current CI-pinned
version.

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

jphein commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator Author

Hi @igorls — friendly nudge on this one when you have a moment. I addressed everything from your review in 13353d9:

  • Dropped the unused os import (F401) and refactored the fixture-builder loader to importlib.util.spec_from_file_location + module_from_spec — no more sys.path mutation at module scope, E402 gone.
  • _route_hint_for(source, directory) now delegates to _wing_for, so the metadata hint and the drawer wing apply identical RFC 002 §2.5 precedence when callers pass options={"wing": ...}.
  • Removed the unjustified AuthRequiredError # noqa: F401.

57 tests pass, ruff clean, and it still merges cleanly on current develop. I've had a few PRs land since (e.g. #1769 into develop on the 22nd), so I'm around to iterate quickly if anything's drifted. Would love another look.

ggettert added a commit to ggettert/mempalace that referenced this pull request Jul 6, 2026
Add OpenClawSourceAdapter on the RFC 002 BaseSourceAdapter contract,
ingesting OpenClaw agent-session trajectory files
(~/.openclaw/agents/<agent>/sessions/*.trajectory.jsonl) into the palace.

- source_file scheme: openclaw://<path>#session=<session-id>
- prompt.submitted -> user turn, model.completed -> assistant turn
- declared transforms: openclaw_extract_turns, _strip_runtime_context,
  _strip_metadata_preamble (reference impls in transforms.py)
- reuses convo_miner.chunk_exchanges for exchange-pair chunking
- is_current via last-event version; default_privacy_class pii_potential
- synthetic trajectory fixtures in tests (no recorded sessions committed)
- registered via mempalace.sources entry point
- skip check uses is_skip_requested() when present, falls back to the
  underlying flag so it works on develop before PR MemPalace#1484 merges

Modeled on the OpenCode adapter (MemPalace#1484). Tracks MemPalace#1943 / RFC 002 MemPalace#989.
ggettert added a commit to ggettert/mempalace that referenced this pull request Jul 21, 2026
Add OpenClawSourceAdapter on the RFC 002 BaseSourceAdapter contract,
ingesting OpenClaw agent-session trajectory files
(~/.openclaw/agents/<agent>/sessions/*.trajectory.jsonl) into the palace.

- source_file scheme: openclaw://<path>#session=<session-id>
- prompt.submitted -> user turn, model.completed -> assistant turn
- declared transforms: openclaw_extract_turns, _strip_runtime_context,
  _strip_metadata_preamble (reference impls in transforms.py)
- reuses convo_miner.chunk_exchanges for exchange-pair chunking
- is_current via last-event version; default_privacy_class pii_potential
- synthetic trajectory fixtures in tests (no recorded sessions committed)
- registered via mempalace.sources entry point
- skip check uses is_skip_requested() when present, falls back to the
  underlying flag so it works on develop before PR MemPalace#1484 merges

Modeled on the OpenCode adapter (MemPalace#1484). Tracks MemPalace#1943 / RFC 002 MemPalace#989.
geco added a commit to geco/mempalace that referenced this pull request Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/mining File and conversation mining enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants