feat(sync): re-enrich stale detail content — --re-enrich + enrich.invalidateOn (v1.53.0) - #180
Merged
paulkr merged 3 commits intoAug 11, 2026
Conversation
…alidateOn (v1.53.0) (#174) A record's detail endpoint was fetched exactly once, ever. Phase 2 selects `WHERE <tsField> IS NULL` and phase 1's upsert deliberately preserves the stamp, so nothing cleared it — including `--full-refresh`, which is the flag a user would reasonably reach for. Stale detail content was permanent; the documented workaround was to delete the SQLite mirror. Implements the sequencing the issue and @ayushsingh82 both favoured: fingerprint invalidation as the default, with a manual escape hatch. - `enrich.invalidateOn`: name a list-endpoint field that moves when the detail changes (historyId for Gmail, updated_at for Fathom). The value seen at enrich time is recorded in `_enrich_fp`; on the next sync, rows whose fingerprint moved have their stamp cleared and are re-enriched. Unchanged records cost nothing. - `one sync run <platform> --re-enrich`: clears every stamp and re-fetches all detail endpoints. One detail call per record, so it is opt-in per run and never implied by `--full-refresh`. - `--no-memory` now applies to phase 2. Phase 1 has always honoured it; the enrich phase's memory mirror ignored it, so the flag silently leaked. Invalidation is additive: rows with no recorded fingerprint are never auto-invalidated, so adding `invalidateOn` to a profile cannot trigger a whole-table re-enrich on the first run after upgrading. Profiles with no suitable fingerprint field simply omit it. Also corrects the documented enrich concurrency default (3 -> 5) in the same section, which did not match DEFAULT_CONCURRENCY. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 11, 2026
Merged
…er-re-enrich-full-refresh-leaves
… now fixed `PRE-EXISTING GAP: --no-memory does not stop phase 2 mirroring into memory` asserted the broken behaviour on purpose, "pinned here so it can't regress silently in either direction". This PR is that change in the other direction: phase 2's memory mirror is now gated on ctx.writeToMemory, so the record must NOT be written. Also asserts the detail endpoint is still called, so the test can't pass by accidentally disabling enrichment altogether — --no-memory suppresses the unified-memory dual-write, not the enrich phase. I missed this locally because better-sqlite3 will not install on Node 24, so the whole enrich-preserve suite skips here. CI caught it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
paulkr
approved these changes
Aug 11, 2026
paulkr
deleted the
feature/int-4394-sync-enriching-profiles-never-re-enrich-full-refresh-leaves
branch
August 11, 2026 14:27
paulkr
pushed a commit
that referenced
this pull request
Aug 13, 2026
…alidateOn (v1.53.0) (#180) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Fixes #174 · INT-4394
Problem
For a profile that declares
enrich, each record's detail endpoint was fetched exactly once, ever. Phase 2 selects only never-enriched rows (WHERE "<tsField>" IS NULL) and phase 1's upsert deliberately preserves_enriched_at, so nothing in the codebase ever cleared it.--full-refreshdid not help: it re-pulls every record from the list endpoint and reconciles deletions, but surviving rows keep their stamp — so phase 2 saw zero unenriched rows and made zero detail calls. Stale detail content was permanent, and the documented workaround was to delete.one/sync/data/<platform>.db.Approach
@ayushsingh82's sequencing note on the issue argued for fingerprint invalidation as the default with the manual flag as the escape hatch, and that both share the same underlying "clear the stamp" path. That is what this implements — both halves, since the shared path is the bulk of the work either way.
enrich.invalidateOn— automatic, the common case. Name a list-endpoint field that moves when the detail changes (historyIdfor Gmail threads,updated_atfor Fathom meetings). The value observed at enrich time is recorded in a_enrich_fpcolumn; on the next sync,invalidateStaleEnrichments()clears the stamp for rows whose fingerprint moved, and phase 2 re-fetches exactly those. Records that did not change upstream cost nothing.--re-enrich— manual, the escape hatch.clearEnrichmentStamps()unstamps everything for the model and phase 2 re-fetches all detail endpoints. One detail call per record, so it is opt-in per run and deliberately not implied by--full-refresh. For profiles with no sensible fingerprint field, or when the detail shape changed.Additive by design. Rows with no recorded fingerprint are never auto-invalidated (
_enrich_fp IS NOT NULLguard). Without it, addinginvalidateOnto a built-in profile would re-enrich the user's entire table the first time they upgraded. Profiles that omitinvalidateOnkeep today's exact behaviour.Also fixed
--no-memoryleaked through phase 2. Phase 1 gates its memory write onoptions.toMemory !== false; the enrich phase's mirror ignored it and wrote anyway. This is the gap pinned by the test literally namedPRE-EXISTING GAP: --no-memory does not stop phase 2 mirroring into memory, which the issue folded in as a secondary ask.While editing the same doc section, corrected the documented enrich
concurrencydefault from 3 to 5 — it did not matchDEFAULT_CONCURRENCY.Docs
Updated all four mandated surfaces:
guide-content.ts(new "Re-enriching" section replacing the "Known gap" text, plusinvalidateOnin the enrich config reference and thesync runflag row),skills/one/SKILL.md, andREADME.md. The "delete the mirror to force it" advice is gone from all three.Tests
12 new tests in
enrich-invalidation.test.ts, including the two guards most likely to regress: never invalidating never-fingerprinted rows, and degrading to a no-op wheninvalidateOnnames a field the list endpoint does not return.They drive a fake driver rather than the real one —
better-sqlite3is anoptionalDependencyand will not install on Node 24, so a test needing it would silently skip on the machines most likely to run it. They therefore assert the emitted SQL and the guard decisions, not SQLite's execution of them; theIS NOTnull-safety andCAST(... AS TEXT)comparison are unexercised against a real database and are worth a look during review.Verification notes
npx tsc --noEmitpasses; new suite 12/12.npm testsuite: on Windows it reads and writes the developer's real~/.one(tests sandboxprocess.env.HOME, whichos.homedir()ignores on win32).package-lock.jsonversion fields hand-edited rather than regenerated, sincenpm installon Node 24 drops thepgoptional dependency from the lockfile.mainand touchsync/index.tsin different places. Whichever merges second will need its version bump rebased.🤖 Generated with Claude Code