Skip to content

feat(mining): incremental passive mining via per-source UID watermark - #2882

Merged
malek10xdev merged 19 commits into
mainfrom
feat/incremental-passive-mining
Sep 14, 2026
Merged

feat(mining): incremental passive mining via per-source UID watermark#2882
malek10xdev merged 19 commits into
mainfrom
feat/incremental-passive-mining

Conversation

@malek10xdev

@malek10xdev malek10xdev commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

Incremental passive mining with a clean design: enums instead of magic strings, getImapBoxes returns each folder's watermark + status, the backend derives the resume cursor from persisted config, and the mining-sources edge function is the single writer for source config.

What this changes

  • Enums are the only vocabulary: MiningRunMode, TaskStatus, SourceHealthState, FolderStatus, SourceBadge are 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).
  • getImapBoxes returns 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.
  • Resume is resolved server-side: the mine request sends intent only (boxes + miningMode); the backend builds resumeFrom = { 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).
  • Run mode is transient: when selected pre-mined folders have new messages, a dialog asks Continue from last position vs Re-scan everything; the answer is sent as a request param. Nothing is persisted (no localStorage).
  • mining-sources is the single config writer: callers send params (mining_flags, folders, passive_mining; internal health/mining); the edge function merges in JS (unknown keys preserved, null clears, per-folder cursors monotonic) and persists with supabase-js + a config_revision compare-and-swap. update_mining_source_config / jsonb_deep_merge are removed.
  • One config migration: the intermediate RPC/merge migrations are squashed into 20260912010857_passive_mining_incremental_config.sql; the separate security migration is kept.

Verification

  • Unit: backend 621/621, emails-fetcher 59/59, frontend focused 22/22, Deno (mining-sources config 5/5, mining-completion 4/4, enums).
  • API E2E (local Supabase + Greenmail + Enron): getImapBoxes returns new_messages + watermark; the backend-built resumeFrom fetches only the new UIDs; mining-completionmining-sources advances mining.last with the revision CAS; idempotent re-trigger.
  • UI E2E (agent-browser): the Continue/Re-scan dialog appears only when pre-mined folders have new messages; "Continue" resumed 32:33 (2/6 fetched); no localStorage key is written.
  • CI: backend, frontend, CodeQL, CodeSee, coverage, Snyk and DeepSource (JavaScript/Docker/SQL/Secrets/Test coverage) all green.

Notes

  • Migration 20260912010857_passive_mining_incremental_config.sql: adds config + config_revision, has get_mining_source_credentials_for_user return config, and drops the superseded SQL merge helpers.
  • Deploy order: migration → emails-fetcher → backend → edge functions → frontend.

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).
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

Coverage Report

Passed

Commit: 0c7baf0

Summary

Name Stmts Branch Funcs Lines
🟡 Total 46.4% 44.3% 52.5% 46.8%
All files
Name Stmts Branch Funcs Lines
🔴 backend/src/controllers/contacts.controller.ts 0% 0% 0% 0%
🟢 backend/src/controllers/imap.controller.ts 81.3% 77.3% 100% 80.9%
🔴 backend/src/controllers/mining.controller.ts 0% 0% 0% 0%
🟡 backend/src/db/completion.ts 44.4% 0% 0% 44.4%
🟢 backend/src/db/types.ts 100% 100% 100% 100%
🟡 backend/src/db/pg/PgSmtpSenders.ts 45.8% 28.9% 55.6% 45.8%
🔴 backend/src/db/supabase/MiningSourceService.ts 11.4% 0% 12.5% 11.6%
🟡 backend/src/services/email-fetching/index.ts 51.6% 40% 60% 51.6%
🔴 backend/src/services/imap/ImapBoxesFetcher.ts 0% 0% 0% 0%
🟢 backend/src/services/tasks-manager-v2/Pipeline.ts 82.5% 80% 83.3% 85.1%
🔴 backend/src/services/tasks-manager-v2/factories.ts 0% 0% 0% 0%
🟢 backend/src/services/tasks-manager-v2/tasks/FetchTask.ts 100% 89.5% 100% 100%
🟢 backend/src/utils/helpers/imapTreeHelpers.ts 91.7% 85.4% 83.3% 91.5%
🟢 backend/src/validators/mining.schema.ts 100% 83.3% 100% 100%
No coverage changes
Name Stmts Branch Funcs Lines
🔴 backend/src/controllers/contacts.controller.ts 0% 0% 0% 0%
🟢 backend/src/controllers/imap.controller.ts 81.3% 77.3% 100% 80.9%
🔴 backend/src/controllers/mining.controller.ts 0% 0% 0% 0%
🟡 backend/src/db/completion.ts 44.4% 0% 0% 44.4%
🟢 backend/src/db/types.ts 100% 100% 100% 100%
🟡 backend/src/db/pg/PgSmtpSenders.ts 45.8% 28.9% 55.6% 45.8%
🔴 backend/src/db/supabase/MiningSourceService.ts 11.4% 0% 12.5% 11.6%
🟡 backend/src/services/email-fetching/index.ts 51.6% 40% 60% 51.6%
🔴 backend/src/services/imap/ImapBoxesFetcher.ts 0% 0% 0% 0%
🟢 backend/src/services/tasks-manager-v2/Pipeline.ts 82.5% 80% 83.3% 85.1%
🔴 backend/src/services/tasks-manager-v2/factories.ts 0% 0% 0% 0%
🟢 backend/src/services/tasks-manager-v2/tasks/FetchTask.ts 100% 89.5% 100% 100%
🟢 backend/src/utils/helpers/imapTreeHelpers.ts 91.7% 85.4% 83.3% 91.5%
🟢 backend/src/validators/mining.schema.ts 100% 83.3% 100% 100%

Generated by Test Coverage Reporter for commit 0c7baf0

malek10xdev and others added 6 commits September 9, 2026 23:50
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.
…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
malek10xdev merged commit 846494e into main Sep 14, 2026
15 checks passed
@malek10xdev
malek10xdev deleted the feat/incremental-passive-mining branch September 14, 2026 00:47
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>
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.

1 participant