Skip to content

Release v3.6.0 — promote develop to main - #2020

Merged
igorls merged 115 commits into
mainfrom
develop
Jul 17, 2026
Merged

Release v3.6.0 — promote develop to main#2020
igorls merged 115 commits into
mainfrom
develop

Conversation

@igorls

@igorls igorls commented Jul 15, 2026

Copy link
Copy Markdown
Member

Promote developmain for the 3.6.0 release

Releases publish only from main (per docs/RELEASING.md). This promotes the 3.6.0 bump merged via #2019 plus the work accumulated on develop since v3.5.0 — 50 PRs.

Version

All release/version surfaces are 3.6.0, the lockfile is regenerated, and the documented/runtime MCP surface is aligned at 36 tools. PR #2019 passed the version guard, lint, package builds, and the full Linux 3.9/3.11/3.13, macOS, and Windows matrix.

Headline changes since v3.5.0

Features

  • Secure-by-default remote/team mempalace serve with bearer auth, TLS, read-only enforcement, Docker Compose, and systemd deployment
  • Opt-in Milvus backend for Milvus Lite, self-hosted Milvus, and Zilliz Cloud
  • Atomic mempalace_kg_supersede fact replacement with half-open temporal boundaries
  • Preserved conversation authored_at, date-window drawer listing, mined-session graph derivation, project exclusions, and LaTeX source coverage

Performance

  • MCP initialization responds without waiting for palace-wide startup preflight
  • Server-side Qdrant metadata facets and lighter pgvector metadata-only reads

Reliability

  • Recovery distinguishes derived-index damage from data loss, uses atomic archive moves, bounds SQLite contention, and requires clean final integrity checks
  • CLI search detects HNSW divergence before opening Chroma and falls back to SQLite BM25
  • Writer leases self-heal; status/read-only classification is complete; mutable transcripts re-mine correctly
  • Explicit --palace graph state stays isolated

Full detail is in CHANGELOG.md under ## [3.6.0].

After merge

Draft a GitHub Release targeting main, tag v3.6.0. Publishing the release triggers publish.yml; approve the gated pypi environment upload.

raman325 and others added 30 commits June 6, 2026 22:55
The openclaw skill was last updated when mempalace exposed 19 MCP
tools. Since then 13 more agent-facing tools have landed; this PR
documents the 8 that openclaw should expose so agents can call them
natively instead of falling back to `npx mcporter call ...`:

Search & Browse:
  - mempalace_list_drawers   (paginated drawer listing)
  - mempalace_get_drawer     (fetch a single drawer by id)

Palace Graph:
  - mempalace_create_tunnel  (explicit cross-wing link)
  - mempalace_list_tunnels   (enumerate explicit tunnels)
  - mempalace_delete_tunnel  (remove an explicit tunnel)
  - mempalace_follow_tunnels (walk explicit tunnels from a room)

Write / Session:
  - mempalace_update_drawer  (mutate content or relocate a drawer)
  - mempalace_memories_filed_away (ack the silent auto-save hook)

The 3 admin-only tools (mempalace_sync, mempalace_hook_settings,
mempalace_reconnect) are intentionally left out — they're host/admin
operations, not agent-facing memory operations. The Hermes
MemoryProvider plugin landing in #1684 makes the
same call.

Version bumped 3.3.0 -> 3.4.0 (additive tool surface, no breaking
changes to existing tool docs).
- Fix mempalace_find_tunnels params: (required) -> optional. The MCP
  handler defaults both wing_a and wing_b to None
  (mempalace/mcp_server.py:1277), so the prior docs were factually
  wrong. Caught by gemini-code-assist on PR #1719.
- Clarify implicit-vs-explicit tunnel distinction with consistent
  casing and a brief in-line definition (implicit = discovered from
  drawer content overlap; explicit = user/agent-declared link).
  Suggested by copilot-pull-request-reviewer.
- Split the mempalace_memories_filed_away one-liner into a short
  description plus 'Returns' and 'When to call' sub-bullets for
  readability. Suggested by copilot-pull-request-reviewer.
- ruff format llm_client.py and miner.py (lint job)
- _copy_file_no_follow: close src fd if the dst open fails (no leak), and
  route the rebuild restore through it so backup + restore share one
  no-follow/regular-file path
- update repair tests to assert the unified hardened copy instead of the
  removed shutil.copy2 calls; backup paths are now timestamped
- update normalize large-file test to stub fstat (size is checked on the
  open fd, not via a pre-open os.path.getsize)
fix: tighten local guards and file handling
…aths

The write-ahead log gained its own module in v3.5.0 but sat at 82% coverage;
the uncovered lines were exactly the failure/guard branches that uphold its
contracts: the cache-hit early return, the restricted-FS chmod/mkdir swallow
paths, and the promise that a WAL write failure is logged and never crashes
the calling tool. Add five tests covering those branches plus the non-string
redaction marker, bringing mempalace/wal.py to 100% and locking the
crash-safety guarantees against regression. Test-only; no production change.
Both miner.py and convo_miner.py silently skip files larger than the
10 MB limit with a bare continue. This is especially painful for
conversation mining where long Claude/ChatGPT exports routinely
exceed 10 MB and vanish with no trace.

Print a SKIP warning per oversized file, matching the existing format
in split_mega_files.py.
The original commit printed SKIP for oversized files to stdout but the
sibling SKIP for symlinks in the same scan_project / scan_convos already
went to stderr. Align the new line with that convention.

Also adds a SKIP-with-error log for the except OSError arm right below the
size check. Files whose stat() raises (permission denied, racing delete,
broken symlink that survived the earlier is_symlink check) were the same
bug class as the silent oversize drop.

Tests switched from captured.out to .err and tightened to the full
template; new test covers the OSError arm via a selective Path.stat
monkeypatch with a follow_symlinks gate for Python 3.10+.
test(wal): cover WAL crash-safety, idempotent setup, and redaction edge paths
…1857)

daemon.py:_detached_kwargs was the last production spawn site still using DETACHED_PROCESS. Swap it to CREATE_NO_WINDOW, matching the hook miner's _detached_popen_kwargs fixed in #1848 — the dedicated follow-up the review bot asked for. `grep -rn DETACHED_PROCESS mempalace/` now returns zero production hits.

Survivability is unchanged: CREATE_BREAKAWAY_FROM_JOB (escapes the parent Job Object's kill-on-close) plus the daemon never being attached to the launching console carry survive-terminal-close; CREATE_NEW_PROCESS_GROUP (also kept) isolates Ctrl-C/Break. CREATE_NO_WINDOW is ignored when OR'd with DETACHED_PROCESS, so this replaces the flag rather than adding it. The daemon already redirects stdout/stderr to daemon.log and reads no stdin, so it needs no console.

Adds the first tests for _detached_kwargs (posix + windows, cross-platform monkeypatch of the Windows-only flag constants, mirroring the #1848 hooks_cli tests).
* fix: sanitize wing slug for project dirs with special characters

Project folders containing characters outside sanitize_name's set
(e.g. a leading '+') leaked into the derived wing name, producing names
like 'wing_+project' that config.sanitize_name rejects, silently
breaking diary auto-save for that project.

Add _safe_wing_slug(): collapse non-word runs to '_', trim, and fall
back to 'sessions' when a name reduces to nothing. Route the three
wing-derivation sites through it.

Tests: unit cases for the helper plus a hypothesis property test
asserting wing_<slug> always passes sanitize_name for any input.

* fix: preserve dots and apostrophes in wing slug for backward compatibility

The first pass collapsed every non-word character (including dot and
apostrophe) to underscore, renaming existing valid wings — e.g. my.app
became wing_my_app — which would orphan diary entries already filed under
the old name. Keep dot and apostrophe (both accepted by sanitize_name),
collapse consecutive dots to avoid the path-traversal rejection, and trim
edge separators.

Add backward-compatibility tests for previously-valid names plus a
double-dot collapse test.

* fix: cap wing slug length to stay within sanitize_name's limit

sanitize_name rejects names over 128 characters, so a very long project
directory name would produce a wing name that fails validation,
re-triggering the silent auto-save break this PR fixes. Truncate the slug
to 120 chars (the wing_ prefix keeps the total under 128).

Widen the hypothesis property test to max_size=300 so it exercises the
length path, and add an explicit truncation test.

Addresses gemini-code-assist review feedback on PR #1852.

---------

Co-authored-by: Ivan Antsimonau <ivan.antsimonau@katim.com>
…ath (#1863)

The non-daemon synchronous mine fallback in _mine_sync() spawned the
mine subprocess without CREATE_NO_WINDOW, flashing a visible console
window on every PreCompact fire on Windows. The async paths
(_spawn_mine, _desktop_toast) already pass it via _detached_popen_kwargs();
this sync path was missed. getattr(..., 0) is a no-op off-Windows.

Fixes #1862

Co-authored-by: David Finkelstein <david@finkelstein.us>
…aceConfig.palace_path correctly called os.path.expanduser() for\nenv-var paths but not for paths read from config.json. If config.json\nstores palace_path as '~/.mempalace/palace' (the default written by\ninit), the tilde was returned unexpanded.\n\nDownstream callers such as cli.py cmd_mine did call expanduser when\n--palace was passed explicitly, but fell through to MempalaceConfig()\nwhen no flag was given, inheriting the unexpanded string. Python's\nos.makedirs and chromadb.PersistentClient treat a leading tilde as a\nliteral directory name rather than the home directory, so the palace\nwas silently written to a CWD-relative path such as\nmy_project/~/.mempalace/palace.\n\nThe fix is a single os.path.expanduser() call on line 343 of\nconfig.py, mirroring the existing env-var branch on line 342. Since\nDEFAULT_PALACE_PATH is already expanded at module load (line 197),\nexpanduser on an absolute path is a no-op, so the default case is\nunaffected.\n\nSymptoms: scattered {project}/~/.mempalace/palace directories, palace\nalways appears empty after mine, search returns Collection does not\nexist, launchd-driven nightly mine writes to a different location than\ninteractive mine.\n\nCo-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>n (#1865)
An empty link_lists.bin is not corruption on its own: hnswlib stores the
layer-0 graph inside data_level0.bin and only writes link_lists.bin for
elements promoted to level > 0. A small/low-fanout index where every
element stays on layer 0 serializes an empty link_lists.bin and loads
fine. Flagging that shape as corrupt produced a self-perpetuating
quarantine loop — repair rebuilt the byte-identical all-layer-0 segment,
the next cold start re-quarantined it, accumulating drift dirs (221 MB in
the reported case) with no ingestion involved.

Use the persist-completion marker as the discriminator instead. ChromaDB
writes index_metadata.pickle last, so an intact pickle envelope proves the
flush finished and the empty link_lists.bin is the legitimate all-layer-0
shape. Only treat an empty link_lists.bin as a partial flush when there is
real payload AND no completion marker (absent or truncated pickle). The
#1457 partial-flush protection (real payload, no/truncated marker) is
preserved; the byte-sniff is factored into _hnsw_metadata_marker_intact
and reused by _segment_appears_healthy.

Also fixes the related single-writer stale-quarantine false positive
(#1564), which shares this all-layer-0 root cause.
fix(chroma): stop quarantining valid all-layer-0 HNSW segments (#1716)
Concurrent killed-mid-write mines can leave embedding_fulltext_search in a
malformed-inverted-index state that fails PRAGMA quick_check while the
underlying rows stay intact (integrity_check ok). The repair preflight then
hard-aborts before reaching the FTS5 rebuild step, so `mempalace repair`
refuses to run and full-text search stays broken — the exact loop #1596
reports. The MineValidationError banner even promises "repair --yes rebuilds
the FTS5 virtual table automatically," which the preflight abort made false.

Add maybe_autoheal_fts5_index(): when every quick_check error is an isolated
"malformed inverted index for FTS5 table" failure, rebuild the index in place
from the intact embedding_fulltext_search_content table
(INSERT ... VALUES('rebuild')) under mine_palace_lock, then re-run quick_check.
The rebuild touches no drawer rows. Wired into both repair preflights
(rebuild_index and cli cmd_repair). Any non-FTS5 error in the set, a lock held
by a live mine, or a rebuild that does not clear quick_check leaves the errors
unchanged so the caller still aborts with the recovery banner — broader
corruption is never silently rebuilt over.
fix(repair): auto-heal isolated FTS5 inverted-index corruption (#1596)
* fix(mcp): stop clobbering host app root logger at import (#1860)

_init_logging() ran at import and called logging.basicConfig(force=True),
resetting the root logger's level, format, and handlers unconditionally. An
app that configured logging before importing mempalace.mcp_server lost its
setup: a host on DEBUG dropped to INFO, custom formatters and handlers were
replaced.

force=True existed (#1495) only to keep MEMPALACE_LOG_FILE working when root
already had handlers. This keeps that contract without the reset: configure
root only when it is unconfigured (standalone); otherwise attach a
mempalace-filtered file handler additively and leave the host's config alone.

Adds _MempalaceLogFilter so the file captures every mempalace logger (the
dotted mempalace.* family plus the flat mempalace_* names) and nothing else.

* fix(mcp): survive importlib.reload and pin file log format (#1860)

Addresses review on #1885.

Restore _logging_configured from globals() so the idempotency guard survives
importlib.reload: a reload re-executes the module body, and a plain reset would
let _init_logging() stack a duplicate file handler on root.

Set an explicit "%(message)s" formatter on the file handler so the embedded
path does not depend on logging's default formatter (which already renders the
same, but is now pinned and identical to the standalone path).

Adds a reload regression test and a format-pin assertion.
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](docker/setup-qemu-action@v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](docker/setup-buildx-action@v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.18 to 0.15.20.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](astral-sh/ruff@0.15.18...0.15.20)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.20
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…detect() (#1893) (#1896)

* fix(chroma): require SQLite magic header for ChromaBackend.detect() (#1893)

Closes #1893.

ChromaBackend.detect() was returning True for a 0-byte chroma.sqlite3 file
because the check was just os.path.isfile(...). On a palace that has any
other backend marker alongside a stale 0-byte chroma.sqlite3,
resolve_backend_name then raises BackendMismatchError and the palace becomes
unopenable until the user manually rm's the empty file.

The 0-byte file appears as a side effect of any sqlite3.connect() on a
missing path — Python creates the file immediately but writes the SQLite
header only on the first statement. So any code path that touches the
chroma.sqlite3 path with bare sqlite3.connect(), including chromadb's own
PersistentClient lazy-init (see the comment at backends/chroma.py:2052),
can leave a 0-byte artifact behind.

Fix: detect() now reads the first 16 bytes and compares to the SQLite
magic prefix b"SQLite format 3\x00" instead of relying on file presence
alone. One extra open() + 16-byte read; detect() isn't a hot path.

Properties:
- Rejects 0-byte files (the symptom #1893 is about).
- Rejects non-SQLite garbage at the canonical path (partial writes, etc.).
- Doesn't false-negative on real chroma palaces: any chroma palace whose
  PersistentClient has done any work has the magic header on disk
  (verified — CREATE TABLE is enough to land the header).
- Doesn't couple detect() to chroma's specific schema; the magic header
  is stable across chromadb releases.

Test sweep: many test files used (chroma.sqlite3).touch() or
.write_bytes(b"") as a "fake palace" shortcut, exploiting the loose
isfile() check (one such site even had the comment "# pass the isfile
guard"). After this change, those stand-ins no longer register as chroma
palaces. Introduced tests/_chroma_palace_helper.py::make_minimal_chroma_sqlite
following the existing _backend_conformance.py precedent, and updated 15
call sites across 8 test files to use it. The existing
test_chroma_detect_matches_palace_with_chroma_sqlite (which encoded the
buggy semantics with write_bytes(b"")) is renamed to
test_chroma_detect_matches_palace_with_sqlite_header and now writes a
real SQLite database via the helper. Added two new tests for the
rejection paths (empty file, non-SQLite garbage).

Full env-cleared suite: 3137 passed, 20 skipped, 0 failed. ruff check
and ruff format --check both clean.

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

* fix(sqlite_exact): require SQLite magic header for SQLiteExactBackend.detect()

Per gemini-code-assist review on #1892 PR #1896: SQLiteExactBackend has the
same os.path.isfile() detection pattern as ChromaBackend did, with the same
0-byte-file vulnerability. Mirrors the chroma fix for repo-wide consistency.

- SQLiteExactBackend.detect() now does the same 16-byte SQLite magic-prefix
  check as ChromaBackend.detect().
- _chroma_palace_helper.py: factored its body into a private
  _write_minimal_sqlite_file() and gained a sibling
  make_minimal_sqlite_exact_sqlite() for the sqlite_exact filename. No churn
  to any existing chroma call sites.
- test_sqlite_exact_backend.py:426 (the one site that wrote b"" for
  sqlite_exact.sqlite3) updated to use the new helper.
- Three new tests in test_sqlite_exact_backend.py mirror the chroma trio:
  matches with valid header, rejects empty file, rejects non-SQLite garbage.

Full env-cleared suite: 3140 passed, 20 skipped, 0 failed.

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…follow-up) (#1892)

* fix(pgvector): skip document column for metadata-only fetches (#1840 follow-up)

Closes the explicit "separate follow-up to keep this low-risk" callout
in PR #1840's description.

For remote pgvector deployments (TLS over WAN), `mempalace_status` and
every other metadata-only consumer was transferring the full `document`
column over the wire even when nothing read it. A single scroll over a
177K-drawer palace on a 175 ms-RTT link moved ~150 MB of document text
plus ~50 MB of metadata; this PR drops that to ~50 MB.

scroll_rows / _scroll gain `with_document: bool = True`. When False,
SELECT projects NULL::text instead of the document column. Positional
_row parser unchanged (record[1] stays the document slot, just receives
NULL). Existing callers default to True and see byte-for-byte identical
behavior.

PgVectorCollection.get_all_metadata override: where=None path goes
single-scroll with with_document=False. Filtered path falls back to base
to keep _matches_where running on array/object metadata values (same
correctness contract as #1840's filtered-path decision).

Tests:
- Update _FakePgVectorClient.scroll_rows to accept with_document; mirror
  the NULL-becomes-empty-string semantics when False
- Update 5 existing scroll_calls assertions to include with_document=True
  (unchanged intent)
- test_pgvector_get_all_metadata_skips_document_column: assert exactly
  one scroll call with with_document=False
- test_pgvector_get_all_metadata_filtered_falls_back_to_base: assert
  filtered path preserves with_document=True

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

* fix(pgvector): extend with_document=False fast path to filtered get_all_metadata

Per gemini-code-assist review feedback on #1892: _matches_where only reads
metadata, so the where=None vs where=set conditional fall-back was unnecessary.
The filtered path can use the same single-scroll with_document=False fast path
and apply the post-filter locally on metadata dicts — extending the wire-byte
win to every get_all_metadata caller, not just unfiltered ones.

Mirrors the pushdown + local _matches_where pattern already used by _rows
in the same file: pushdown when _requires_local_filter is False, post-filter
in Python otherwise. Same correctness contract as #1840's filtered get path.

Renames test_pgvector_get_all_metadata_filtered_falls_back_to_base to
test_pgvector_get_all_metadata_filtered_uses_fast_path and asserts the new
behavior (with_document=False + pushdown forwards the equality filter to SQL).

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* feat(convo): preserve authored timestamp from transcripts

Conversation drawers only carried `filed_at` (ingest time), so a bulk
re-mine collapsed every drawer to a single instant and the chronological
signal was lost — even though each Claude Code / Codex JSONL line already
carries an ISO-8601 `timestamp`. The recency-window fallback and any
date-aware consumer then saw ingest order, not when content was written.

- convo_miner: derive `authored_at` (per-file max line `timestamp`) and
  store it as drawer metadata; falls back to `filed_at` when absent
- searcher: surface `authored_at` in search results, and break exact
  hybrid-score ties toward the more recently authored drawer (ISO strings
  sort chronologically; missing dates sort oldest) — benchmark-neutral as
  it only reorders exact ties
- tests: cover `_extract_authored_at` (latest wins, skips/tolerates lines
  without timestamps, non-jsonl/missing -> None) and the tie-break

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(search): surface authored_at in CLI + backfill for existing data

Completes the authored_at work so the field is visible end-to-end and
existing palaces can adopt it without re-mining.

- layers: CLI `search` output shows an `authored:` date line per result
  (peer of the existing date; markdown drawers fall back to filed_at)
- scripts/backfill_authored_at.py: in-place migration that stamps
  authored_at on convos drawers from their source transcripts — metadata
  only (no re-embedding), idempotent, dry-run by default
- docs/authored-at.md: documents created_at (ingest) vs authored_at
  (written) and both backfill paths (in-place / drop-and-recreate)
- tests: backfill integration tests over an ephemeral ChromaDB collection

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(search): address review — non-string timestamp guard + top-level authored_at tiebreak

Two correctness fixes from the PR review:

- _extract_authored_at: only compare when the parsed `timestamp` is a str.
  A non-string timestamp on a malformed/foreign JSONL line previously raised
  TypeError outside the try and could crash the mine.
- _hybrid_rank: the tie-break read `authored_at` only from nested `metadata`,
  but the search_memories path (MCP / Claude Code) carries it at the top level
  of each hit — so the tie-break silently no-op'd there. Read both shapes.
- tests: non-string timestamp cases, and a top-level-shape tie-break test
  (which fails before this fix).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* style: apply ruff format to authored_at changes

CI ruff format --check flagged 4 files; ruff check already passed.
Formatting only — no behavior change.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Igor Lins e Silva <4753812+igorls@users.noreply.github.com>
@igorls
igorls requested a review from milla-jovovich as a code owner July 15, 2026 00:30
Copilot AI review requested due to automatic review settings July 15, 2026 00:30

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.

Pull request overview

Promotes develop to main for the v3.6.0 release, bringing the codebase, tests, docs, and plugin/website surfaces into alignment with the new release features (remote/team server, new backends, KG supersession, authored timestamps, etc.).

Changes:

  • Bump release/version surfaces to 3.6.0 and refresh changelog + docs/tooling counts (36 MCP tools).
  • Add/expand backend + server functionality (Milvus backend registration/config/docs; remote/team server docs + deploy templates; facets + metadata-only paths).
  • Strengthen reliability and correctness via new behaviors + substantial new test coverage (WAL hardening, authored_at handling/backfill, HNSW divergence guard, lock re-entrancy, etc.).

Reviewed changes

Copilot reviewed 104 out of 105 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
website/reference/modules.md Update tool-count docs
website/reference/mcp-tools.md Document 36 tools + kg_supersede
website/reference/cli.md Document --backend flag
website/guide/remote-server.md New remote/team server guide
website/guide/openclaw.md Update tool-count docs
website/guide/mcp-integration.md Update tool-count docs
website/guide/configuration.md Backend configuration reference expansion
website/guide/claude-code.md Update tool-count docs
website/.vitepress/theme/style.css Table layout/overflow improvements
website/.vitepress/config.mts Add remote-server nav item
tests/test_wal.py WAL behavior tests
tests/test_sync.py Use minimal chroma sqlite helper
tests/test_sqlite_exact_backend.py sqlite_exact detection + helpers
tests/test_serve.py New mempalace serve tests
tests/test_searcher.py CLI HNSW divergence routing tests
tests/test_qdrant_backend.py Qdrant facet_counts tests
tests/test_pgvector_backend.py pgvector metadata-only fast path tests
tests/test_palace.py Minimal chroma sqlite helper usage
tests/test_palace_locks.py Cross-thread re-entrant lock tests
tests/test_palace_graph_tunnels.py Config-scoped tunnel persistence tests
tests/test_normalize.py TOCTOU-safe normalize size test update
tests/test_miner.py exclude_patterns + tunnel config threading tests
tests/test_mcp_http_transport.py Read-only + TLS + disconnect handling tests
tests/test_knowledge_graph.py Supersession boundary regression tests
tests/test_hybrid_search.py Tie-break ordering tests
tests/test_hooks_cli.py _safe_wing_slug + property-based tests
tests/test_hnsw_payload_health.py All-layer-0 HNSW validity tests
tests/test_hallways.py Config-scoped hallway persistence tests
tests/test_format_miner.py Config threading in format miner tests
tests/test_entity_detector.py LaTeX prose detection tests
tests/test_entities.py New structural-entity extractor tests
tests/test_embedding_wrapper.py Wrapper forwarding regression tests
tests/test_daemon.py Detached spawn kwargs tests
tests/test_convo_miner.py mtime-aware convo re-mine tests
tests/test_convo_miner_unit.py Scan filters + authored_at extraction tests
tests/test_config.py Milvus config + palace_path override tests
tests/test_config_palace_path.py ~ expansion tests
tests/test_cli.py Repair rebuild-index dispatch/exit tests
tests/test_cli_hallways.py New hallways CLI tests
tests/test_clean_nul_bytes.py NUL-byte sanitization tests
tests/test_backfill_authored_at.py Backfill script integration tests
tests/test_backends.py Robust chroma detect tests
tests/conftest.py Ensure chromadb clients are closed in tests
tests/_chroma_palace_helper.py New minimal sqlite marker helper
skills/mempalace/SKILL.md Update tool-count docs
scripts/backfill_authored_at.py New authored_at backfill script
README.md Backend table + version/tool-count updates
pyproject.toml Version bump + Milvus entry point/extra + Ruff pin
mempalace/version.py Version bump
mempalace/service.py Tool classification update
mempalace/searcher.py Tie-break + HNSW divergence preflight + authored_at surfacing
mempalace/palace_graph.py Config threading for tunnel persistence
mempalace/normalize.py TOCTOU-safe open + size/type guards
mempalace/migrate.py Preserve symlinks in backups
mempalace/llm_client.py Avoid env-key use on external probes
mempalace/layers.py Surface authored date in output
mempalace/knowledge_graph.py Half-open temporal filter + supersede primitive
mempalace/hooks_cli.py Windows console suppression + safe wing slugging
mempalace/hallways.py Config-scoped persistence + CLI plumbing
mempalace/format_miner.py Use selected palace config for tunnels
mempalace/entity_detector.py Treat LaTeX as prose
mempalace/entities.py New structural entity extraction
mempalace/daemon.py Windows detached flags revision + docs
mempalace/config.py NUL stripping + Milvus config + palace_path override
mempalace/backends/sqlite_exact.py SQLite magic-header detect
mempalace/backends/registry.py Register Milvus backend
mempalace/backends/qdrant.py Add facet_counts + capability
mempalace/backends/pgvector.py Skip document column for metadata-only reads
mempalace/backends/embedding_wrapper.py Forward concrete BaseCollection methods
mempalace/backends/chroma.py HNSW completion marker + NUL stripping + detect hardening
mempalace/backends/base.py Add facet_counts API
mempalace/backends/init.py Export Milvus symbols
integrations/openclaw/SKILL.md Update version + tool surface docs
hooks/mempal_save_hook.sh Use python -m mempalace invocation
hooks/mempal_precompact_hook.sh Use python -m mempalace invocation
docs/authored-at.md New authored_at documentation
deploy/server.env.example New remote server env template
deploy/mempalace-server.service New systemd unit template
deploy/docker-compose.server.yml New compose deployment template
CHANGELOG.md Add 3.6.0 release notes
.github/workflows/version-guard.yml Update checkout action version
.github/workflows/publish.yml Update checkout action version
.github/workflows/docker-publish.yml Update checkout + docker action versions
.github/workflows/deploy-docs.yml Update checkout action version
.github/workflows/ci.yml Update checkout action version
.cursor-plugin/README.md Update tool-count docs
.cursor-plugin/plugin.json Update tool-count docs
.cursor-plugin/marketplace.json Update tool-count docs
.codex-plugin/README.md Update tool-count docs
.codex-plugin/plugin.json Version + tool-count updates
.claude-plugin/README.md Update tool-count docs
.claude-plugin/plugin.json Version + tool-count updates
.claude-plugin/marketplace.json Version + tool-count updates

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mempalace/normalize.py
Comment on lines +127 to +131
fd = os.open(filepath, flags)
file_stat = os.fstat(fd)
if not stat.S_ISREG(file_stat.st_mode):
raise IOError(f"Could not read {filepath}: not a regular file")
if file_stat.st_size > 500 * 1024 * 1024: # 500 MB safety limit
igorls and others added 5 commits July 14, 2026 21:52
chore: sync main hotfixes into develop before 3.6.0
mempalace_checkpoint hard-coded added_by="checkpoint" for every drawer,
dropping the filing agent's identity even though it arrives in the same
call via diary.agent_name. Add an optional top-level added_by parameter
and resolve attribution as explicit > diary agent_name > "checkpoint";
blank/whitespace/non-string values defer to the next source. The value
is declared in the tool schema so tools/call admits it on both stdio and
HTTP transports.

Fixes #2023

Co-Authored-By: epinethrone <172391900+epinethrone@users.noreply.github.com>
The new real-Chroma checkpoint test dropped its create-time client with a
bare del, leaving the per-path SharedSystemClient's SQLite/HNSW handles
open on Windows (#1128). Close both clients so the temp palace is released,
matching the conftest fixture's close-not-del pattern.
igorls added 2 commits July 16, 2026 19:07
fix(mcp): preserve agent attribution in mempalace_checkpoint (#2023)
feat(routing): add shared daemon write-routing policy

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

SUCH GREAT WORK IGOR! I've been thinking about some of the improvements and though you always do such meticulous and clean work these new additions are really important for the MemPalace philosophy and creating a richer, safer and more nuanced experience for people!🥳 -m

@igorls
igorls merged commit 8ab251c into main Jul 17, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.