sync: upstream/develop through v3.8.0 — 166 commits (+ ancestry repair) - #401
Merged
Conversation
…edup-convo-miner-drawers fix: prevent duplicate drawers when re-mining LLM conversations
…ble-drawer-id fix(search): return round-trippable drawer IDs
fix(convo_miner): stop sweeper drawers being purged (MemPalace#2089)
…exit fix(mcp): self-exit on stdin EOF/broken pipe so orphaned stdio sessions release locks
…k-contention-retry fix(daemon): defer jobs refused the palace lock, don't fail them (MemPalace#2014)
fix(mcp): refuse config and ack writes in read-only mode (MemPalace#2103)
fix(entity): defuse entity-candidate ReDoS on long ASCII runs (MemPalace#2065)
Bump package, plugins, lock, and README badge to 3.7.0. Promote the post-3.6.0 integrity spine into CHANGELOG: single-writer ownership, HNSW write defaults and preflight, safer repair/re-mine, MCP/daemon lifecycle hardening, entity ReDoS guard, and hook write-routing. Also ruff-format two test files that drifted during conflict merges. Local validation: ruff check/format clean; 3497 passed, 31 skipped.
chore(release): 3.7.0
Address actionable MemPalace#2129 bot feedback and the Windows closet KeyError: - Reopen immutable sqlite_exact readers only when both WAL sidecars exist (partial pair keeps the clean snapshot instead of failing the reconnect). - Retry get_collection without options when plugin backends reject the kwarg. - Stamp multi-conversation content_hash only on chunk 0 to avoid O(N²) meta. - Always include results: [] on search error envelopes so callers never KeyError. - Clearer hybrid-search assertions in the closet isolation test.
…lish fix: release 3.7.0 polish — WAL reopen, search errors, hash stamp
from-sqlite ignored --dry-run and performed the real archive+rebuild (MemPalace#2095, MemPalace#2133). Preview after source validation, skip the destructive confirm, never take the mine-lock or rename the palace, and fail closed when SQLite row counts are unreadable instead of inventing zeros.
…-dry-run fix(repair): honor --dry-run for repair --mode from-sqlite (MemPalace#2133)
…n-mined-state fix(convos): honor mined state during dry runs
…age-soft-token fix(embedding): remap unsupported EmbeddingGemma token IDs
fix(hooks): ingest only the active transcript (MemPalace#2137)
…topwords feat(searcher): wire i18n stop words into BM25 tokenizer (MemPalace#973)
…emPalace#1217) scan_convos() now prunes any directory named 'subagents' during os.walk. Claude Code records Explore/Plan/Grep subagent transcripts in <session-uuid>/subagents/agent-*.jsonl and on a typical workspace these outweigh main session files ~80:1, dominating mining time and producing near-zero additional signal (the parent session already summarizes them). Adds a --include-subagents opt-in flag for users who want full history. The shared SKIP_DIRS set in palace.py is left untouched, so project mining (miner.scan_project) still descends into legitimate user-created subagents/ directories in code projects.
- Case-insensitive directory match (d.lower() == 'subagents') so the filter still kicks in if Claude Code or a plugin ever emits 'Subagents/' on case-preserving filesystems (Windows, macOS APFS). - Drop defensive getattr in cmd_mine: argparse always defines the attribute since --include-subagents is unconditionally registered. Direct args.include_subagents access matches every neighbouring field and fails loudly if the registration is ever removed. - Soften CLI help and docstring: drop the 80:1 ratio (reporter- specific) and the in-code (MemPalace#1217) reference. Add explicit default=False on the argparse flag for symmetry with --extract. - Add 2 negative tests: 'mysubagents'/'subagentsbackup' must still be mined (regression guard against substring-match), and 'Subagents/' must be skipped (case-insensitive coverage).
perf(mcp): make a proxied stdio session stop loading the storage stack
…us-columns fix(sqlite_exact): index wing/room/hall for structured access
On the i7-8750H chroma palace, list_drawers(limit=20) took 36s and find_tunnels died at 30s because both paged col.get() and cold-loaded the vector index. chroma.sqlite3 already has embedding_metadata. - sqlite_list_id_metadata: ids + metadata + chroma:document without HNSW - sqlite_room_wing_hall_counts shared with graph_stats - build_graph uses that grouped read when no collection is injected - find_tunnels/traverse no longer open the collection first MCP tests assert the client paging path is not used.
…o sql Follow-up on the same palace. The metadata scan joined every embedding_metadata row, and chroma:document lives in that table — so list_drawers pulled the palace's entire verbatim text into memory to render 20 previews, then filtered wing/room in Python after scanning the whole collection. Measured on the 1.7 GB / 165k-drawer chroma palace, list_drawers(wing=..., limit=20): before 2.31 s, 1148 MB peak RSS after 0.01 s, 86 MB peak RSS Unfiltered goes 1.61 s / 1162 MB -> 1.57 s / 894 MB. - exclude chroma:document from the scan; hydrate the displayed page via sqlite_documents_for_ids (two indexed steps — embedding_id is only indexed under UNIQUE(segment_id, embedding_id), so a single join on it scans embedding_metadata: 5.7 s for one page) - push the wing/room equality into SQL as a join per key - decode cells through _metadata_cell_value with column probing, so bool_value is not silently dropped on newer chroma schemas - grouped counts carry MAX(date), restoring find_tunnels' "recent", which the sqlite path had blanked (both backends) - resolve the graph's sqlite reader from the configured backend instead of sniffing the palace dir, so a two-backend directory still raises BackendMismatchError instead of being silently picked - find_tunnels/traverse fall back to the collection when sqlite cannot serve, so a missing palace reports "Chroma database missing" again rather than [] and "Room not found" Tests: sql tripwires for the scan (no documents, filter pushed down, id-scoped preview read), recent-from-sqlite, missing-palace diagnostics, and backend-gating for the graph reader.
…tadata-reads fix(chroma): read list_drawers and tunnels from sqlite, skip HNSW
… rule Coordination stalls are usually listening failures, not protocol failures: a task sits open because the agent it was addressed to was never watching, and the requester cannot tell "working on it" from "nobody is home". The load-bearing correction is the cursor. Events are ordered by append order (ORDER BY rowid), not wall clock, so a peer's event is appended whenever it syncs and can already be older than a timestamp high-water mark. Resuming with since_created_at therefore drops late-arriving cross-replica events permanently. Measured one such inversion in a single 50-event window: a windows-origin event created 09:10:48Z ingested after a mac-origin event created 09:13:21Z. list_events' docstring already said since_event_id is "the precise cursor ... regardless of timestamp ties" — that just never reached an agent. - coordination-protocol.md: new "Monitoring the stream" section — the cursor rule, four modes (inbox sweep / long-poll / SSE / declared-idle), the announce-your-watch convention, and declaring when you are NOT watching. Two new hard rules. - coordination-protocol.md: system-prompt snippet updated, since that block is copied verbatim into every agent's instructions. - agent-logstream.md: matching "Monitoring" concepts section. - mcp_server.py: event_list and event_wait descriptions now state the cursor rule at the point of use, and both since_created_at params are marked "NOT a resume cursor". The announce-your-watch shape is generalized from windows:grok:mempalace's issue-354-live-layer announcement, which named its filter, its cursor, and the work not to duplicate. Docs and tool descriptions only; no behavior change.
…an be woken by `logstream wait` is a primitive, not a watcher. It caps at MAX_WAIT_TIMEOUT_MS and reports a timeout, so every caller writes the same re-arm loop and each one has to remember to carry the cursor forward. Agents mostly did not, so coordinated tasks stalled on nobody listening rather than on the work. Two filters a watcher needs cannot be expressed by list_events, whose SQL is single-valued and positive-only: * "wake me for task.request OR patch.ready" * "everything addressed to me EXCEPT my own events" The second is not a nicety. to_agent=<me> deliberately also matches '*' broadcasts, and an agent's own broadcasts are broadcasts — so a hand-rolled watcher wakes itself every time it posts a status. Found by running one: mac-claude woke on its own fleet announcement within seconds. - logstream.watch_events(): owns the re-arm loop and the cursor. Yields ([], cursor) on idle polls so callers can time out, heartbeat, or checkpoint without a second clock. The cursor advances past every event examined, not only matches, so a restart never re-judges what it rejected. - event_matches_watch() / normalize_watch_values() / pushdown_watch_filters(): single-valued filters push down to SQL to keep the query selective; multi-valued and negative parts are re-checked client-side. Pushdown is an optimization, never a correctness dependency. - read/write_watch_cursor(): atomic temp+rename. A corrupt state file costs a replay; refusing to start costs every event after it, so reads degrade to None rather than raising. - CLI `logstream watch`: --agent (shorthand for --to-agent X --exclude-from-agent X), repeatable filters meaning "or", --state-file, --follow, --idle-exit-ms. Exit 0 on a match, 2 on idle — the same convention `wait` uses, so a harness can background the process and treat its exit as "you have mail". Docs updated to lead with it: coordination-protocol.md (including the system-prompt snippet every agent copies), the agent-logstream concepts page, and the CLI reference. 20 tests: self-exclusion at both the matcher and CLI level, multi-valued ORs, cursor advance-past-rejected, resume-without-replay, --since-event-id overriding the state file, and the exit-code contract.
…iew cursor docs Codex P2s on the watch PR, confirmed on Windows: - Cap each wait_events timeout to remaining --idle-exit-ms so idle 400ms does not wait out a 300s long-poll (measured 2687ms -> 572ms). - Persist the watch cursor after successful stdout on a match; unmatched advances still checkpoint immediately. A broken pipe no longer skips the event on resume. - preview=true docs told clients to re-fetch via since_event_id of the truncated event, but that cursor is strictly after it. Repeat the original filters with preview=false instead.
… the log windows:grok:mempalace flagged the first-run replay on PR MemPalace#2315. Measured on the real shared brain: a fresh `logstream watch --agent mac-claude` with no cursor woke holding 41 events, the oldest 49 days old. Nothing in the payload marks them stale, so an agent reads July's task.requests as new work — the opposite of the problem this command exists to solve. `latest_event_id()` already documents the right behaviour for the SSE live-tail: capture the tip at connect time "so they receive only post-connect events". The CLI watcher not doing the same made one product with two first-run semantics. - default: a watch with no --since-event-id and no stored cursor starts at the tip. Backlog belongs to the inbox sweep (`logstream list`), which can page it deliberately; a watcher is for what arrives from now on. - never silent: it prints what it skipped and how to get it, on stderr, in --json mode too, so the note cannot corrupt a parsed payload. - --from-start opts back into the replay. Six existing tests turned out to depend on the replay, which is the evidence this mattered. Their intent is filtering, not first-run behaviour, so the shared args helper opts them into --from-start and three new tests cover the tip default explicitly — including that an explicit --since-event-id or a state-file cursor still wins over it. Note for review: test_agent_shorthand_does_not_wake_on_your_own_broadcast asserts exit 2, which the tip default would satisfy for the wrong reason. It opts into --from-start so it keeps testing the exclusion. 4395 passed, 31 skipped.
Seven inline P2s on 8663ecd. Two were stale — Codex anchored them to lines 0d3f216 had already fixed. The other five were real, and all five are mine. (a) read_watch_cursor: a state file holding valid JSON that is not an object (null, [], a bare string) made .get() raise AttributeError, which the except clause did not catch — so the watcher refused to start, the exact opposite of the recovery contract in its own docstring. (b) KeyboardInterrupt returned normally, exiting 0. Exit 0 is the documented "a match was printed" signal, so SIGINT told a supervisor it had mail that never arrived. Now 130. (c) --poll-timeout-ms 0 with no idle deadline made the callable return zero forever, so watch_events' expired-deadline branch yielded without ever polling and burned a core. Rejected at the CLI, so that branch can now only mean a deadline actually elapsed. I saw this shape while reviewing 0d3f216 and wrongly filed it as unreachable in-tree. (d) A regression I introduced in 8663ecd. read_watch_cursor returns None for both "no state file yet" and "state file present but unreadable"; the tip-default treated the second like the first, skipping everything since the last good checkpoint and then re-checkpointing at the tip, making the loss permanent. A failed read now replays and says so, which is what read_watch_cursor promises: "a corrupt state file costs a replay". (e) --follow --json printed repeated indented documents, which json.load and jq reject as trailing data — a machine-readable flag that was not, on the mode built for daemons. Follow now emits NDJSON, one compact record per line; single-shot keeps the one pretty document. Five regression tests, each verified to fail against 8663ecd. The --poll-timeout-ms one initially passed pre-fix for the wrong reason — an idle timeout also exits nonzero — so it now pins the validation error itself rather than "some nonzero exit". 4400 passed, 31 skipped.
…behind Third Codex round, one P2 on 57d8d24, and a third variant of the same family as the last two: "no cursor" is not one fact. Sequence: a stateful watch starts against an empty log, so latest_event_id() is None and write_watch_cursor ignored the falsey cursor, leaving no file. The watcher idles out. An event arrives while it is stopped. The next launch sees no state file, concludes first run, jumps to that event as the new tip, and skips it — then checkpoints past it, making the loss permanent. The root cause both this and the previous round shared is that a missing cursor was being read as a single condition when it is four: absent no state file — a genuine first run, may start at the tip ok a usable cursor, resume strictly after it empty file exists recording cursor: null — started against an empty log, has not seen an event yet. NOT a first run. corrupt unreadable, truncated, or valid JSON that is not an object Only `absent` may start at the tip. `empty` and `corrupt` replay. read_watch_state returns (cursor, condition) and the CLI branches on the condition; read_watch_cursor stays as a wrapper for callers that do not care. write_watch_cursor now persists a null cursor rather than skipping the write, and the starting position is recorded at startup instead of waiting for the first idle yield, so a kill in between cannot reproduce the same hole. Three regression tests, each verified to fail against 57d8d24, including the full arrives-while-stopped sequence end to end. 4403 passed, 31 skipped. CI green on 57d8d24 across all nine checks.
…one must not fail quietly Fourth Codex round, two P2s on b35bfc1. Same family as rounds two and three — both are ways a *present* checkpoint gets mistaken for a first run — so both are checked against the invariant rather than patched individually: a restart may cost a duplicate, never a missed delegation. read_watch_state no longer preflights with os.path.exists(). That call answers False both for "no such file" and for "cannot traverse the parent directory", so a checkpoint that exists but is momentarily unreachable was classified absent, started at the tip, and skipped every event since the stored cursor. Opening directly lets FileNotFoundError mean absent and every other OSError — permissions, a directory in the way, I/O error — mean corrupt, which replays. It also closes the exists()/open() race. write_watch_cursor gains required=. Best effort is the right default because a lost checkpoint costs a replay while a crashed watcher costs every event after it. That trade-off inverts for the first checkpoint of a fresh watch: if it never lands there is no state file, the next launch calls itself a first run, and it starts at the tip — so a swallowed failure there guarantees the exact skip the checkpoint existed to prevent. The CLI now refuses to start rather than continuing into that. Three regression tests, each verified to fail against b35bfc1. The unreachable-file case is exercised twice, via a directory standing where a file is expected (OSError on every platform) and via a simulated PermissionError, so it holds on the Windows runner too. 4406 passed, 31 skipped.
…ced it Fifth Codex round, one new P2 on dd9b123, and the sixth instance of one bug: a starting cursor that never reaches disk, so the next launch calls itself a first run and jumps to the tip. This time --since-event-id with no state file skipped the immediate checkpoint entirely, because that write lived inside the "resolve the tip" branch and an explicit cursor never enters it. Nothing was persisted until watch_events yielded, which by default is up to five minutes later; an interrupt inside that window leaves no file at all. Rather than add a third call site I hoisted it: one place resolves the starting position, one place records it. The position can arrive three ways — an explicit --since-event-id, the tip, or None from an empty log or --from-start — and all three now take the same required write whenever no state file exists yet. That removes the shape the last four rounds kept finding new instances of, instead of removing one more instance. Two test-quality notes, both the same lesson: The first version of this test passed against the unfixed code. The helper uses a 60ms poll timeout, so watch_events yielded immediately and the loop's own checkpoint wrote the file — the startup window the bug lives in never opened. It now stubs watch_events to interrupt before yielding, which is what actually pins the startup write, and covers all three entry paths. The hoist also broke test_match_does_not_checkpoint_if_output_fails, which asserted that no state file existed after a failed match write. That was pinning the symptom: a startup checkpoint now legitimately exists. The invariant is that its cursor must still be the pre-match position so the undelivered event replays, so it asserts the cursor value instead. 4407 passed, 31 skipped.
…tive idle as forever Sixth Codex round, two P2s on b5c4af4. A typo'd or stale --since-event-id was persisted by the required startup write before anything checked it. list_events raises on an unknown anchor and that ValueError was uncaught, so the run died with a traceback while the bad id sat in the state file — and every later run without the flag reloaded it and died the same way until someone deleted the file by hand. The cursor is now verified before it is written anywhere. That check distinguishes where the cursor came from, because the right answer differs. An explicit --since-event-id that does not resolve is user error: refuse, and leave nothing behind to reload. A *stored* cursor whose event has gone — log rebuilt, replica reset — is corrupt state, and refusing would strand the watcher exactly as an unreadable file would, so it replays with a notice instead. Same invariant as the rest of this branch: a duplicate, never a missed delegation. --idle-exit-ms now rejects negatives. Only 0 documents "wait forever", but a negative took that same branch silently, so a value arriving from config or from timeout arithmetic would leave a harness waiting on a watcher it believed would time out. Three regression tests. The negative-idle one cannot simply be run against the old code to prove it: the pre-fix behaviour is an infinite wait, so the tripwire check hangs rather than fails, and confirming it needed an external timeout. Worth knowing before someone "fixes" that test by shortening it. 4410 passed, 31 skipped.
… polling Seventh Codex round, two P2s on af9b7e2, and the same shape twice: input that was only rejected once the loop had already started. Filter validation depended on arity. list_events sanitizes what it is given, so a single-valued watch filter was checked for free by being pushed down, while a repeated one was not pushed down and got compared raw. `--type Task.Request` alone was an error; `--type Task.Request --type patch.ready` was accepted and then matched nothing, so the watcher waited forever for an event type that cannot exist. sanitize_watch_spec now runs every value through the same sanitizers list_events uses, whatever the arity, and normalizes them so a padded routing value still matches. --limit 0 was accepted by argparse and raised inside the first poll, where the only handler was for KeyboardInterrupt: a traceback, and under --json no error document at all. Validated up front. Both are argument checks, so they belong together and ahead of everything else: _watch_spec now validates and builds the whole spec before any polling, cursor resolution, or checkpoint write. That ordering is the point — round six's bug was a bad cursor being persisted before it was validated, and keeping validation in one place ahead of side effects is what stops the next one. Added a ValueError backstop around the loop as well. Every known bad input is rejected earlier now, but an escape would otherwise surface as a traceback, and under --json as nothing a machine consumer can parse. Four regression tests, each verified to fail against af9b7e2. 4414 passed, 31 skipped.
…oring feat(logstream): background watcher agents can be woken by, plus the monitoring protocol
…ted-tereshkova-566240 fix(embedding): never let CoreML return NaN vectors for embeddinggemma
…borate-content fix(repair): check the FTS5 content table before rebuilding from it (MemPalace#2278)
Version bumped across all seven sources and CHANGELOG [Unreleased] promoted to [3.8.0]. Note for whoever cuts the next one: `integrations/openclaw/SKILL.md` carries a `version:` in its frontmatter and is the seventh source. version-guard checks five, a test enforces the README badge, and nothing at all enforces that one — it has been bumped by hand in every release commit back to 3.3.0. docs/RELEASING.md still says "all five". uv.lock carries only the mempalace version line. Regenerating it locally also resolved onnxruntime-gpu for Python 3.10 from 1.24.3 down to 1.20.2 and dropped the 1.24.3 entry — an unrelated dependency change that has no place riding along in a release bump, so the lock was restored and the single line edited instead. Verified with --frozen.
chore(release): 3.8.0
PR #394 squash-merged the v3.7.0 sync, so main carries that upstream content but not its ancestry — git's merge base stayed at 2ec4bae and every subsequent sync would replay all 433 already-resolved commits. This is a no-op tree-wise (-s ours): it only records 8516db7 as a parent so merge-base moves forward to the true synced point. Verified before recording: the v3.7 content IS present in main (kg_supersede, _aligned_query_ids, upstream repair flow, 48-tool surface). Future syncs must land as MERGE COMMITS, not squash, to keep this correct.
…flict files Preceded by an ancestry-repair commit (merge -s ours 8516db7): #394 landed as a squash, so the merge base was still pre-v3.7 and git wanted to replay all 433 already-resolved commits. With ancestry recorded, only the 166 genuinely new commits replayed. THIS PR MUST LAND AS A MERGE COMMIT (not squash) to keep that true for the next sync. Upstream: since/before date-window search across search/list/CLI (MemPalace#463), openai-compat embedding backend, RFC 002 adapter dispatch formalized (MemPalace#2062) — cmd_mine --source now uses it with typed exits, mempalace-mcp entry point → mcp_proxy, chunk_total metadata + interrupted-mine cleanup (MemPalace#2183/MemPalace#2122), read-paired source_mtime (#22), overview-cache invalidation, v3.8.0. Fork-preserved: adaptmem_ft alongside openai-compat (four embedding options), novelty tagging beside chunk_total in both miners, daemon-strict routing (getattr-tolerant for minimal test configs), delegating CLI search() with upstream's window parse + HNSW fence composed in, room-taxonomy warnings beside upstream's cache invalidation, tags + the 96f83d7 union guard. Window correctness extended to fork-only candidate sources upstream couldn't know: postgres BM25 arm, graph expansion, sqlite fallback top-up all honor [since, before); the pre-fusion trim keeps the full in-window pool. Tests: 6189 collected; suite green (6099 passed, 82 skipped, 123 deselected; two deselects are known worktree-run artifacts: the PYTHONPATH-leak init test and the -I-subprocess reload test — both exercise the installed tree, green in CI). ruff check+format clean; check-docs 7/7; uv.lock regenerated; 48-tool surface unchanged. Fixes #400. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Important Review skippedToo many files! This PR contains 160 files, which is 60 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (160)
You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
… of tags) v3.6/v3.7 hit this 2026-08-09; v3.7.1/v3.8.0 again today. The compare family carries no lint value once the pattern is structural.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#394 landed as a squash, which severed upstream ancestry: this sync initially wanted to replay all 433 already-resolved commits. The first commit here (
910a2a54, tree-identicalmerge -s ours 8516db7f) records the true synced point, dropping the real conflict set to 15 files. Squashing this PR would throw that away again.Summary
Sync through v3.8.0 (
3e56979f, 166 commits). Upstream:since/beforedate-window search across search/list/CLI (MemPalace#463 family),openai-compatembedding backend, RFC 002 source-adapter dispatch formalized (MemPalace#2062 —cmd_mine --sourcenow uses it with typed exits),mempalace-mcp→mcp_proxy,chunk_totalmetadata + interrupted-mine cleanup, read-pairedsource_mtime, overview-cache invalidation.Fork-preserved through the 15 conflicts: adaptmem_ft alongside openai-compat (four embedding options), novelty tagging beside
chunk_totalin both miners, daemon-strict CLI routing (now getattr-tolerant of minimal configs), the delegating CLIsearch()with upstream's window parse + HNSW fence composed in, tags + the96f83d7union guard. Window correctness extended to the fork-only candidate sources upstream couldn't know about: postgres BM25 arm, graph expansion, and the sqlite fallback top-up all honor[since, before); the pre-fusion trim keeps the full in-window pool (upstream's review finding, applied to the fork flow).Test plan
-I-subprocess reload test) — both exercise the installed tree and run green in CI.ruff check+formatclean;check-docs.sh7/7; uv.lock regenerated; tool surface stays 48.🤖 Generated with Claude Code