Skip to content

feat: add OpenCode SQLite session database support - #23

Open
JakobSachs wants to merge 2 commits into
MemPalace:developfrom
JakobSachs:feat/opencode-support
Open

feat: add OpenCode SQLite session database support#23
JakobSachs wants to merge 2 commits into
MemPalace:developfrom
JakobSachs:feat/opencode-support

Conversation

@JakobSachs

@JakobSachs JakobSachs commented Apr 7, 2026

Copy link
Copy Markdown

Played around with the project a little, but also wanted to carry over my Opencode sessions. The patch should hopefully be as minimal as possible.

Had to do this tmp-dir workaround to get the same directory-tree behaviour as with the other tools, trying not to touch too much of the existing logic. So it uses the dir-column in the DB to create a tmp-dir structure.
The table:

   session table (SQLite)
   ┌──────────┬─────────────────────┐
   │ id       │ directory           │
   ├──────────┼─────────────────────┤
   │ ses_a1b2 │ /home/user/frontend │
   │ ses_c3d4 │ /home/user/frontend │
   │ ses_e5f6 │ /home/user/backend  │
   │ ses_g7h8 │ /home/user/my-infra │
   └──────────┴─────────────────────┘

becomes

mempalace_oc_*/
├── frontend/
│   ├── frontend_ses_a1b2.txt
│   └── frontend_ses_c3d4.txt
├── backend/
│   └── backend_ses_e5f6.txt
└── my_infra/
    └── my_infra_ses_g7h8.txt

which is then handled the same way how claude-code/chatgpt/etc. ingestion works.


Ingest should work via:

mempalace mine ~/.local/share/opencode/opencode.db --mode convos

Tests + Lint should be clean

@thecodenomad

Copy link
Copy Markdown

Just curious, what version of opencode are you running? For me, this change couldn't find anything with opencode v1.3.10

@JakobSachs

Copy link
Copy Markdown
Author

Just curious, what version of opencode are you running? For me, this change couldn't find anything with opencode v1.3.10

Using v1.3.13 on macOS, (mind you im not sure where opencode saves the db on other platforms..)

Did you get any error or just no-output/nothing found ?

IgorTavcar added a commit to IgorTavcar/mempalace that referenced this pull request Apr 7, 2026
Cherry-picked from upstream PR MemPalace#23 (JakobSachs).
- Support mining OpenCode CLI sessions from their SQLite DB
- New normalize_opencode_sessions() in normalize.py
- Usage: `mempalace mine ~/.../opencode.db --mode convos`
- Reuses existing conversation mining pipeline
- 30 new tests

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

Copy link
Copy Markdown

Just curious, what version of opencode are you running? For me, this change couldn't find anything with opencode v1.3.10

Using v1.3.13 on macOS, (mind you im not sure where opencode saves the db on other platforms..)

Did you get any error or just no-output/nothing found ?

No errors, it just didn't find any data, or at least it reported it found 0 files.

I'll update just to be sure and give it another go.

NOTE: On Linux I do see the db at ~/.local/share/opencode/opencode.db

.local/share/opencode
❯ ls
auth.json  log          opencode.db-shm  snapshot  tool-output
bin        opencode.db  opencode.db-wal  storage

@myInstagramAlternative

Copy link
Copy Markdown
opencode db path

@bensig

bensig commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

Cool feature but conflicts with main. Can you rebase if you'd like to continue? We'd be interested in OpenCode support.

@JakobSachs
JakobSachs force-pushed the feat/opencode-support branch from 74e9c01 to 91a3c5e Compare April 7, 2026 21:26
@JakobSachs

JakobSachs commented Apr 7, 2026

Copy link
Copy Markdown
Author

Cool feature but conflicts with main. Can you rebase if you'd like to continue? We'd be interested in OpenCode support.

done, rebased 👍

@JakobSachs

JakobSachs commented Apr 7, 2026

Copy link
Copy Markdown
Author

Just curious, what version of opencode are you running? For me, this change couldn't find anything with opencode v1.3.10

Using v1.3.13 on macOS, (mind you im not sure where opencode saves the db on other platforms..)
Did you get any error or just no-output/nothing found ?

No errors, it just didn't find any data, or at least it reported it found 0 files.

I'll update just to be sure and give it another go.

NOTE: On Linux I do see the db at ~/.local/share/opencode/opencode.db

.local/share/opencode
❯ ls
auth.json  log          opencode.db-shm  snapshot  tool-output
bin        opencode.db  opencode.db-wal  storage

Actually, found the issue with this: For some reason on my one Mac OpenCode has a different db path, on my private one it also uses the XDG data dir path (like on linux).

pushed a small fix to search both places so we catch all of it now 👍

@thecodenomad

thecodenomad commented Apr 7, 2026

Copy link
Copy Markdown

Just curious, what version of opencode are you running? For me, this change couldn't find anything with opencode v1.3.10

Using v1.3.13 on macOS, (mind you im not sure where opencode saves the db on other platforms..)
Did you get any error or just no-output/nothing found ?

No errors, it just didn't find any data, or at least it reported it found 0 files.
I'll update just to be sure and give it another go.
NOTE: On Linux I do see the db at ~/.local/share/opencode/opencode.db

.local/share/opencode
❯ ls
auth.json  log          opencode.db-shm  snapshot  tool-output
bin        opencode.db  opencode.db-wal  storage

Actually, found the issue with this: For some reason on my one Mac OpenCode has a different db path, on my private one it also uses the XDG data dir path (like on linux).

pushed a small fix to search both places so we catch all of it now 👍

Thank you! Just tried again, it worked for me! I did noticed that there was an unrelated failure:

Failed to send telemetry event CollectionGetEvent: capture() takes 1 positional argument but 3 were given

This appears to be due to a breaking change with posthog, but pinning it posthog = ">=2.4.0,<6.0.0" fixes it *(Opened #163 for that issue)

Nice work!

mining opencode convo

=======================================================
  MemPalace Mine — Conversations
=======================================================
  Wing:    step1
  Source:  /tmp/mempalace_oc_eszqwao7/step1
  Files:   4
  Palace:  /var/home/user/.local/distrobox_homes/cppdev/.mempalace/palace
-------------------------------------------------------

Before establishing client
Before client.get_collection
  ✓ [   1/4] step1_ses_330da6dbcffed2cb9JowsSAxJK.txt           +29
  ✓ [   2/4] step1_ses_335b2b72bffeS76Nj6gG2702Kw.txt           +11
  ✓ [   3/4] step1_ses_335bdf907ffeYLloh40zvEWBIl.txt           +4
  ✓ [   4/4] step1_ses_335dc12b1ffeZ1KP1hY8UDtjfv.txt           +8

=======================================================
  Done.
  Files processed: 4
  Files skipped (already filed): 0
  Drawers filed: 52

  By room:
    technical            3 files
    decisions            1 files

  Next: mempalace search "what you're looking for"
=======================================================

@bgauryy

bgauryy commented Apr 8, 2026

Copy link
Copy Markdown

PR Review: feat: add OpenCode SQLite session database support

Executive Summary

Aspect Value
PR Goal Add OpenCode SQLite session database as a 6th supported conversation format for mining
Files Changed 7
Risk Level 🟡 MEDIUM - New I/O path with SQLite, has a Python 3.9 compat crash and a resource leak
Review Effort 3/5 - Moderate: new data source integration, SQL queries, temp-dir recursion pattern
Recommendation 🔄 REQUEST_CHANGES

Affected Areas: mempalace/normalize.py (core), mempalace/convo_miner.py (integration), tests/test_normalize.py, mempalace/cli.py, READMEs, uv.lock

Business Impact: Enables OpenCode users to mine their AI coding sessions into MemPalace — expands supported tool ecosystem.

Flow Changes: Adds an early-return path in mine_convos() for .db files that bypasses the normal scan_convos()normalize() loop, creating temp directories grouped by project before recursing back into mine_convos().

Ratings

Aspect Score
Correctness 3/5
Security 5/5
Performance 4/5
Maintainability 3/5

PR Health

  • Has clear description
  • References ticket/issue (not applicable — community feature contribution)
  • Appropriate size (201 additions is reasonable)
  • Has relevant tests (single happy-path test only)

High Priority Issues

🐛 #1: str | None union syntax crashes on Python 3.9

Location: mempalace/normalize.py_resolve_opencode_db() and normalize_opencode_sessions() | Confidence: ✅ HIGH

The project declares requires-python = ">=3.9" in pyproject.toml and lists Python 3.9 in classifiers. The str | None union syntax for type hints is only available at runtime from Python 3.10+. On Python 3.9 this raises TypeError: unsupported operand type(s) for |: 'type' and 'NoneType' at import time, breaking the entire normalize module.

- def _resolve_opencode_db(db_path: str | None = None) -> str:
+ def _resolve_opencode_db(db_path: Optional[str] = None) -> str:
- def normalize_opencode_sessions(db_path: str | None = None) -> List[dict]:
+ def normalize_opencode_sessions(db_path: Optional[str] = None) -> List[dict]:

Optional is already imported in the file — this is a straightforward fix.


🐛 #2: SQLite connection leak on exception in main processing loop

Location: mempalace/normalize.pynormalize_opencode_sessions() | Confidence: ✅ HIGH

The SQLite connection is opened with conn = sqlite3.connect(resolved) and manually closed in two specific error branches (missing tables, no json_extract). However, if the main session-processing loop throws any exception (e.g., malformed JSON in json_extract, unexpected None in unpacking), conn.close() is never called. This leaks the connection and can leave the database file locked.

- def normalize_opencode_sessions(db_path: Optional[str] = None) -> List[dict]:
-     resolved = _resolve_opencode_db(db_path)
-     try:
-         conn = sqlite3.connect(resolved)
-         tables = { ... }
-     except Exception as e:
-         raise IOError(...)
-
-     if not {"session", "message", "part"}.issubset(tables):
-         conn.close()
-         raise IOError(...)
-     try:
-         conn.execute("SELECT json_extract('{}', '$')")
-     except Exception:
-         conn.close()
-         raise IOError(...)
-     sessions = conn.execute(...).fetchall()
-     results = []
-     for sid, title, directory, created in sessions:
-         ...  # ← exception here = connection leaked
-     conn.close()
-     return results
+ def normalize_opencode_sessions(db_path: Optional[str] = None) -> List[dict]:
+     resolved = _resolve_opencode_db(db_path)
+     try:
+         conn = sqlite3.connect(resolved)
+     except Exception as e:
+         raise IOError(f"Could not read SQLite database: {resolved}: {e}")
+
+     try:
+         tables = {
+             r[0] for r in conn.execute(
+                 "SELECT name FROM sqlite_master WHERE type='table'"
+             ).fetchall()
+         }
+         if not {"session", "message", "part"}.issubset(tables):
+             raise IOError(f"Not a recognized OpenCode database (missing tables): {resolved}")
+         conn.execute("SELECT json_extract('{}', '$')")
+
+         sessions = conn.execute(...).fetchall()
+         results = []
+         for sid, title, directory, created in sessions:
+             ...
+         return results
+     except IOError:
+         raise
+     except Exception as e:
+         raise IOError(f"Error reading OpenCode database: {resolved}: {e}")
+     finally:
+         conn.close()

Medium Priority Issues

🔄 #3: Dual processing path — .db files behave differently when found via directory scan

Location: mempalace/convo_miner.py:276 + mempalace/normalize.py:44 | Confidence: ✅ HIGH

Adding .db/.sqlite3/.sqlite to CONVO_EXTENSIONS means scan_convos() will discover these files during directory walks. However, the two discovery paths produce very different results:

Path Trigger Behavior
Direct .db file mempalace mine opencode.db --mode convos Early return → per-project temp dirs → grouped mining
.db in directory mempalace mine ~/chats/ --mode convos scan_convos() picks it up → normalize()_normalize_opencode_sqlite() → ALL sessions concatenated into one blob → single wing/room

The directory-scan path loses project grouping and dumps all sessions into one flat transcript. Two options:

Option A — Remove .db/.sqlite3/.sqlite from CONVO_EXTENSIONS so only explicit file paths trigger OpenCode mining:

  CONVO_EXTENSIONS = {
      ".txt",
      ".md",
      ".json",
      ".jsonl",
-     ".db",
-     ".sqlite3",
-     ".sqlite",
  }

Option B — Handle .db files specially in the file-processing loop (check inside the for i, filepath in enumerate(files, 1) loop and delegate to the same temp-dir approach).

Option A is simpler and avoids surprising behavior. Users would need to point directly at the .db file, which is clearer.


🚨 #4: _extract_opencode_messages type hint mismatch

Location: mempalace/normalize.py_extract_opencode_messages() | Confidence: ⚠️ MED

The parameter session_id: str = None declares type str but defaults to None. Should be Optional[str] = None for consistency with the type system and to avoid mypy/pyright warnings.

- def _extract_opencode_messages(
-     conn: sqlite3.Connection, session_id: str = None
- ) -> List[Tuple[str, str]]:
+ def _extract_opencode_messages(
+     conn: sqlite3.Connection, session_id: Optional[str] = None
+ ) -> List[Tuple[str, str]]:

🚨 #5: Error message uses input db_path instead of resolved path

Location: mempalace/normalize.pynormalize_opencode_sessions() | Confidence: ✅ HIGH

When the database is missing required tables, the error message prints the original db_path parameter (which could be None if called without arguments) instead of the resolved path:

- raise IOError(f"Not a recognized OpenCode database (missing tables): {db_path}")
+ raise IOError(f"Not a recognized OpenCode database (missing tables): {resolved}")

Low Priority Issues

🎨 #6: Missing space after # in comment

Location: mempalace/normalize.py — above OPENCODE_DB_PATHS | Confidence: ✅ HIGH

PEP 8 requires a space after # in inline comments.

- #List of known opencode db paths
+ # List of known opencode db paths

🎨 #7: Test coverage is thin — only happy path

Location: tests/test_normalize.py | Confidence: ⚠️ MED

The single test test_opencode_sqlite covers only the happy path (valid DB with one session). Missing test scenarios:

  • Database without required tables (should raise IOError)
  • Empty sessions (< 2 messages, should be skipped)
  • Multiple sessions across different project directories
  • The convo_miner.py temp-dir recursive mining flow
  • json_extract unavailability error path
  • Tool-call filtering (messages starting with "Called the ")

Not blocking, but the lack of error-path coverage means the resource leak (#2) could go unnoticed.


Flow Impact Analysis

User passes .db directly          User passes directory containing .db
          │                                       │
          ▼                                       ▼
  mine_convos()                            mine_convos()
  convo_path.is_file() ─── True            convo_path.is_file() ─── False
          │                                       │
          ▼                                       ▼
  normalize_opencode_sessions()             scan_convos() finds .db
  returns List[dict] per session            adds to files list
          │                                       │
          ▼                                       ▼
  Write to temp dirs by project             normalize() per file
  ┌── project_a/                                  │
  │   ├── session1.txt                            ▼
  │   └── session2.txt              _normalize_opencode_sqlite()
  └── project_b/                    concatenates ALL sessions
      └── session3.txt              into ONE transcript blob
          │                                       │
          ▼                                       ▼
  Recursive mine_convos()            chunk_exchanges() on blob
  per project subdir                 → single wing, single room
  → proper wing per project          → project grouping LOST

Created by Octocode MCP https://octocode.ai 🔍🐙

@bensig

bensig commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

@JakobSachs pls see robot review above

@web3guru888 web3guru888 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.

Review of #23feat: add OpenCode SQLite session database support

Scope: +201/−7 · 7 file(s)

  • README.md (modified: +2/−2)
  • mempalace/README.md (modified: +1/−1)
  • mempalace/cli.py (modified: +1/−1)
  • mempalace/convo_miner.py (modified: +34/−1)
  • mempalace/normalize.py (modified: +123/−2)
  • tests/test_normalize.py (modified: +30/−0)
  • uv.lock (modified: +10/−0)

Issues

  • ⚠️ Hardcoded filesystem path — breaks portability

Suggestions

  • Magic number(s) 1000 — consider extracting to named constant(s)

Strengths

  • ✅ Includes test coverage

🟡 Minor items — good work overall, a few things to address.


🏛️ Reviewed by MemPalace-AGI · Autonomous research system with perfect memory · Showcase: Truth Palace of Atlantis

@bensig
bensig changed the base branch from main to develop April 11, 2026 22:23
@igorls igorls added enhancement New feature or request area/mining File and conversation mining labels Apr 14, 2026
bensig added a commit that referenced this pull request Apr 18, 2026
Draft plugin specification for source adapters, mirroring RFC 001's
role for storage backends. Formalizes the contract six community
ingester PRs (#274, #23, #169, #232, #567, #98, #702) plus #981's
metadata-only mode have been reinventing ad-hoc, so adapter authors
can build to a stable surface.

Key decisions:
- Single ingest() method; lazy adapters yield SourceItemMetadata
  ahead of drawers, eager adapters interleave
- Declared-transformation model (§1.4) replaces informal verbatim
  promise with a verifiable one; byte_preserving adapters declare
  the empty set, declared_lossy adapters enumerate. Existing
  miner.py and the convo_miner+normalize pipeline map cleanly
- Palace is the incremental cursor via is_current(item, metadata);
  no sidecar persistence
- Routing is adapter-owned; detect_room/detect_hall move into the
  filesystem adapter
- Flat metadata per ChromaDB (RFC 001 §1.4) — entity hints as
  json_string field, KG triples route to SQLite knowledge graph
- Closets stay core-built as a post-step; adapters may emit flat
  closet_hints. Closes existing gap where convo drawers get no
  closets
- No per-drawer field renames: source_file, filed_at, source_mtime,
  added_by, normalize_version, entities, ingest_mode all preserved.
  Spec adds adapter_name, adapter_version, privacy_class

§9 enumerates the cleanup PR prerequisites (mempalace/sources/
module, PalaceContext facade, KnowledgeGraph.add_triple gaining
backwards-compatible source_drawer_id + adapter_name params).

Tracking issue: #989
jphein referenced this pull request in techempower-org/mempalace Apr 30, 2026
…#232 Cursor, #169 Pi, MemPalace#702 Cursor+factory.ai)

Updates the multi-agent-support bullet to cite the actual upstream
work instead of just gesturing at it. RFC 002 itself is PR MemPalace#990
(tracking issue MemPalace#989). Existing third-party prototypes already
proposed against the spec:

* OpenCode SQLite — PR #23
* Cursor SQLite — issue #274
* Cursor JSONL (earlier variant) — PR #232
* Pi agent JSONL — PR #169
* Combined Cursor + factory.ai — PR MemPalace#702

Each becomes a mempalace-source-<agent> package once RFC 002 lands.
Names the path explicitly: fork unblocks the pattern by helping land
RFC 002; per-agent adapter PRs land from their respective authors.

Aider, Gemini CLI, Codex CLI, and Warp are roadmap targets without
existing adapter PRs and are listed as such (no fabricated PR refs).

https://claude.ai/code/session_01GvwducFnFtN8KYmfbWKMR6
@igorls

igorls commented May 8, 2026

Copy link
Copy Markdown
Member

Hi, thanks for the contribution.

This PR has merge conflicts with develop, and the branch has not been updated in over 7 days, which puts it before our most recent release. The conflicts are likely against work that landed in that release.

Could you rebase onto develop so we can take another look?

If this change is no longer relevant, feel free to close the PR.

(This message is part of a periodic backlog pass, sent to all open PRs that match this state.)

@igorls igorls added the needs-rebase PR has merge conflicts with develop and needs rebase label May 8, 2026
@jphein

jphein commented May 13, 2026

Copy link
Copy Markdown
Collaborator

Hi @JakobSachs — thanks for kicking off OpenCode support here, the DB-schema spadework (session / message / part with json_extract, the role-coerce and tool-echo / file-injection skips) is what gave me a clean read on the data when I picked this up.

I'm working in @jphein's fork on a version that targets the RFC 002 source-adapter contract (#990 / #989) — BaseSourceAdapter, declared transformations, describe_schema(), entry-point registration. The shape is different from #23, but the OpenCode-specific extraction logic is essentially what you wrote.

Three ways I could see this going, easiest to hardest from your side:

  1. Co-authored — I cherry-pick the schema/extraction logic from your branch with Co-authored-by: on the commit, you get credit on the merge.
  2. feat: add OpenCode SQLite session database support #23 lands first — you carry feat: add OpenCode SQLite session database support #23 over the rebase and merge as-is; I rework the RFC 002 version on top as a follow-up cleanup.
  3. Something else — happy to defer to whatever you'd prefer.

No rush on this — I'm doing local prototyping in parallel so nothing is blocked on a response. Mostly want to make sure the coordination isn't a surprise.

jjcav84 pushed a commit to jjcav84/mempalace that referenced this pull request Aug 2, 2026
…purge

MemPalace#104 (CRITICAL, data-loss): the sweeper writes drawers with no
extract_mode at all (ingest_mode="sweep"). _metadata_matches_extract_mode's
legacy-compat rule -- "no extract_mode means treat as a legacy exchange
row" -- couldn't tell that apart from a genuine pre-schema convo_miner
row, so mempalace mine --mode convos (default extract=exchange) swept
every sweeper drawer for a shared transcript into its purge scope and
deleted them on the very next re-mine. The legacy-compat fallback now
only applies when the drawer is otherwise convo_miner's own (no
ingest_mode at all, or convo_miner's own "convos" tag) -- a drawer
positively identified as another producer's (sweep, or any other
foreign ingest_mode) never matches, using the same ingest_mode
discriminator sync.py already relies on for its own registry-row check.

MemPalace#105 (MEDIUM, silent-failure): convo_miner's own instance of the
purge-failure swallow already fixed for miner.py at MemPalace#23 -- a failed
purge in _file_chunks_locked was logged at debug level and mining
proceeded anyway, silently producing duplicate/stale drawers under
mixed schema versions. Now aborts (returns skipped=True, leaving the
old drawers' stored mtime untouched so the next mine retries) and
prints a visible warning.

428 tests pass across test_palace.py/test_convo_miner*.py/test_miner.py/
test_sweeper.py/test_hallways.py/test_format_miner.py/test_repair.py, no
regressions.
pull Bot referenced this pull request in nenyatech-mirror/mempalace Aug 12, 2026
#21 (CRITICAL, data-loss): multi-batch re-mine had no completion marker.
A mid-file crash after batch 1 committed but before a later batch left
permanently silent partial data -- the surviving drawers shared the
file's unchanged on-disk mtime, so file_already_mined() treated the
file as fully mined forever. Every chunk now carries chunk_total, and
file_already_mined() verifies a matching-mtime group's drawer count
reaches chunk_total before reporting True. Drawers with no chunk_total
(legacy rows, single-shot add_drawer()) are trusted as before.

#22 (HIGH, correctness/TOCTOU): source_mtime was captured via a fresh
os.path.getmtime() well after content was read, chunked, and
room-detected. A file appended to in that window got its new drawers
stamped with an mtime that already matched the (now newer) on-disk
state, so the appended tail was silently, permanently skipped on every
future mine. _read_text_no_follow now returns (content, mtime) from
the same fstat() that validates the file; process_file threads that
single value through instead of re-stating.

#23 (HIGH, silent-failure): a failed stale-drawer purge was swallowed
to a debug log and mining proceeded anyway, silently producing
duplicate or orphaned drawers. A purge failure now aborts this file's
mine attempt (old drawers' stored mtime is untouched, so the next mine
still sees a mismatch and retries) and prints a visible warning,
matching every other degraded path in this module.

#24 (LOW, data-loss): the old-drawer delete ran unconditionally, but
the closet purge+rebuild only ran when drawers_added > 0 -- a file
whose chunks all landed below min_chunk_size after boundary-splitting
lost its drawers but kept stale closets pointing at now-deleted IDs.
purge_file_closets now runs whenever the delete-and-rebuild cycle
does, regardless of the new chunk count; only the rebuild itself stays
conditional.

169 tests pass across test_miner.py/test_convo_miner*.py/test_palace.py/
test_hallways.py/test_format_miner.py/test_miner_fts5_validation.py, no
regressions. Full suite: 1 unrelated pre-existing flake in
test_mcp_server.py (module-global peer-writer-lock state leaking across
test files in full-suite ordering -- passes standalone and as a full
file; the diff here never touches mcp_server.py).
offendingcommit added a commit to offendingcommit/mempalace that referenced this pull request Aug 16, 2026
* fix(mcp_server): reset chromadb System cache on staleness reconnect (MemPalace#2002)

_get_client() detects a peer writer's inode/mtime change and rebuilds the
client via ChromaBackend.make_client(), but chromadb caches its System (and
the live in-memory HNSW segment) keyed by path. The rebuilt client is handed
back the same stale segment, which on its next _persist() overwrites the
on-disk index, destroying records other writers had already indexed. Observed
in a live multi-writer palace: the persisted index count went backwards (4 to 3).

Call the existing _force_chroma_cache_reset() on the staleness path, before
make_client(), so chromadb rebuilds the segment from the on-disk state. The
call is guarded by the existing inode_changed/mtime_changed check, so it has
no effect on first-open.

Adds test_get_client_resets_chroma_system_cache_on_reconnect, which asserts the
reset runs before make_client on an mtime reconnect (fails without the fix).

Refs MemPalace#1963.

* fix(chroma): reset chromadb System cache in ChromaBackend._client() on inode/mtime reopen

_client() reconstructs PersistentClient on an inode/mtime change but did not drop chromadb's process-global SharedSystemClient cache first, so the rebuilt client reused the stale path-keyed System (and its in-memory HNSW segment) and could persist an outdated index over on-disk changes -- the same class as MemPalace#2002, reached via _client() instead of _get_client.

Add SharedSystemClient.clear_system_cache() to the external-change branch of _client(), mirroring mcp_server._force_chroma_cache_reset (MemPalace#2026) and repair._close_chroma_handles. Backend-level regression test asserts the reset fires on the change reopen, strictly before the reconstruct, and not on first open (chroma-core/chroma#2536, #5843).

Fixes MemPalace#2028.

* test(chroma): reformat test_backends.py to satisfy ruff format

Two monkeypatch.setattr calls were wrapped across lines that fit within
the line length; ruff format --check flagged them. Formatter-only, no
behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gg6g5efZ1rNbBTHqGz2Tjw

* test(mcp_server): re-acquire closets handle after delete_by_source

The MemPalace#2002 staleness reconnect makes _get_client() call
_force_chroma_cache_reset(), which clears chromadb's path-keyed
SharedSystemClient cache. Two TestDeleteBySource tests grabbed a
closets collection handle *before* calling tool_delete_by_source and
then asserted on it afterwards, by which point the reset had dropped
the Rust binding underneath the handle (AttributeError:
'RustBindingsAPI' object has no attribute 'bindings').

Re-acquire the closets collection after the tool call in both tests.
Production callers already re-acquire fresh handles per call, so this
is a test-lifetime issue, not a regression in the fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gg6g5efZ1rNbBTHqGz2Tjw

* fix(miner): close four re-mine safety gaps in process_file

MemPalace#21 (CRITICAL, data-loss): multi-batch re-mine had no completion marker.
A mid-file crash after batch 1 committed but before a later batch left
permanently silent partial data -- the surviving drawers shared the
file's unchanged on-disk mtime, so file_already_mined() treated the
file as fully mined forever. Every chunk now carries chunk_total, and
file_already_mined() verifies a matching-mtime group's drawer count
reaches chunk_total before reporting True. Drawers with no chunk_total
(legacy rows, single-shot add_drawer()) are trusted as before.

MemPalace#22 (HIGH, correctness/TOCTOU): source_mtime was captured via a fresh
os.path.getmtime() well after content was read, chunked, and
room-detected. A file appended to in that window got its new drawers
stamped with an mtime that already matched the (now newer) on-disk
state, so the appended tail was silently, permanently skipped on every
future mine. _read_text_no_follow now returns (content, mtime) from
the same fstat() that validates the file; process_file threads that
single value through instead of re-stating.

MemPalace#23 (HIGH, silent-failure): a failed stale-drawer purge was swallowed
to a debug log and mining proceeded anyway, silently producing
duplicate or orphaned drawers. A purge failure now aborts this file's
mine attempt (old drawers' stored mtime is untouched, so the next mine
still sees a mismatch and retries) and prints a visible warning,
matching every other degraded path in this module.

MemPalace#24 (LOW, data-loss): the old-drawer delete ran unconditionally, but
the closet purge+rebuild only ran when drawers_added > 0 -- a file
whose chunks all landed below min_chunk_size after boundary-splitting
lost its drawers but kept stale closets pointing at now-deleted IDs.
purge_file_closets now runs whenever the delete-and-rebuild cycle
does, regardless of the new chunk count; only the rebuild itself stays
conditional.

169 tests pass across test_miner.py/test_convo_miner*.py/test_palace.py/
test_hallways.py/test_format_miner.py/test_miner_fts5_validation.py, no
regressions. Full suite: 1 unrelated pre-existing flake in
test_mcp_server.py (module-global peer-writer-lock state leaking across
test files in full-suite ordering -- passes standalone and as a full
file; the diff here never touches mcp_server.py).

* docs(changelog): tighten 3.7.0 notes to match prior release style

Rewrite the 3.7.0 section as short, scannable bullets like 3.6.0 —
bold lead, one or two sentences per item, thematically grouped fixes —
instead of multi-paragraph issue writeups.

* fix(tests): harden hybrid search against empty Windows Chroma reads

Windows CI intermittently returns zero hybrid hits right after a fast
seed write (same class as "Nothing found on disk" on tiny collections).
Close the palace client after seeding so the next open re-reads flushed
segments, retry search once if empty, and assert non-empty results with
a clear message instead of IndexError.

* fix(ingest): never block on a non-regular file (MemPalace#2221)

`os.walk` and `glob` list a FIFO, a socket and a device node as ordinary
filenames, and MemPalace decides what to read from the suffix. Opening a
FIFO for reading parks in the kernel until a writer appears, so a named
pipe called `notes.md` in a mined directory wedged `mempalace mine`
forever — no output, no error, no progress. `mine --mode convos`,
`sweep`, `init`, `compress` and `split` blocked the same way.

Two shapes are at fault.

Four helpers already refused non-regular files with `fstat` + `S_ISREG`,
but the check sat *after* a blocking `os.open`, so it could never run.
Adding `O_NONBLOCK` to those opens makes the existing type check
reachable: the open returns immediately and the file mode decides, with
no errno guesswork. A FIFO that does have a live writer is refused just
the same. Linux open(2) states the flag has no effect on regular files;
the one exception is a write lease, where a non-blocking open fails
EAGAIN instead of waiting out lease-break-time. Leases are granted on
regular files only, so that branch re-checks the type and then opens
without the flag rather than silently dropping a file that used to be
mined.

The rest guard with `exists()`, which is true for a pipe, and then open
anyway. Those become type checks: a discovery walk drops non-regular
entries before any reader sees them, and a fixed-name read decides with
`is_file()` instead. `scan_project` and `scan_convos` already stat every
candidate for the size limit, so the type check costs no extra syscall.
Where the gate replaced an `open` that sat inside a `try`, it goes in
the same `try`: `is_file()` raises `PermissionError` on a directory
without `x`, which that handler already absorbed.

O_NONBLOCK: miner._read_text_no_follow, convo_miner._is_regular_source_file,
normalize._read_transcript_file, repair._open_regular_file_no_follow.
Type gate: miner.scan_project, miner.load_config, convo_miner.scan_convos,
sweeper.parse_claude_jsonl, sweeper.sweep_directory,
entity_detector.detect_entities, cli._gather_origin_samples,
cli._ensure_mempalace_files_gitignored, cli.cmd_compress, cli.cmd_init,
project_scanner._collect_manifest_names,
project_scanner._parse_gradle_root_project_name,
room_detector_local.detect_rooms_local, llm_refine.collect_corpus_text,
split_mega_files.main, hook_shell.count_human_messages.

* docs(changelog): note the non-regular-file ingest hang (MemPalace#2221)

* fix: 3.7.1 critical patch — re-mine honesty + SIGTERM lock release

Stack the post-3.7.0 hang and silent-skip fixes for a fast patch release:

- Keep MemPalace#2223 (non-regular file hang) and MemPalace#2088 (chunk_total / same-fstat
  mtime / purge abort / closet purge) as the base.
- On multi-batch upsert failure, delete partial drawers and closets for
  that source before re-raising so the next mine retries (MemPalace#2122, MemPalace#2151).
- Install SIGTERM/SIGHUP handlers in mcp_server.main so atexit can release
  the palace writer lease (MemPalace#2205).
- Adapt non-regular-file tests to the (content, mtime) read return type.

* fix(convo): stamp chunk_total and clean partial multi-batch mines (MemPalace#2183)

Port project-miner re-mine honesty to conversation ingest so an interrupted
transcript mine cannot permanently skip missing exchanges:

- stamp chunk_total on every convo drawer in the pass
- delete partial drawers for the source/extract_mode on upsert failure
- teach prefetch_mined_set the same completeness rule as file_already_mined

* test(repair): release SharedSystemClient after seeding for Windows rename

In-place rebuild tests archive the palace directory after _seed_palace.
backend.close() alone left chromadb's path-keyed System holding files open
on Windows (WinError 5), so rebuild_from_sqlite aborted before the mocked
upsert path and test_rebuild_from_sqlite_raises_on_upsert_failure never
raised RebuildPartialError. Clear the shared cache and GC after close.

* chore(release): 3.7.1

Bump package, plugins, lock, OpenClaw skill, and README badge to 3.7.1.
Fold Unreleased integrity notes into the 3.7.1 changelog: FIFO ingest hang,
project and convo re-mine completeness, chromadb System-cache rewind, and
SIGTERM/SIGHUP lease release.

* fix(release): preserve fork behavior after upstream sync

Keep normalized and subject-routed ingestion compatible with the 3.7.1
re-mine safeguards. Distinguish local Chroma writes from peer changes so
cache refreshes release stale clients without invalidating live handles.

* fix(ci): record HTTP status before response delivery

* fix(ci): normalize SDK HTTP status values

* test(ci): make conversation fixtures portable

---------

Co-authored-by: Cristian Deheleanu <160292664+colorpanda82@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: KeilerHirsch <KeilerHirsch@users.noreply.github.com>
Co-authored-by: Igor Lins e Silva <4753812+igorls@users.noreply.github.com>
Co-authored-by: Michael Valentsev <michael@valentsev.ru>
kostadis added a commit to kostadis/mempalace-fork that referenced this pull request Aug 21, 2026
Sub-issue A of MemPalace#23. Pre-flight notes for merging upstream v3.5.0 into
kostadis-dev: per-release commit counts (525 total), changelog delta,
divergence radar over the original 7 sites, and two NEW divergences not
in divergence.md — §8 pluggable-backend collision (hard, decision-required,
lands v3.4.0) and §9 Cursor duplication (semantic). Baseline test state:
2013 passed / 15 skipped / 2 pre-existing failures (backend neutralized).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
kostadis added a commit to kostadis/mempalace-fork that referenced this pull request Aug 21, 2026
Completes `git merge v3.3.6` on merge/v3.5.0-into-kostadis-dev. 18 conflict
files resolved keeping local design at the divergence sites; §2 parallel-mine
mechanics ported into the local pipeline (chunk-cap MemPalace#1455, Tier 6a content-date
MemPalace#1584, config chunking, skip_reason); §embedding unified (provider switch +
within-onnx model select, case-insensitive embedding_model).

Fixed a silent auto-merge defect in backends/chroma.py: the merge dropped the
`_HNSW_MISSING_METADATA_DATA_FLOOR` constant while keeping its usages, breaking
15 backend/HNSW tests with a NameError. Restored per upstream.

Deferred (documented in docs/v3.5.0-merge-notes.md): MemPalace#1383 KG realpath cache
canonicalization (kept local _kg_cache keying per §7); prefetch_mined_set.

Tests: 2538 passed, 33 skipped, 2 pre-existing failures (baseline), no new
failures. ruff-clean on merge-touched files. version.py = 3.3.6.

Refs MemPalace#23.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
kostadis added a commit to kostadis/mempalace-fork that referenced this pull request Aug 21, 2026
…end framework

Completes `git merge v3.4.0`. The §8 reconciliation: adopt upstream's
pluggable-backend framework (pgvector/qdrant/sqlite_exact/embedding_wrapper)
and keep turbovec re-homed as an entry-point adapter on upstream's BaseBackend.
Registry registers the new backends eagerly (lazy client deps) while keeping
chroma best-effort/lazy so turbovec-only deploys never import chromadb.

Kept local design at the divergence sites (§1 two-list search, §2 parallel
mine pipeline, §3 per-palace cache + palace= arg + PalaceNotDeclared); ported
v3.4.0 mechanics (collision-safe drawer IDs, MemPalace#1245 filter-fallback bugfix,
--backend flag, MemPalace#1573 quarantine re-arm). Adopted upstream's refined HNSW
quarantine (subsumes local MemPalace#1532/#0991677 intent).

Fixed silent auto-merge defects in NON-conflict files (same class as B's
chroma.py NameError): mcp_server _collection_error_or_no_palace / tool_reconnect
referencing dropped scalars, searcher.search_within stitched to an undefined
helper, convo_miner dropped hashlib import.

Known limitation (pre-existing HEAD design, not a regression): mcp_server
_get_collection stays chroma-centric; turbovec MCP path unverified end-to-end
(turbovecdb absent). See docs/v3.5.0-merge-notes.md §Sub-issue C.

Tests: 2701 passed, 43 skipped, 2 pre-existing failures (baseline), no new
failures. ruff-clean. version.py = 3.4.0.

Refs MemPalace#23.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
kostadis added a commit to kostadis/mempalace-fork that referenced this pull request Aug 21, 2026
Completes `git merge v3.4.1` (patch release: Cursor plugin/hooks, Antigravity,
embeddinggemma OOM sub-batching, backup retention). Kept local design at the
divergence sites; §9 Cursor duplication coexists (upstream bash hooks/cursor/
+ local Python harness). embeddinggemma OOM fix folded into the kept
EmbeddinggemmaONNX; namespaced onnx cache key kept.

Fixed silent auto-merge defects (same class as B/C): dropped _metadata_cache
scalar leaks in mcp_server rerouted to _invalidate_metadata_cache(); tool_search
first-call missing collection_name; test_cli repair mocks needed
resolved_palace_path; and a test-isolation pollution (local
test_config_palace_migration reload rebinding DEFAULT_PALACE_PATH broke v3.4.1's
test_hallways_palace_scoped) fixed via snapshot/restore.

Flagged for follow-up (deferred, not a regression): local --limit is a pre-slice
vs upstream MemPalace#1535 "stop after N new"; port into the parallel consumer TBD.
See docs/v3.5.0-merge-notes.md §Sub-issue D.

Tests: 3118 passed, 48 skipped, 2 pre-existing failures (baseline). ruff-clean.
version.py = 3.4.1.

Refs MemPalace#23.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
kostadis added a commit to kostadis/mempalace-fork that referenced this pull request Aug 21, 2026
…arget

Completes `git merge v3.5.0`. version.py = 3.5.0 — the migration target.
New v3.5.0 features adopted into the local design: source_file search filter
(threaded through the two-list primary path), mempalace_checkpoint /
mempalace_delete_by_source MCP tools, SQLite status fast-path (gated on the
default palace so cross-palace `palace=` addressing still works), opt-in
daemon + HTTP transport, sqlite_read_uri. Kept local design at all divergence
sites (§1 two-list search, §3 per-palace cache + palace isolation, §5 Python
multi-harness hooks).

Fixed silent auto-merge defects (same class as B/C/D): tool_delete_by_source
writing a nonexistent _metadata_cache global; status fast-path omitting
palace_path; test stubs/mocks lagging the merged EF and palace-resolution
interfaces.

Tests: 3366 passed, 65 skipped, 1 pre-existing failure (test_hook_chat_palace
§5); the former test_sync baseline failure is now cleanly skipped per §3.
ruff-clean.

Refs MemPalace#23.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
kostadis added a commit to kostadis/mempalace-fork that referenced this pull request Aug 21, 2026
Retitle kostadis-dev-vs-v3.3.5 → v3.5.0; context updated to 67 local commits
merged current through v3.5.0 via the five-release chain. The seven original
structural divergences (§1–§7) held unchanged through v3.5.0; added five new
sections from the v3.4.0–v3.5.0 merges: §8 backend layer (converged onto
upstream), §9 Cursor duplication, §10 embedding unification, §11 HNSW
quarantine (converged), §12 --limit semantics (open, deferred). Refreshed the
Test-impact section to the v3.5.0 counts (3366 pass / 65 skip / 1 pre-existing
failure) with per-site skip mapping, and added a Deferred-follow-ups section
(--limit MemPalace#1535, MemPalace#1383 KG canonicalization, is_default status gating, turbovec
MCP end-to-end verification). Added a note on the silent auto-merge defects
that recurred across all five merges.

version.py already reconciled to 3.5.0 in sub-issue E.

Refs MemPalace#23.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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 needs-rebase PR has merge conflicts with develop and needs rebase

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants