Get started

Schedulers

Background work that has to survive a closed browser tab lives here: reflection mining and The Feed, the collector's two always-server-side scheduled passes.

Maturity: Beta. Statements on this page were checked against QuoxCORE code on 2026-10-30.

Quox's rule for anything that needs to run on a timer is fixed: server-side, never a browser timer. A setInterval in React only runs while a tab is open; it stops the moment the tab closes, the laptop sleeps, or the user is on a different page. Every scheduled or background job in the platform has to live in a long-running server process instead, so it keeps running whether or not anyone has the dashboard open.

The collector runs several such schedulers, including reflection mining and The Feed. QuoxBrain, a separate service, runs the embedding backfill scheduler. The tasks service's team schedule trigger loop is covered at the end for completeness.

Reflection mining

Reflection mining replaced the last browser setInterval in the codebase (reflectionLoop.js) with a server-side cycle inside the collector. On each tick it:

  1. Lists idle conversations (AEE envelopes with no recent activity) since a per-org watermark.
  2. Extracts memories, decisions, and entities from each one and writes them to QuoxMemory.
  3. Detects patterns across recent envelopes: repeated failures, error-hour clustering, host-level error rates, activity spikes, and heartbeat silence.
  4. Generates a proactive alert for anything that crosses a threshold, deduplicated so the same underlying condition does not re-fire on every tick.

Reflection mining starts when the collector boots, with the first cycle 10 minutes later. It can be switched off per organisation with REFLECTION_MINING_ENABLED (default on). Mining of chat conversations is opt-in (REFLECTION_MINING_CHATS_ENABLED). The interval is configurable:

FlagPurposeDefault
REFLECTION_MINING_INTERVAL_MSTime between mining cycles.5 minutes
REFLECTION_MINING_ENABLEDPer-organisation switch for mining.on
REFLECTION_MINING_CHATS_ENABLEDOpt-in mining of chat conversations.off

A durable per-org watermark (not an in-memory set) means a restart never reprocesses conversations already mined, and a hard cap (maxOrgsPerCycle, default 25) round-robins across ticks on a multi-tenant deployment with more live orgs than that, rather than fanning out to all of them on every tick.

The Feed

The Feed is QuoxBrain's autopilot ingestion scheduler: it pulls from a registry of sources (AEE conversations, transcripts, folders, Obsidian vaults, fleet logs, memory, git history, RepoBrain, and several database adapters) and pushes documents into Qortex, so the second brain grows without someone manually connecting every source every time. Its scheduler shape deliberately mirrors QuoxDream's: a warmup delay, then a recurring interval, both .unref()'d so they never block a clean process exit.

Unlike reflection mining, The Feed ships dark. The environment variables below are defaults; per-organisation stored settings override them.

FlagPurposeDefault
QUOXFEED_ENABLEDDefault switch. No timers are created unless exactly 'true' or the organisation enables it in settings.false
QUOXFEED_ORGSComma-separated org IDs to ingest for. Empty means nothing runs even if enabled.empty
QUOXFEED_WARMUP_MSDelay before the first tick after boot.60 seconds
QUOXFEED_INTERVAL_MSInterval between ticks.30 minutes
QUOXFEED_MAX_ITEMSPer-source batch cap on each tick.50
QUOXFEED_OAUTH_SOURCES_ENABLEDRegisters the Notion and Google Drive connectors. Off by default so the source catalog cannot claim a connector before an OAuth app is provisioned.false

Per-(org, source_type) enable state lives in a feed_sources registry table, not an env var: a source ingests only when the organisation has an explicit, non-disabled feed source entry; an absent or unreadable entry means nothing is ingested. Every ingest attempt, success or failure, writes its outcome back to that row. Every source ingester runs inside its own try/catch, so one failing source or org never blocks any other on the same tick.

Embedding backfill

QuoxBrain's hybrid retrieval (keyword plus dense vector search) depends on chunks having embeddings. Without a scheduler, embedding coverage depends on manual runs. The embedding backfill scheduler (services/quoxbrain/lib/embeddingBackfillScheduler.js) closes that gap, mirroring the same warmup-then-interval shape as QuoxDream and The Feed, both .unref()'d so they never block a clean process exit. Each tick discovers the orgs with missing embeddings, capped per tick, and runs the backfill for each one: best-effort, so one org's failure never blocks another's turn on the same tick.

Ships dark, same as The Feed:

FlagPurposeDefault
QUOXBRAIN_EMBED_BACKFILL_ENABLEDDefault switch. No timers are created unless exactly 'true' or the setting is enabled.false
QUOXBRAIN_EMBED_BACKFILL_WARMUP_MSDelay before the first tick after boot.2 minutes
QUOXBRAIN_EMBED_BACKFILL_INTERVAL_MSInterval between ticks.15 minutes
QUOXBRAIN_EMBED_BACKFILL_MAX_ORGSDistinct orgs discovered and processed per tick.10
QUOXBRAIN_EMBED_BACKFILL_BATCH_CAPPer-org chunk batch cap on each tick, passed through to the underlying backfill call.200

The embed client (embeddingClient.js) now bounds every request with QUOXBRAIN_EMBED_TIMEOUT_MS (default 45 seconds), using AbortSignal.timeout(). Without a bound, a single hung request against the embedding provider could hold a batch open indefinitely, silently halting the whole backfill rather than failing that batch and moving on. A tick that discovers missing embeddings but embeds nothing that pass now logs a warning rather than being indistinguishable from "already caught up".

Coverage itself is now a visible number rather than something only inferrable from server logs or manual SQL: GET /brain/status returns an embeddings object (total, embedded, missing, percent), and the Qortex settings cockpit renders it, with a plain-English warning under 50 per cent that semantic search is falling back to keyword matching for most queries.

Team schedule poll

The tasks service's team-schedule trigger loop (services/tasks/lib/teamScheduler.js) reads SCHEDULER_POLL_MS for its poll interval (default 5 seconds), plus a live stored setting that can change it at runtime.

Maturity

SchedulerStatusNotes
Reflection miningStableServer-side, starts at collector boot; durable per-org watermark and alert dedup.
The FeedBetaDark by default (QUOXFEED_ENABLED=false); source registry and per-source ingesters are unit-tested.
OAuth connectors (Notion, Google Drive)BetaBuilt and unit-tested, gated off by QUOXFEED_OAUTH_SOURCES_ENABLED.
Embedding backfill (QuoxBrain)BetaDark by default (QUOXBRAIN_EMBED_BACKFILL_ENABLED=false); unit-tested, catches up an existing embedding backlog per org.
Team schedule trigger loop (tasks service)StableRuns server-side; polls every 5 seconds by default, tunable with SCHEDULER_POLL_MS or the stored setting.

Settings: the scheduler flags are registry settings in the platform flags panel, and the environment variables act as defaults that per-organisation stored settings override. Embedding coverage is visible in the Qortex settings cockpit via GET /brain/status.

  • QuoxDream: the consolidation pass The Feed's and the embedding backfill scheduler's shape was mirrored from, and one of the schedulers reading from the same Qortex store.
  • Memory System: the destination for reflection mining's extracted memories and alerts.
  • Agent Response Contract: a different kind of drift-catching guard, checking claims rather than config. </content>

Last verified 2026-10-30 against QuoxCORE origin/main.