Skip to content

fix(sync): index newly watched local projects after attachment - #1861

Closed
TPSJASPER wants to merge 1 commit into
basicmachines-co:mainfrom
TPSJASPER:fix/1817-new-project-watch
Closed

TPSJASPER wants to merge 1 commit into
basicmachines-co:mainfrom
TPSJASPER:fix/1817-new-project-watch

Conversation

@TPSJASPER

@TPSJASPER TPSJASPER commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Why

Fixes #1817

A project created in an empty folder is watched only after the next project reload. Files copied into it before that reload produce no watch events, so they stay unindexed until they change, bm reindex runs, or the server restarts. The watcher also filtered projects with the config snapshot it was created with, so a project added later could stay unwatched even after a reload.

What changed

  • _select_projects_to_watch re-reads the current ConfigManager config and keeps the existing locally-syncable and constrained-project rules.
  • WatchService tracks projects it has finished catching up by project external id, not by root path. Startup projects register from their existing initial scan.
  • After a new project's watch is attached, one project index pass runs before that cycle handles its event batches. It covers files copied before attachment that produced no events. A failed pass is recorded and retried on the next cycle; a pass interrupted by a stop stays pending.
  • A path change no longer triggers a full scan of the new root. bm project move rewrites the path before the user moves the files, so scanning the still-empty root would plan every old path as deleted and remove the project's index.
  • The reload timer waits while the catch-up pass is running and draining its buffered batches. watchfiles clears pending changes when stop is set, so closing the cycle underneath the scan would discard changes collected for every project. The wait is capped at one extra interval so a project with continuous writes cannot defer reloads forever.

The bm-project manual page describes the new-project catch-up pass.

Validation

  • New watcher regression tests: 11 passed, covering current-config selection, files copied while attaching, ignore rules, startup projects not scanned twice, per-project failure and retry, removed projects, restart during catch-up, a real watchfiles backfill plus live write, cancellation, path changes, and the reload wait.
  • Existing watcher, initialization and new-project MCP tests: 23 passed, 5 skipped.
  • Full tests/index suite on SQLite: 210 passed, 10 skipped, 1 deselected.
  • The new public MCP flow creates a project through bm_project, copies a nested Markdown file before attach, and finds it with bm_search.
  • ruff check, ruff format --check, ty check for the changed files, and git diff --check passed.
  • doctor --local completed sandboxed write, index and search in an isolated config directory.

The full-repository ty check still reports the same 8 environment diagnostics as the unmodified baseline (optional pymilvus/uvloop, Windows resource members); no ignores were added. The full test suite and Postgres tests were not run. The implementation was also replayed against origin/main with git apply --check.

Risks / rollback

The catch-up runs once per newly seen project identity after attachment; files written during the pass are still delivered by watch events. Startup still relies on its existing background initial scan rather than a second pass. Revert the commit to roll back.

The watcher selected projects from the config snapshot it was created with, so a project added later could stay unwatched. Re-read the current config on every selection.

A watch attaches after the project's first scan; files copied before attachment produced no events. Once a new root is watched, run one project index pass before handling its event batches, retry a failed pass on the next cycle, and keep it pending while a restart is requested. Track projects by identity, not root, so a path change does not rescan an empty directory and delete the existing index. A reload waits for the catch-up drain so watchfiles does not discard buffered batches; startup projects keep their existing initial scan and are not scanned twice.

Fixes basicmachines-co#1817

Signed-off-by: TPSJASPER <592827953@qq.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-25T08:23:17.174687Z
We couldn’t complete this review. Comment @codex review to try again.
48550d8 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@phernandez

Copy link
Copy Markdown
Member

Thanks, @TPSJASPER, for investigating the new-project watcher gap and contributing the fix and thorough regression coverage!

We cherry-picked your commit into #1866, preserving your authorship and sign-off. That PR also adds a real MCP/native-watcher lifecycle integration test and fixes an additional catch-up gap exposed by continuous ignored file activity. Closing this PR in favor of #1866, which is approved and passing CI. Thank you for the contribution!

@phernandez phernandez closed this Oct 25, 2026
phernandez added a commit that referenced this pull request Oct 25, 2026
…1865)

## Why

Two reported SQLite failures block reindexing or startup: deleting a
large chunk manifest exceeds SQLite's expression-depth limit (#1850),
and an interrupted script-ngram migration leaves a scratch table that
prevents the next startup (#1853). Both reproduce on current main with
the contributor's regression tests.

Fixes #1850. Fixes #1853.

## What Changed

- Cherry-pick @MbappeWU's fixes from #1857 and #1859, retaining
authorship and sign-offs.
- Batch pending-vector deletion predicates and row-id deletes in groups
of 200.
- Drop a leftover `search_index_rebuilt` scratch table before rebuilding
the intact source index.
- Add real SQLite integration tests covering a 1,201-record delete,
protected generations/project partitions, repeated deletion, and
upgrade/downgrade retries with empty or populated scratch tables.

## Implementation Details

Deletion retains the existing project, key, source-hash and
pending-status guards in one transaction. Batching limits expression
depth and bound parameters without changing the concurrency protocol.

The migration rebuilds from `search_index`; the scratch table has no
authoritative state. Integration tests commit the interrupted state
before opening a new transaction and verify preserved full-text matches
afterward. They use Alembic's real Operations context and sqlite-vec
without application mocks. Contributor workflow-evidence notes are
omitted from the final diff.

## Testing

- Before fixes: the two new contributor regressions fail on main with
`Expression tree is too large` and `table search_index_rebuilt already
exists`.
- `uv run pytest
tests/repository/test_sqlite_vector_search_repository.py
tests/test_script_ngram_search_migration.py
test-int/test_sqlite_index_recovery.py -q -o addopts= --tb=short`: 46
passed.
- `just fast-check`: lint, format and full typecheck passed.
- `just doctor`: sandboxed write, index and search passed.
- `git diff --check`: passed.

## Risks / Follow-ups

Changes are SQLite-specific; PostgreSQL production paths are unchanged.
The recovery assumes the original FTS table remains intact, matching the
reported interrupted upgrade. #1861's watcher lifecycle changes are
reviewed separately.

---------

Signed-off-by: MbappeWU <MbappeWU@users.noreply.github.com>
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: MbappeWU <MbappeWU@users.noreply.github.com>
phernandez added a commit that referenced this pull request Oct 25, 2026
…1866)

## Why

Files copied into a newly created project's empty folder before the next
watcher reload produce no events and stay unindexed. The watcher also
uses a startup config snapshot that can exclude projects added later.

Fixes #1817. Carries forward @TPSJASPER's #1861, cherry-picked with
original authorship and sign-off.

## What Changed

- Reload current project config when selecting local projects to watch.
- After attaching a newly seen project's watch, run a catch-up index
pass. Preserve startup scan ownership and project identity across path
moves.
- Retain the contributor's retry, cancellation and reload-drain
coverage.
- Handle continuous ignored events during attachment: observe raw
batches in catch-up cycles and apply the existing visibility filter
before indexing.
- Add a production MCP lifecycle integration test with a real native
watcher, database, file indexing and embeddings. It creates a project
through `bm_project`, copies a note, checks search, then edits the file
and checks search again while hidden-file activity continues.

## Implementation Details

The catch-up starts only after `awatch` has attached its watches.
Running in the event consumer prevents the scan and event batches from
overlapping. Project identities track completed catch-up, so changing an
existing project's path does not scan a still-empty move destination and
delete its index.

The new integration test found an additional gap in the original
contribution: watchfiles suppresses batches whose events are all
filtered. Continuous hidden-file activity can therefore suppress both
observable batches and timeout yields, preventing catch-up indefinitely.
New-project cycles receive raw batches and filter them locally. Only an
actual quiet batch clears the drain guard.

The integration test uses the real MCP lifespan and normal database
initialization, rather than API overrides or application mocks. Its only
environment overrides select production runtime behavior; a bounded poll
waits for observable search results. Existing inline tests remain
available for fast feedback.

## Testing

- The production lifecycle test failed against the unmodified
cherry-picked #1861 (45-second search deadline); passes with the
ignored-event fix.
- `uv run pytest tests/index -q -o addopts= --tb=short`: 221 passed.
- `uv run pytest tests/index/test_watch_new_projects.py
tests/mcp/test_new_project_watching.py
tests/services/test_initialization.py tests/index/test_watch_service.py
-q -o addopts= --tb=short`: 33 passed.
- `uv run pytest test-int/mcp/test_new_project_watch_lifecycle.py -q -o
addopts= --tb=short`: passed on SQLite with continuous hidden-file
activity.
- `BASIC_MEMORY_TEST_POSTGRES=1 uv run pytest
test-int/mcp/test_new_project_watch_lifecycle.py
tests/index/test_watch_new_projects.py
tests/services/test_initialization.py -q -o addopts= --tb=short`: 24
passed.
- `just fast-check`: lint, format and full typecheck passed.
- `just doctor`: sandboxed write, index and search passed.
- `git diff --check`: passed.

## Risks / Follow-ups

New projects receive one project-wide scan after watch attachment, which
can delay event processing while that scan runs. Reload deferral remains
bounded as in the contributor's implementation. This changes local
watching only; hosted indexing is unaffected. Existing project path
moves retain their current behavior.

---------

Signed-off-by: TPSJASPER <592827953@qq.com>
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: TPSJASPER <592827953@qq.com>
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.

Files copied into a just-created project are missed until the next watcher reload

2 participants