fix: import dedup off-switch honesty + brain-owned lifecycle for the background pass

The post-import background deduplication pass (a merge-DELETE writer,
debounced ~5 minutes after import) had four lifecycle defects:

- enableDeduplication: false did not gate the background schedule — an
  import that explicitly opted out could still have entities auto-removed
  minutes later. The flag now gates both the inline and background passes.
- Each import() constructed its own coordinator-owned deduplicator, so the
  debounce never spanned imports (N imports = N delete timers). The brain
  now owns a single lazy instance (getBackgroundDeduplicator).
- close() never cancelled pending dedup; a delete pass could fire against
  a closed brain. close() now cancels it first.
- The 5-minute timer held the process open (exit-hang class); now unref'd.

Four regression tests pin the contract (background-dedup-lifecycle);
import guides document that false disables both passes.
This commit is contained in:
David Snelling 2026-07-18 10:40:45 -07:00
parent 120205b69c
commit 4fcef7b8ed
7 changed files with 179 additions and 10 deletions

View file

@ -10,6 +10,28 @@ Full auto-generated changelog: `CHANGELOG.md` · Releases: https://github.com/so
---
## v8.8.1 — 2026-07-18 (the import dedup off-switch is now honest + lifecycle-safe)
The post-import background deduplication pass (a merge-DELETE writer that runs ~5 minutes
after an import, merging entities judged duplicates by id / name / vector similarity) had
three lifecycle defects, all fixed:
- **`enableDeduplication: false` now actually disables it.** The background pass was
scheduled unconditionally — an import that explicitly opted out could still have
entities auto-removed 5 minutes later. The flag now gates BOTH the inline merge and
the background pass (regression-pinned).
- **One deduplicator per brain, owned by the brain.** Each `import()` call constructed its
own coordinator + deduplicator, so the "debounced" timer never actually debounced across
imports (N imports = N delete timers). The brain now owns a single instance — the
debounce genuinely spans imports — and `close()` cancels pending work, so a delete pass
can never fire against a closed brain.
- **The 5-minute timer is unref'd** — a pending pass no longer holds the process open
(the exit-hang class; this timer had escaped the earlier sweep).
Retention note for keep-everything deployments: with `enableDeduplication: false` on
import calls and `retention: 'all'` in config, no engine path removes records
automatically.
## v8.8.0 — 2026-07-17 (OS-limit detection for pool-scale deployments)
Small minor: brains now detect the two OS limits that bite at pool scale and warn **before**