Conversation
There was a problem hiding this comment.
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.
| 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, | ||
| } |
There was a problem hiding this comment.
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),
}| if palace._skip_requested: | ||
| palace._skip_requested = False |
There was a problem hiding this comment.
| "filed_at": datetime.now(timezone.utc) | ||
| .replace(microsecond=0) | ||
| .isoformat() | ||
| .replace("+00:00", "Z"), |
There was a problem hiding this comment.
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,| # 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 |
There was a problem hiding this comment.
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
- Imports are always put at the top of the file, just after any module comments and docstrings, and before module globals and constants. (link)
…#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>
|
Addressed all 4 items from @gemini-code-assist's review in commit faee2b9:
Bonus: removed an unused Tests: 57 pass (28 opencode + 29 base sources); ruff clean. |
|
Nice adapter — solid RFC 002 conformance, good test coverage, and the fixture-builder approach (no recorded Two things to address before merge: 1. CI lint is red — two ruff violations in The E402 comes from the 2. Route-hint wing mismatch — Fix: have Minor (non-blocking): the |
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>
|
Thanks @igorls — addressed all four in commit 13353d9:
Tests: 57 pass (28 opencode + 29 base sources); ruff clean on all touched files. |
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.
…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>
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.
9d6b31a to
acc233f
Compare
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>
|
Hi @igorls — friendly nudge on this one when you have a moment. I addressed everything from your review in
57 tests pass, ruff clean, and it still merges cleanly on current |
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.
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.
Summary
Adds
OpenCodeSourceAdapter— an RFC 002BaseSourceAdapterthat ingests OpenCode AI-coding-CLI session transcripts from OpenCode's local SQLite store (~/.local/share/opencode/opencode.db) into the palace, formatted to matchconvo_miner's exchange-pair drawer shape.What's included
mempalace/sources/opencode.py— 482-line adapter:SourceItemMetadatathenDrawerRecords per sessionsource_fileper session shaped asopencode://<absolute-db-path>#session=<sid>convo_miner.chunk_exchangesexchange-pair shapemempalace.sources.transformsper RFC 002 §7.3session_id,session_title,project_dir,session_created_at,message_count,extract_mode,opencode_db_pathsupports_incremental+adapter_owns_routing;default_privacy_class = "pii_potential"session.directorybasename (or explicitoptions["wing"]); room fromdetect_convo_room; hall from_detect_hall_cachedmempalace/sources/transforms.py— +141 lines adding the 6 opencode-namespaced reference transformationspyproject.toml— entry-point registration under[project.entry-points."mempalace.sources"]asopencode = "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 casestests/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.dbships because real-session content is unsanitizableCoordination
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.pybranch. 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
Notes
upstream/developcarrying only commit9030f06so no fork-specific narrative leaks.🤖 Generated with Claude Code