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:
- Lists idle conversations (AEE envelopes with no recent activity) since a per-org watermark.
- Extracts memories, decisions, and entities from each one and writes them to QuoxMemory.
- Detects patterns across recent envelopes: repeated failures, error-hour clustering, host-level error rates, activity spikes, and heartbeat silence.
- 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:
| Flag | Purpose | Default |
|---|---|---|
REFLECTION_MINING_INTERVAL_MS | Time between mining cycles. | 5 minutes |
REFLECTION_MINING_ENABLED | Per-organisation switch for mining. | on |
REFLECTION_MINING_CHATS_ENABLED | Opt-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.
| Flag | Purpose | Default |
|---|---|---|
QUOXFEED_ENABLED | Default switch. No timers are created unless exactly 'true' or the organisation enables it in settings. | false |
QUOXFEED_ORGS | Comma-separated org IDs to ingest for. Empty means nothing runs even if enabled. | empty |
QUOXFEED_WARMUP_MS | Delay before the first tick after boot. | 60 seconds |
QUOXFEED_INTERVAL_MS | Interval between ticks. | 30 minutes |
QUOXFEED_MAX_ITEMS | Per-source batch cap on each tick. | 50 |
QUOXFEED_OAUTH_SOURCES_ENABLED | Registers 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:
| Flag | Purpose | Default |
|---|---|---|
QUOXBRAIN_EMBED_BACKFILL_ENABLED | Default switch. No timers are created unless exactly 'true' or the setting is enabled. | false |
QUOXBRAIN_EMBED_BACKFILL_WARMUP_MS | Delay before the first tick after boot. | 2 minutes |
QUOXBRAIN_EMBED_BACKFILL_INTERVAL_MS | Interval between ticks. | 15 minutes |
QUOXBRAIN_EMBED_BACKFILL_MAX_ORGS | Distinct orgs discovered and processed per tick. | 10 |
QUOXBRAIN_EMBED_BACKFILL_BATCH_CAP | Per-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
| Scheduler | Status | Notes |
|---|---|---|
| Reflection mining | Stable | Server-side, starts at collector boot; durable per-org watermark and alert dedup. |
| The Feed | Beta | Dark by default (QUOXFEED_ENABLED=false); source registry and per-source ingesters are unit-tested. |
| OAuth connectors (Notion, Google Drive) | Beta | Built and unit-tested, gated off by QUOXFEED_OAUTH_SOURCES_ENABLED. |
| Embedding backfill (QuoxBrain) | Beta | Dark by default (QUOXBRAIN_EMBED_BACKFILL_ENABLED=false); unit-tested, catches up an existing embedding backlog per org. |
| Team schedule trigger loop (tasks service) | Stable | Runs 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.
Related
- 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.