fix(palace): file_already_mined iterates all groups when source_file has multiple parent_drawer_id mining passes - #1652
Merged
Conversation
…has multiple parent_drawer_id mining passes
Under the additive-mining model (drawer history preserved across re-mines),
a single source_file can have multiple parent_drawer_id groups in the
palace, one per mining pass. Each group carries its own stored
source_mtime and normalize_version.
`file_already_mined` previously used `collection.get(where={"source_file": X},
limit=1)` in the `extract_mode is None` branch and only checked the single
returned row's metadata. ChromaDB's `get(..., limit=1)` has no ordering
guarantee across multiple matching rows, so the returned row was effectively
arbitrary. When ChromaDB happened to return a stale group (older mining
pass with a different stored mtime), the function returned False, the
additive miner concluded the file had changed, and wrote yet another
duplicate group of drawers for a file that had not actually changed since
the last successful mine.
Steady-state failure: for any source_file that has ever been edited (so
that the palace contains groups with different stored mtimes), each re-mine
has a probability of spuriously concluding "file changed" and writing
another duplicate group. Each spurious group compounds the problem because
its stored mtime can also trigger the next spurious re-mine. Storage grows
without bound; search results, hallway counts, and entity-frequency stats
become inflated proportionally.
The fix mirrors the paginated-iteration pattern already used in the
`extract_mode is not None` branch — iterate every drawer for the
source_file in 1000-row pages, short-circuit on the first matching group.
A correct group is one that passes the existing checks: normalize_version
not stale; if check_mtime, stored source_mtime within 0.001 seconds of the
current file mtime. The two branches (extract_mode is None vs set) collapse
into one loop that skips the extract_mode check when no extract_mode was
specified.
Trade-off: average-case cost rises from O(1) limit=1 query to O(N/1000)
paginated scan. For typical sources (1-3 groups) the cost is unchanged
because the loop short-circuits on the first matching group within the
first page. For pathological sources with thousands of groups, the cost
is O(number-of-pages-until-match) — still bounded, no longer flaky.
RED test pins the failure space deterministically
`test_file_already_mined_handles_multiple_groups_under_one_source_file`
uses a MockCollection that simulates two parent_drawer_id groups under one
source_file: a STALE group (older mtime) and a CURRENT group (matching
mtime). The mock returns the STALE group when called with limit=1
(worst-case ChromaDB ordering) and returns BOTH groups when called with
limit=1000 (what a correctly-iterating implementation must do). Test asserts
file_already_mined returns True even when limit=1 picks the stale group.
- Against pre-fix code: test FAILS (function returns False because
limit=1 picks stale group, mtime mismatch returns False)
- Against post-fix code: test PASSES (iteration finds the current group,
short-circuits to True)
Verified RED-then-GREEN locally. Existing 4 file_already_mined tests in
tests/test_miner.py continue to pass:
- test_file_already_mined_check_mtime
- test_file_already_mined_scopes_convo_extract_mode
- test_file_already_mined_extract_mode_paginates_large_sources
- test_file_already_mined_returns_false_for_stale_normalize_version
Verification
- macOS Python 3.12 (local) full pytest : 2268 passed, 0 failed
- Linux Python 3.9.25 (OrbStack) : 2260 passed, 0 failed
- Linux Python 3.11.15 (OrbStack) : 2261 passed, 0 failed
- Linux Python 3.13.13 (OrbStack) : 2261 passed, 0 failed
- ruff check + ruff format --check : all clean
Provenance
Surfaced during the per-query audit on the PR #1628 amendment cycle (the
search for every bare `where={"source_file": ...}` query in the repo).
One of six sites identified. The other five are legitimately file-global
in intent (closet purges, full-rebuild deletes, paginated mode-filtered
scans). This site is the one whose failure mode mirrors the cross-group
stitching pattern PR #1628 fixed at the searcher layer.
Contributor
There was a problem hiding this comment.
Code Review
This pull request updates the file_already_mined function in mempalace/palace.py to correctly handle multiple groups under a single source_file. Instead of relying on a limit=1 query which has undefined ordering in ChromaDB and can return stale groups, the function now uses a paginated approach to iterate through all groups and check if any of them match the current file's modification time and version. Additionally, a unit test has been added in tests/test_miner.py to verify this behavior using a mock collection. There are no review comments, so no feedback is provided.
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.
Summary
Under the additive-mining model (drawer history preserved across re-mines), a single
source_filecan have multipleparent_drawer_idgroups in the palace — one per mining pass — each with its own storedsource_mtimeandnormalize_version.file_already_mined(the function the project miner uses to decide whether a file needs to be re-mined) previously usedcollection.get(where={"source_file": X}, limit=1)and only checked the single returned row. ChromaDB does not guarantee ordering forlimit=1across multiple matching rows, so the returned row was effectively arbitrary. When ChromaDB returned a stale group (older mining pass), the function returned False, the miner concluded the file had changed, and wrote yet another duplicate group of drawers for a file that had not actually changed.This PR replaces the
limit=1shortcut with the paginated-iteration pattern theextract_mode is not Nonebranch has always used. The function now returns True if ANY stored group is current (matching version + matching mtime), regardless of which group ChromaDB orders first.Closes #1653.
What changes
mempalace/palace.py—file_already_minediterates all groups for the source_file in 1000-row pages, short-circuits on the first matching group. The two branches (extract_mode is Nonevs set) collapse into one loop that skips the extract_mode check when no mode is specified.tests/test_miner.py— addstest_file_already_mined_handles_multiple_groups_under_one_source_file, a deterministic regression test that uses a MockCollection to force worst-case ChromaDB ordering (returns stale group onlimit=1) and asserts the function correctly returns True by iterating.How the fix works
Trade-off: average-case cost rises from O(1)
limit=1query to O(N/1000) paginated scan. For typical sources (1–3 groups) the cost is unchanged because the loop short-circuits on the first matching group within the first page. For pathological sources with thousands of groups, the cost is O(pages-until-match) — still bounded, no longer flaky.Failure shape this prevents
Behavioral, not data-loss. Storage grows without bound; search results may show same content N times; closet pointers, hallway counts, and entity-frequency stats become inflated proportionally. Invisible until
mempalace statusreveals the bloat.Test plan
RED-then-GREEN pinned deterministically. The new test uses a MockCollection that simulates two
parent_drawer_idgroups under onesource_file:source_mtime) returned forlimit=1calls (worst-case ordering)limit=1000paginated callsAgainst pre-fix code: test FAILS (function returns False because
limit=1picks stale group). Against post-fix code: test PASSES (iteration finds the current group).ruff check+ruff format --checkExisting 4
file_already_minedtests continue to pass:test_file_already_mined_check_mtimetest_file_already_mined_scopes_convo_extract_modetest_file_already_mined_extract_mode_paginates_large_sourcestest_file_already_mined_returns_false_for_stale_normalize_versionBackwards compatibility
parent_drawer_id) is also unchanged — the function iterates one drawer and returns the same result it would have underlimit=1Provenance
Surfaced during the per-query audit on PR #1628's amendment cycle (the search for every bare
where={"source_file": ...}query in the repo). One of six sites identified; the other five are legitimately file-global in intent (closet purges, full-rebuild deletes, paginated mode-filtered scans). This site is the one whose failure mode mirrors the cross-group stitching pattern PR #1628 fixed at the searcher layer.🤖 Generated with Claude Code