feat(mining): incremental passive mining via per-source UID watermark - #2882
Merged
Conversation
Replace the fragile, user-level "since" heuristic (which re-scanned the full inbox whenever a prior task was never persisted as done, and re-read whole days due to IMAP SINCE day-precision) with a durable per-folder UID watermark stored in mining_sources.config. - emails-fetcher: accept resumeFrom; fetch UID ranges [last_uid+1 .. uidNext-1] chunked, no SEARCH, when uidvalidity matches; track max UID per folder and emit it on the final progress message; fall back to since/full on mismatch. - backend: thread resumeFrom+sourced sourceId; persist the watermark ONLY on full pipeline success (never on cancel) via a PATCH to mining-sources; typed mining_sources.config contract with legacy normalization. - supabase: PATCH /mining-sources/:id/config with atomic jsonb deep-merge (row-locked, SECURITY DEFINER, search_path=''); passive-mining resumes from the watermark and falls back to since only on first run; passive lifecycle now derives from task rows instead of a start-time "completed" write; re-auth recovery clears the new health.state and legacy keys. - frontend: typed config mirrors + read-merge-write (fixes wholesale-replace that wiped sibling keys); source health badge from config.health; resume-then- consume watermark; passive dialog persists selected folders + flags. Validated end-to-end against a live Gmail inbox: full first run establishes the watermark; subsequent runs fetch only UID ranges above it (0 messages when nothing new) and advance the watermark only on completion.
…ts, drop unauthenticated invoke Audit fixes from combined #2881/#2882 review, targeting the passive-mining surface (fetch-mining-source + mining-sources): - Expiry unit bug: credentials store expiresAt in epoch ms but simple-oauth2's expired() parsed numbers as seconds, so auto-refresh never fired. normalizeExpiresAtMs/isTokenExpired/refreshedExpiresAtMs now handle ms, seconds and ISO strings and write back a single canonical epoch-ms value. - Import-time crash: getRequiredEnv at module load killed the whole function on deployments missing a var; envs() + getOptionalEnv resolve lazily and FRONTEND_HOST/OAUTH_CALLBACK_BASE_URL degrade to empty. - OAuth clients (google/azure) built lazily so single-provider deployments don't crash at import when both clients are pulled into scope. - Drop obsolete unauthenticated public.invoke_edge_function(TEXT); the authenticated private.invoke_edge_function is the single path. - Regression tests: expiry normalization, write-path persistence of epoch-ms, env-helpers optional/required semantics (28 passing).
malek10xdev
added a commit
that referenced
this pull request
Sep 9, 2026
…oid collision PR #2882 introduces 20260904000000_passive_mining_incremental_config.sql, so the refine ledger migration must move to 20260904000001 to keep migration ordering deterministic when both merge to main.
- Replace non-null assertions with a narrowed handler const (JS-0339). - Return Promise.resolve() instead of empty async shutdown (JS-0321). - Pass missing expiry through a variable instead of a literal undefined (JS-W1042). - Mark fake test credentials with skipcq: SCT-A000 pragmas (official DeepSource method for placeholders in test files).
…urface - miningSourceConfig.ts (frontend + _shared Deno mirror): extract legacy-key folding into foldLegacyHealth/Flags/Mining helpers to bring normalizeConfig under the cyclomatic-complexity threshold (JS-R1005). - sources.ts deepMerge: collect null-removed keys and filter on return instead of deleting computed property keys (JS-0320). - ImapConnectionProvider: replace `import * as nodeTls` with named imports checkServerIdentity + type PeerCertificate (JS-C1003).
…ining conflict Resolution keeps the V1 config-write architecture (patchSourceConfig / recordRunStart / recordRunFailure, per-folder UID watermark resume, saved folders from config.folders) and ports #2880's error surfacing onto it: - backendError() helper + type-aware OAuth 401 classification in the cron error path (invalid_grant OR 401 on an OAuth-type source => permanent needs_reauth; plain IMAP 401 stays retrying). - getMiningSources: fetch broadly (passive_mining=true) and filter re-auth in code covering both legacy needs_reauth and health.state shapes; select now includes `type` for the 401 classification. - getBoxes uses backendError; startMiningEmail keeps resumeFrom/since fallback and flags from the parsed V1 config.
The merge with main unlocks ci-pr.yml on this branch for the first time; its backend job runs `prettier --check .` which failed on this PR-owned test file (formatting-only change, 607/607 tests still pass).
Cancel during extraction/cleaning (after fetch completed) still persisted the passive-mining watermark, permanently skipping un-mined messages. Now every Canceled task in the pipeline blocks persistence. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
Carry the incremental mining improvements to PR #2882, including per-folder IMAP cursor state, safe config updates, and cancellation-aware watermark persistence. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
…alth Address the end-to-end audit findings on incremental passive mining: - fetcher: add a pure folder-fetch planner (uid-resume/since/full/skip) and run SEARCH SINCE while the mailbox is open, fixing the fallback that silently fetched nothing. Only exact scans advance the UID watermark; date-filtered scans never advance it, so no message can be skipped. - backend: persist the watermark when the mining tasks settle, independent of optional enriching/signature tasks, and resolve the owned source id when a run is started by email only. - frontend: resume from the persisted watermark by default in the mine wizard with an explicit "re-mine everything" opt-out; derive the source badge from health.state so healthy IMAP sources no longer show "Credential expired". Tests: backend 609, emails-fetcher 59, frontend focused 33; builds clean.
- format MiningPipeline and the new frontend/fetcher test files - drop redundant `undefined` args flagged in sourceStatus/miningRunMode/ miningFolderState tests; make resolveMiningFolderState's watermark optional - sse.initConnection returns consistently; document the Promise-typed onopen - folderPlan tests return resolved promises instead of async-without-await
…ction
Replace the pipeline-side, clean-aware persistence with the intended flow:
- FetchTask persists the fetcher's watermark into its own task row
(details.watermark) when the fetch succeeds.
- The Pipeline triggers the new mining-completion edge function once the
Extract task succeeds, independently of the optional clean and signature
tasks (cleaning is user-configurable and must not gate the cursor).
- mining-completion reads the fetch task, builds the {health, mining.last}
patch and writes it through the atomic config RPC.
- backend completion client uses supabase.functions.invoke; passive-mining
writes config through the Supabase admin client.
Removes the mining-task gating, backend payload building and raw fetch use.
This was referenced Sep 11, 2026
Closed
…nfig writer
- enums: mirrored MiningRunMode/TaskStatus/SourceHealthState/FolderStatus/
SourceBadge with a contract test against contracts/mining-enums.json.
- getImapBoxes: joins the source's persisted watermark with the live IMAP
cursor and returns per-folder { watermark, status, has_new_messages,
latest_uid }, so the frontend no longer computes cursors.
- resume: the backend derives the per-folder { uidvalidity, last_uid } map
from the source config; the mine request no longer carries `resumeFrom`,
and the emails-fetcher keeps all IMAP logic.
- config: mining-sources is the single writer. It merges caller params
(mining_flags/folders/passive_mining/health/mining) with supabase-js and a
`config_revision` compare-and-swap; dropped update_mining_source_config and
jsonb_deep_merge. mining-completion, fetch-mining-source and passive-mining
now go through it.
- frontend: removed the UID cursor state machine and the run-token counter;
SSE callbacks are guarded by the active miningId. Status uses enums.
- frontend: remove the localStorage run-mode persistence and the force-full utility; ask continue-vs-rescan in a transient dialog only when pre-mined folders have new messages, and pass the choice as a request param. - enums: use MiningRunMode/TaskStatus/SourceHealthState in the backend and edge functions; consolidate the duplicated SourceHealthState in _shared. - migrations: squash the three config/RPC migrations into one (20260912010857_passive_mining_incremental_config); keep the security one. - repo: remove the contracts/ folder; enum tests assert values inline.
malek10xdev
added a commit
that referenced
this pull request
Sep 14, 2026
…2885) The amber/red folder status dots from #2882 were explained only by a detached gray sentence, and a fully mined folder looked identical to a never-mined one. The legend now sits inside the folders tree with real color swatches, uses short consequence-first labels ("Full re-scan needed" instead of "Mailbox identity changed"), keeps the technical explanation in per-state tooltips, adds a check marker for "Already mined" folders, and only renders once at least one folder has been mined. FR translations updated. 🤖 Generated with Codebuff Co-authored-by: Codebuff <noreply@codebuff.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.
Summary
Incremental passive mining with a clean design: enums instead of magic strings,
getImapBoxesreturns each folder's watermark + status, the backend derives the resume cursor from persisted config, and themining-sourcesedge function is the single writer for source config.What this changes
MiningRunMode,TaskStatus,SourceHealthState,FolderStatus,SourceBadgeare mirrored in the backend, edge functions and frontend. No'full'/'incremental'/"active"literals in code paths that write or compare domain values (legacy input decoding maps to the enum).getImapBoxesreturns per-folder status:{ name, key, total, attribs?, children?, watermark?, status, has_new_messages, latest_uid }, computed server-side by joining the live IMAP cursor with the persisted watermark. The frontend cursor/watermark state machine is gone.boxes+miningMode); the backend buildsresumeFrom = { folders: { <folder>: { uidvalidity, last_uid } } }from the source config and the emails-fetcher owns all IMAP logic (uidvalidity checks,[last_uid+1 .. uidNext-1], fallback, watermark emission).mining-sourcesis the single config writer: callers send params (mining_flags,folders,passive_mining; internalhealth/mining); the edge function merges in JS (unknown keys preserved,nullclears, per-folder cursors monotonic) and persists with supabase-js + aconfig_revisioncompare-and-swap.update_mining_source_config/jsonb_deep_mergeare removed.20260912010857_passive_mining_incremental_config.sql; the separate security migration is kept.Verification
mining-sourcesconfig 5/5,mining-completion4/4, enums).getImapBoxesreturnsnew_messages+ watermark; the backend-builtresumeFromfetches only the new UIDs;mining-completion→mining-sourcesadvancesmining.lastwith the revision CAS; idempotent re-trigger.32:33(2/6 fetched); no localStorage key is written.Notes
20260912010857_passive_mining_incremental_config.sql: addsconfig+config_revision, hasget_mining_source_credentials_for_userreturnconfig, and drops the superseded SQL merge helpers.