Commit Graph

277 Commits

Author SHA1 Message Date
Igor Lins e Silva c9fbf7c50a Merge pull request #786 from MemPalace/pr/hooks-dont-write-in-chat
fix: stop hooks from making agents write in chat — save tokens
2026-04-13 16:43:37 -03:00
MSL a3b7988d87 fix: stop hooks from making agents write in chat — save tokens
The save hook and precompact hook were telling the agent to write
diary entries, add drawers, and add KG triples IN THE CHAT WINDOW.
Every line written stays in conversation history and retransmits on
every subsequent turn — ~$1/session in wasted tokens.

Fix: hooks now say "saved in background, no action needed" and use
decision: allow instead of block. The agent continues working without
interruption. All filing happens via the background pipeline.

Also updated hooks README with:
- Known limitation: hooks require session restart after install
- Updated cost section: zero tokens, background-only

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:41:59 -03:00
Igor Lins e Silva 744ede76c0 Merge pull request #785 from MemPalace/pr/strip-noise-from-transcripts
fix: strip system tags, hook output, and Claude UI chrome from drawers
2026-04-13 16:33:27 -03:00
Igor Lins e Silva 28e263748b merge: develop (#784 file-locking, #820 version sync)
Non-trivial merge in convo_miner.py: this branch's _file_convo_chunks
(purge stale + upsert with normalize_version) and develop's
_file_chunks_locked (mine_lock + double-checked file_already_mined)
both touched the same critical section. Combined into a single
_file_chunks_locked helper that does lock → double-check → purge →
upsert, preserving both the multi-agent safety guarantee from #784
and the schema-rebuild contract from this PR.

Also folds develop's mine_lock import into both miner.py and
convo_miner.py alongside NORMALIZE_VERSION.

707/707 tests pass, ruff + format clean under CI-pinned 0.4.x.
2026-04-13 16:29:50 -03:00
Igor Lins e Silva 7e5eeda9a5 feat(normalize): auto-rebuild stale drawers via NORMALIZE_VERSION schema gate
Without this, the strip_noise improvement only helps new mines. Every
user who had already mined Claude Code JSONL sessions would keep their
noise-polluted drawers forever, because convo_miner's file_already_mined
skip short-circuits before re-processing.

Adds a versioned schema gate so upgrades propagate silently:

- palace.NORMALIZE_VERSION=2 — bumped when the normalization pipeline
  changes shape (this PR's strip_noise is the v1→v2 bump).
- file_already_mined now returns False if the stored normalize_version
  is missing or less than current, triggering a rebuild on next mine.
- Both miners stamp drawers with the current normalize_version.
- convo_miner now purges stale drawers before inserting fresh chunks
  (mirrors miner.py's existing delete+insert), extracted into
  _file_convo_chunks helper to keep mine_convos under ruff's C901 limit.

User experience: upgrade mempalace, run `mempalace mine` as usual, old
noisy drawers get silently replaced with clean ones. No erase needed,
no "you need to rebuild" changelog footgun.

Tests:
- test_file_already_mined_returns_false_for_stale_normalize_version —
  pins the version gate contract for missing/v1/current.
- test_add_drawer_stamps_normalize_version — fresh project-miner drawers
  carry the field.
- test_mine_convos_rebuilds_stale_drawers_after_schema_bump — end-to-end
  proof that a pre-v2 palace gets silently cleaned on next mine, with
  orphan drawers purged and NOT skipped.

Existing test_file_already_mined_check_mtime updated to include the
new field; all other tests unaffected.
2026-04-13 16:20:55 -03:00
Igor Lins e Silva ca2598a9f6 fix(normalize): make strip_noise verbatim-safe and scope it to Claude Code JSONL
The initial strip_noise() regressed on three fronts when audited against
adversarial user content — each verified with executable repros against
the cherry-picked code:

  1. `<tag>.*?</tag>` with re.DOTALL span-ate across messages: one
     stray unclosed <system-reminder> anywhere in a session merged with
     the next closing tag, silently deleting everything between them
     (including full assistant replies).
  2. `.*\(ctrl\+o to expand\).*\n?` nuked entire lines of user prose
     whenever a user happened to document the TUI shortcut.
  3. `Ran \d+ (?:stop|pre|post)\s*hook.*` with IGNORECASE ate the
     second sentence from "our CI has a stop hook ... Ran 2 stop hooks
     last week" — legitimate user commentary.

These are unambiguous violations of the project's "Verbatim always"
design principle.

Fixes:

- All tag patterns are now line-anchored (`(?m)^(?:> )?<tag>`) and their
  body forbids crossing a blank line (`(?:(?!\n\s*\n)[\s\S])*?`), so a
  dangling open tag cannot eat neighboring messages.
- `_NOISE_LINE_PREFIXES` are line-anchored and case-sensitive — user
  prose mentioning "CURRENT TIME:" mid-sentence is preserved.
- Hook-run chrome requires `(?m)^`, explicit hook names (Stop,
  PreCompact, PreToolUse, etc.), and no IGNORECASE.
- "… +N lines" is line-anchored.
- "(ctrl+o to expand)" only matches Claude Code's actual collapsed-
  output chrome shape `[N tokens] (ctrl+o to expand)`; a bare
  parenthetical in user prose stays intact.

Scope:

- `strip_noise()` is no longer called on every normalization path.
  Only `_try_claude_code_jsonl` invokes it, per-extracted-message — so
  Claude.ai exports, ChatGPT exports, Slack JSON, Codex JSONL, and
  plain text with `>` markers pass through fully verbatim. Per-message
  application also makes span-eating structurally impossible.

Tests:

- 15 new tests in test_normalize.py pin the boundary: 6 guard user
  content that must survive (each of the adversarial repros), 9 assert
  real system chrome is still stripped. All pass; full suite 702 pass
  (2 failures are the unrelated pre-existing version.py bug, cleared
  by #820).

Known limitation (not fixed here): convo_miner.py does not delete
drawers on re-mine, so transcripts mined before this PR keep noise-
filled drawers until the user manually erases + re-mines. Proper fix
needs a schema-version field on drawer metadata + re-mine trigger —
out of scope for this PR.
2026-04-13 16:11:03 -03:00
Igor Lins e Silva ab46c8ebf1 Merge pull request #784 from MemPalace/pr/multi-agent-lock
fix: file-level locking to prevent multi-agent duplicate drawers
2026-04-13 15:58:35 -03:00
Igor Lins e Silva 386da51ae5 style: ruff format mempalace/palace.py
Add blank lines after inline imports in mine_lock. Pure formatting.
2026-04-13 15:54:52 -03:00
Igor Lins e Silva 09f218cbb2 refactor: extract locked filing block to keep mine_convos under C901
Adding the per-file lock + double-checked file_already_mined() in the
previous commit pushed mine_convos cyclomatic complexity from 25 to 26,
just over ruff's max-complexity threshold. Hoist the locked critical
section into _file_chunks_locked() so the outer loop stays within
budget. No behavior change.
2026-04-13 15:48:54 -03:00
Igor Lins e Silva 297517db0e Merge pull request #820 from MemPalace/fix/version-sync-3.2.0
fix: sync version.py to 3.2.0
2026-04-13 15:47:18 -03:00
Igor Lins e Silva 69d6e2f7f3 fix: sync version.py to 3.2.0
Commit 6614b9b bumped pyproject.toml to 3.2.0 but missed
mempalace/version.py, breaking test_version_consistency on
every PR's CI. This syncs them.
2026-04-13 15:46:27 -03:00
MSL 9b99c136ee fix: strip system tags, hook output, and Claude UI chrome from drawers
normalize.py now strips before filing:
- <system-reminder>, <command-message>, <command-name> tags
- <task-notification>, <user-prompt-submit-hook>, <hook_output> tags
- Hook status messages (CURRENT TIME, Checking verified facts, etc.)
- Claude Code UI chrome (ctrl+o to expand, progress bars, etc.)
- Collapsed runs of blank lines

This noise was going straight into drawers, wasting storage space
and polluting search results. strip_noise() runs on all normalized
output regardless of input format (JSONL, JSON, plain text).

689/689 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 07:30:12 -03:00
MSL 30a431924b fix: add file-level locking to prevent multi-agent duplicate drawers
Root cause: when multiple agents mine simultaneously, both pass
file_already_mined() check, both delete+insert the same file's
drawers, creating duplicates or losing data.

Fix: mine_lock() in palace.py — cross-platform file lock (fcntl on
Unix, msvcrt on Windows). Both miner.py and convo_miner.py now lock
per-file during the delete+insert cycle and re-check after acquiring
the lock.

Tested:
- Lock acquires and releases correctly
- Second agent blocks until first releases (0.25s wait)
- 33/33 existing tests pass
- Cross-platform: fcntl (macOS/Linux), msvcrt (Windows)

Based on v3.2.0 tag.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 07:20:58 -03:00
Igor Lins e Silva 6614b9b4e7 bump version v3.2.0 (#761) 2026-04-13 03:21:50 -03:00
Mikhail Valentsev a2432a3245 fix: parse Claude.ai privacy export with messages key and sender field (#677) (#685)
* fix: parse Claude.ai privacy export with messages key and sender field (#677)

The privacy-export branch in _try_claude_ai_json only checked for the
"chat_messages" key, missing exports that use "messages" instead.  It
also only read the "role" field while real privacy exports use "sender".
Both gaps caused the file to fall through to plain-text, producing a
single giant drawer.

Changes:
- Accept "messages" alongside "chat_messages" in the conversation-object
  guard and inner extraction.
- Accept "sender" alongside "role" as the author field.
- Fall back to a top-level "text" key when content blocks are empty.
- Produce one transcript per conversation instead of concatenating all
  conversations into a single blob.
- Extract shared logic into _collect_claude_messages helper.
- Add 6 regression tests covering each variant.

* style: apply ruff format to normalize.py

* fix: guard against null text field in Claude.ai export parsing

item.get("text", "").strip() crashes when "text" is explicitly null
in the JSON (legal and observed in some exports). Use
(item.get("text") or "").strip() and add a regression test.

---------

Co-authored-by: Igor Lins e Silva <4753812+igorls@users.noreply.github.com>
2026-04-13 02:11:03 -03:00
Igor Lins e Silva e200ce2c8a fix: detect mtime changes in _get_client to prevent stale HNSW index (#757)
When external tools write to the palace database (CLI mining, scripts), the MCP server's cached ChromaDB collection becomes stale — its HNSW index doesn't know about new vectors. Develop already invalidates on inode changes (catches rebuilds) but not on mtime changes (misses in-place writes).

This PR:
- Adds st_mtime tracking alongside st_ino in _get_client; invalidates the cached client on either change.
- Adds the mempalace_reconnect MCP tool for explicit cache flush.

Original author: @jphein (#663). Original approval: @Ari4ka.
Skips test_missing_db_invalidates_cache on Windows (ChromaDB holds chroma.sqlite3 open).
2026-04-13 01:53:13 -03:00
Igor Lins e Silva 39e1651af3 fix: correct typo in entity_detector interactive classification prompt (#755)
'(r)roject' had a duplicate 'r', making it read as '(r)roject'
instead of the intended '(r)project'.

Small UX fix — no behavior change.

Co-authored-by: Arnold Wender <arnold.wender@gmail.com>
2026-04-13 01:43:57 -03:00
clach04 c17cf079ad fix #733: diagram misaligned (#734)
Converted to 7-bit clean us-ascii, which also aligns diagram.
Moved "hall" connection to be inline with Rooms in diagram.
2026-04-13 01:43:01 -03:00
shafdev f4226047cb fix: hash full content in tool_add_drawer drawer ID (#716)
* fix: hash full content in tool_add_drawer drawer ID

* style: apply ruff format

* style: fix ruff format for CI ruff 0.4.x
2026-04-13 01:40:46 -03:00
Sanjay Ramadugu 3a50966766 fix: remove 10k drawer cap from status display (#603) (#707) 2026-04-12 21:22:39 -07:00
Milla J e6d232f538 docs: add CHANGELOG.md covering v3.0.0 through v3.2.0-dev (#752)
Full changelog from git history and merged PRs:
- v3.0.0 (2026-04-06): initial public release
- v3.1.0 (2026-04-09): 80+ commits, security hardening, Windows compat, tests 20→92
- Unreleased/v3.2.0: 50+ commits, i18n, backend seam, migrate command, more security

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 21:04:31 -07:00
Igor Lins e Silva 41b8601d86 Merge pull request #739 from MemPalace/copilot/fix-unauthorized-data-deletion
Harden palace deletion, WAL redaction, and MCP search input handling
2026-04-12 23:13:56 -03:00
Igor Lins e Silva c68370609d fix: address Copilot review comments on PR #739
- query_sanitizer: require matching quote pair in _strip_wrapping_quotes
- query_sanitizer: re-check MIN_QUERY_LENGTH after trim in tail_sentence path
- migrate: neutral confirmation message accurate for both migrate and repair
- cli: os.path.normpath instead of rstrip to handle '/' root edge case
2026-04-12 23:07:46 -03:00
Igor Lins e Silva 22328540e1 style: ruff format 2026-04-12 22:20:27 -03:00
copilot-swe-agent[bot] c383523768 chore: clarify security guardrails
Agent-Logs-Url: https://github.com/MemPalace/mempalace/sessions/775f2fc4-3051-462e-8586-6d694b55da0d

Co-authored-by: igorls <4753812+igorls@users.noreply.github.com>
2026-04-12 22:19:58 -03:00
copilot-swe-agent[bot] b1a676fa24 fix: make quote trimming explicit
Agent-Logs-Url: https://github.com/MemPalace/mempalace/sessions/775f2fc4-3051-462e-8586-6d694b55da0d

Co-authored-by: igorls <4753812+igorls@users.noreply.github.com>
2026-04-12 22:19:58 -03:00
copilot-swe-agent[bot] 976289aa5c fix: refine security validation messaging
Agent-Logs-Url: https://github.com/MemPalace/mempalace/sessions/775f2fc4-3051-462e-8586-6d694b55da0d

Co-authored-by: igorls <4753812+igorls@users.noreply.github.com>
2026-04-12 22:19:58 -03:00
copilot-swe-agent[bot] 248ecd98f1 fix: polish sanitizer and repair messaging
Agent-Logs-Url: https://github.com/MemPalace/mempalace/sessions/775f2fc4-3051-462e-8586-6d694b55da0d

Co-authored-by: igorls <4753812+igorls@users.noreply.github.com>
2026-04-12 22:19:58 -03:00
copilot-swe-agent[bot] d2d4e62543 test: expand security regression coverage
Agent-Logs-Url: https://github.com/MemPalace/mempalace/sessions/775f2fc4-3051-462e-8586-6d694b55da0d

Co-authored-by: igorls <4753812+igorls@users.noreply.github.com>
2026-04-12 22:19:58 -03:00
copilot-swe-agent[bot] c478dfa173 fix: harden palace security checks
Agent-Logs-Url: https://github.com/MemPalace/mempalace/sessions/775f2fc4-3051-462e-8586-6d694b55da0d

Co-authored-by: igorls <4753812+igorls@users.noreply.github.com>
2026-04-12 22:19:58 -03:00
copilot-swe-agent[bot] bb577bb41f Initial plan 2026-04-12 22:19:58 -03:00
Ben Sigman 5ac600b3bb style: ruff format convo_miner.py (#741) 2026-04-12 16:34:16 -07:00
Igor Lins e Silva 3b77624f8a Merge pull request #718 from MemPalace/feature/i18n
feat: i18n support — 8 languages for MemPalace
2026-04-12 20:08:59 -03:00
Ben Sigman 8efd4e53b5 docs: add CLAUDE.md + mission/principles to AGENTS.md (#720)
* docs: add CLAUDE.md + mission/principles to AGENTS.md

Add project mission, design principles, and contribution guidelines
to CLAUDE.md (new file) and prepend them to AGENTS.md.

Six non-negotiable principles: verbatim always, incremental only,
entity-first, local-first zero API, performance budgets, privacy
by architecture.

* docs: update CLAUDE.md with Milla's edits, add MISSION.md, symlink AGENTS.md

CLAUDE.md:
- Add Zettelkasten + AAAK explanation to mission (Milla's edit)
- Add 7th principle: background everything
- Update project structure to match current develop (all modules)
- Fix hooks listing to match actual public repo
- Add backends/ to structure and key files

MISSION.md:
- Milla's personal narrative on why MemPalace exists
- Origin story, AAAK inside joke, v4 goals

AGENTS.md:
- Now symlinks to CLAUDE.md (single source of truth)

* docs: trim MISSION.md — remove v4 notes and workflow (moved to private)
2026-04-12 15:28:01 -07:00
Mikhail Valentsev 87e8bafad8 fix: prevent convo_miner from re-processing 0-chunk files on every run (#654) (#732)
* fix: register 0-chunk files to prevent re-processing on every mine (#654)

mine_convos() has three early-exit paths (OSError, content too short,
zero chunks) that skip writing anything to ChromaDB. Since
file_already_mined() checks for the presence of a document with a
matching source_file, these files are re-read and re-processed on
every subsequent run.

Add _register_file() that upserts a lightweight sentinel document
(room="_registry", ingest_mode="registry") so file_already_mined()
returns True on future runs.

Note: Bug 2 from the issue (drawers_added counter always 0) was
already resolved upstream via the switch from collection.add() to
collection.upsert().

* fix: resolve macOS path symlink in test + remove unused variable
2026-04-12 14:25:34 -07:00
Sanjay Ramadugu 9b60c6edd7 fix: remove 8-line AI response truncation in convo_miner (#692) (#708)
The _chunk_by_exchange() function was silently truncating AI responses
to 8 lines via ai_lines[:8]. Any content beyond line 8 was discarded,
violating the project's verbatim storage principle.

Now the full AI response is preserved. When a combined exchange exceeds
CHUNK_SIZE (800 chars, aligned with miner.py), it is split across
consecutive drawers instead of being truncated.
2026-04-12 14:23:57 -07:00
shafdev d52d6c9622 fix: store full AI response in convo_miner exchange chunking (#695) 2026-04-12 14:23:52 -07:00
Mikhail Valentsev 091c2fe1c6 fix: mine --dry-run TypeError on files with room=None (#586) (#687)
* fix: return "general" room from process_file error paths (#586)

process_file() returned (0, None) for already-mined, unreadable, and
too-short files.  In --dry-run mode the caller always enters the
room_counts branch, so None ended up as a dict key and crashed the
summary printer with "unsupported format string passed to
NoneType.__format__".

Returning "general" instead of None makes the function contract
explicit: it always yields (int, str).  This matches the consensus
fix discussed in the issue thread.

* style: apply ruff format to test_miner.py
2026-04-12 14:23:44 -07:00
Jeffrey Hein 862a07b198 fix: skip arg whitelist for handlers accepting **kwargs (#572) (#684)
* fix: skip arg whitelist for handlers accepting **kwargs (#572)

The schema-based argument filter (from #647) strips all kwargs not
declared in input_schema. This breaks handlers that accept **kwargs
for pass-through to ChromaDB or other backends.

Add inspect.Parameter.VAR_KEYWORD check before filtering — handlers
with **kwargs receive all arguments unfiltered.

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

* fix: guard inspect.signature failure, default to filtering

Wrap inspect.signature() in try/except — on failure, default to
filtering (safe fallback). Addresses Copilot feedback on fragility.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 14:23:39 -07:00
Jeffrey Hein 6e2ced3287 fix: allow Unicode in sanitize_name() — Latvian, CJK, Cyrillic (#637) (#683)
* fix: allow Unicode in sanitize_name() — Latvian, CJK, Cyrillic names (#637)

_SAFE_NAME_RE was ASCII-only ([a-zA-Z0-9]), rejecting valid Unicode
names like "Jānis" or "太郎". Changed to \w which matches Unicode
word characters (letters, digits, underscore) in Python 3.

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

* fix: tighten Unicode regex, add sanitize_name tests

Use [^\W_] for first/last char to allow Unicode letters/digits but
reject leading/trailing underscores (Copilot feedback). Add 7 tests
covering Latvian, CJK, Cyrillic, path traversal, and edge cases.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 14:23:34 -07:00
Jeffrey Hein 915b8b2c75 fix: add --yes flag to init instructions for non-interactive use (#534) (#682)
AI agents calling `mempalace init` hang on the interactive confirmation
prompt. The --yes flag skips it, enabling automated setup.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 14:23:29 -07:00
Igor Lins e Silva c3f9b76d9a fix(ci): resolve ruff lint + format failures
- Remove unused `json` and `current_lang` imports from
  mempalace/i18n/test_i18n.py (F401)
- Reformat Dialect.__init__ signature in mempalace/dialect.py
  (ruff format collapses multi-line signature, adds blank line
  after lazy import)

Both auto-fixes from `ruff check --fix` / `ruff format`. No behavioral
changes.
2026-04-12 17:14:06 -03:00
MSL baf3c0ab64 feat: i18n support — 8 languages for MemPalace
Add language dictionaries: English, French, Korean, Japanese, Spanish,
German, Simplified Chinese, Traditional Chinese.

Each language is a single JSON file with:
- Localized terms (palace, wing, closet, drawer, etc.)
- CLI output strings with {var} interpolation
- AAAK compression instructions in that language
- Regex patterns for offline topic/quote/action extraction

Usage: Dialect(lang="ko") or set "language": "ko" in config.
Contributors can add new languages by copying en.json and translating.

Dialect class now accepts lang param and loads AAAK instruction +
regex patterns from the i18n dictionary automatically.

Tests: mempalace/i18n/test_i18n.py — all 8 languages pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 10:09:47 -07:00
Igor Lins e Silva 74e5bf6090 Merge pull request #691 from MemPalace/codex-github-pages-publishing
ci: fix github pages publishing
2026-04-12 05:57:57 -03:00
Igor Lins e Silva a487d059ba ci: fix github pages publishing 2026-04-12 05:19:30 -03:00
Igor Lins e Silva f224c48e09 Merge pull request #439 from igorls/docs/vitepress-site
docs: add VitePress documentation site
2026-04-12 04:55:58 -03:00
Mikhail Valentsev f56e67b516 docs: fix stale milla-jovovich org refs and branch target in contributor docs (#679)
The repo moved to the MemPalace org but several docs still point at the
old milla-jovovich URLs.  Also, CONTRIBUTING.md tells people to PR
against main while the actual workflow (per ROADMAP.md) targets develop.

Files touched:
- CONTRIBUTING.md: clone URL, issues URL, PR target branch
- examples/gemini_cli_setup.md: clone URL
- integrations/openclaw/SKILL.md: homepage and license URLs
2026-04-12 00:00:21 -07:00
travisBREAKS 89206107fa fix(bench): remove hardcoded credential paths from benchmark runners (#177)
The `_load_api_key()` function in longmemeval_bench.py and locomo_bench.py
searched for API keys in a fixed path (`~/.config/lu/keys.json`) using
personal key names (`anthropic_milla`, `anthropic_claude_code_main`).

This leaks internal infrastructure details into the public codebase and
trains contributors to store credentials in a non-standard location
rather than using the standard ANTHROPIC_API_KEY env var.

Simplified to: CLI flag > env var > empty string. Updated help text
and HYBRID_MODE.md docs to match.

Co-authored-by: Tadao <tadao@travisfixes.com>
2026-04-11 23:14:36 -07:00
Brooke Whatnall dc143471bc fix: expand ~ in split command directory argument (#361)
Path("~/foo") does not expand tilde on its own, causing
`mempalace split ~/some/dir` to silently find no files.

Fix by calling .expanduser().resolve() in both places the
path is constructed: cmd_split in cli.py (defensive, at the
CLI boundary) and main() in split_mega_files.py (the root cause).

Co-authored-by: Brooke Whatnall <brookewhatnall@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 23:14:28 -07:00
7. Sun 15d9ee1b51 fix: close KnowledgeGraph SQLite connections in test fixtures (#450)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 23:14:23 -07:00