Skip to content

Agent Pipeline ​

The agent pipeline manages all background processing in Cortexa through a SQLite-backed job queue and 2 async worker threads with priority scheduling. It handles embedding generation, LLM summarization, the Deep Think thesis pipeline, research synthesis, knowledge graph linking, and audio briefing generation. Lightweight jobs (embed, auto_link) run at higher priority before heavy LLM jobs (reflection, cross-synthesis).

Architecture ​

                    ┌─────────────────┐
                    │   Tauri Commands │
                    │   HTTP API       │
                    │   MCP Server     │
                    └────────┬────────┘
                             │ enqueue(Job)
                             v
                    ┌─────────────────┐
                    │    Job Queue     │
                    │   (SQLite DB)    │
                    └────────┬────────┘
                             │ claim_next()
               ┌─────────────┴─────────────┐
               v                           v
          ┌─────────┐                ┌─────────┐
          │ Worker 0 │                │ Worker 1 │
          └────┬────┘                └────┬────┘
               │                           │
               v                           v
          handlers::execute(job)
               │
               ├── embed()
               ├── summarize()
               ├── auto_link()
               ├── expand()
               ├── counterpoint()
               ├── synthesize()
               ├── research()
               ├── generate_briefing()
               ├── generate_item_briefing()
               ├── orchestrate_item()
               ├── reflection_pass()
               └── cross_synthesize()

Job Queue ​

The job queue is implemented in src-tauri/src/agent/queue.rs. It uses a jobs table in the same SQLite database as items, protected by a std::sync::Mutex<Connection>.

Job Types ​

rust
pub enum Job {
    Embed { item_id: String },
    Summarize { item_id: String },
    AutoLink { item_id: String },
    Expand { item_id: String },
    Counterpoint { item_id: String },
    Synthesize { item_id: String },
    Research { item_id: String },
    GenerateBriefing { briefing_id: String },
    GenerateItemBriefing { item_id: String },
    Orchestrate { item_id: String },
    ReflectionPass,
    CrossSynthesize { peer_item_id: String },
}
JobTriggerLLM RequiredDescription
EmbedEvery new/updated itemEmbedding providerGenerate embedding vector, store in items.embedding, insert into HNSW index
SummarizeAuto-summarize or manual toggleChat providerGenerate one-paragraph summary, store in items.summary
AutoLinkAfter embedding completesNone (uses HNSW)Find top-K nearest neighbors, create/update links rows
ExpandDeep Think pipeline step 1Chat providerDecompose idea into thesis, sub-questions, expansion, thinking lenses
CounterpointDeep Think pipeline step 2Chat providerCritique, find contradictions with prior thoughts, devil's advocate
SynthesizeDeep Think pipeline step 3Chat providerProduce structured JSON brief with readiness gates
ResearchManual research toggleChat provider + DuckDuckGoPull SERP snippets, synthesize with LLM, store as linked research item
GenerateBriefingScheduled or manualChat provider + TTSGenerate spoken script from N items, optionally produce audio via OpenAI TTS or Piper (local)
GenerateItemBriefingPer-item Brief button or auto-briefChat provider + TTSGenerate spoken briefing for a single thought
OrchestrateDeep Think (orchestrator mode)Chat providerLLM-powered orchestrator plans and executes registry tools across the specialized agents with parallel scheduling
ReflectionPassScheduled (configurable interval)Chat providerAnalyze recent thoughts for themes, contradictions, patterns, gaps. Runs confidence decay. Stores output as type:reflection item
CrossSynthesizeHive import of item with thesisChat + EmbeddingSemantic search for local items with theses on similar topics, LLM synthesis of cross-user perspectives. Stores output as type:cross-synthesis item

Job Lifecycle ​

enqueue() ──> PENDING ──> claim_next() ──> RUNNING
                 ^                            │
                 │                    ┌───────┴───────┐
                 │                    │               │
                 │               success           failure
                 │                    │               │
                 │               complete()       fail()
                 │                    │               │
                 │                    v         attempts < max?
                 │                  DONE         │         │
                 │                              yes        no
                 │                               │         │
                 └──── (backoff) ────────────────┘         v
                                                         DEAD

Enqueue ​

Jobs are enqueued with a UUID, the job type, a JSON-serialized payload, and status = 'pending':

rust
pub fn enqueue(&self, job: Job) -> Result<String> {
    self.enqueue_at(job, Utc::now(), DEFAULT_MAX_ATTEMPTS)  // max_attempts = 3
}

After insertion, the queue calls notify.notify_one() to wake an idle worker.

Claim ​

Workers call claim_next() which atomically selects the highest-priority pending job whose next_run_at is in the past and updates it to running. Priority levels: embed=0, auto_link=1, summarize=2, expand/counterpoint/synthesize=3, research/orchestrate=4, briefing=5, reflection/cross_synthesize=6.

sql
SELECT ... FROM jobs WHERE status = 'pending' AND next_run_at <= ?
ORDER BY priority ASC, next_run_at ASC LIMIT 1

UPDATE jobs SET status = 'running', attempts = attempts + 1 WHERE id = ? AND status = 'pending'

The two-step select + conditional update provides atomic claiming without database-level locks. If another worker claims the same job between the SELECT and UPDATE, the UPDATE changes zero rows and the worker retries.

Retry with Exponential Backoff ​

When a job fails, fail() checks whether the attempt count has reached max_attempts:

  • Under the limit: Status returns to pending with a delayed next_run_at:

    rust
    fn backoff_secs(attempts: u32) -> u64 {
        let base = 1u64 << attempts.min(10); // 2^attempts
        base.min(60)
    }
    AttemptBackoff
    12 seconds
    24 seconds
    38 seconds
    416 seconds
    532 seconds
    6+60 seconds (cap)
  • At the limit: Status changes to dead. The worker emits a job-dead event.

Dead Letter Handling ​

Dead jobs remain in the database for inspection. The UI shows them in the job queue trace panel. Two recovery paths exist:

  • Manual retry: retry(id) resets a dead job to pending with attempts = 0 and next_run_at = now, making it immediately claimable.
  • Purge: purge_dead() deletes all dead jobs. purge_done() cleans up completed jobs.

Crash Recovery ​

On startup, the queue resets any running jobs to pending:

sql
UPDATE jobs SET status = 'pending' WHERE status = 'running'

This handles the case where the app crashed while a job was executing. The job will be re-attempted on next startup.

Worker Pool ​

Two workers are spawned in src-tauri/src/agent/worker.rs via spawn_workers(handle, 2). Each worker runs an async loop:

rust
async fn worker_loop(handle: AppHandle, notify: Arc<Notify>, worker_id: usize) {
    loop {
        // Pause while vault is locked
        if state.is_locked() { sleep(2s); continue; }

        match queue.claim_next() {
            Ok(Some(rec)) => {
                // Execute the job handler
                handlers::execute(&handle, rec.job).await;
                // Report success or failure
            }
            Ok(None) => {
                // No work -- wait for notify or 2-second timeout
                timeout(2s, notify.notified()).await;
            }
            Err(e) => { sleep(2s); }
        }
    }
}

Key behaviors:

  • Vault lock: Workers pause while the vault is locked. Jobs accumulate in the queue and resume processing after unlock.
  • Notify wake: When a job is enqueued, notify.notify_one() wakes an idle worker immediately rather than waiting for the 2-second poll.
  • Tracing: Every job start, completion, and failure is recorded in the trace system and emitted as Tauri events (job-started, job-completed, job-dead, job-retry-scheduled).

Deep Think Pipeline ​

The three-stage thesis pipeline is the most complex job chain. Each stage is a separate job, chained automatically:

Stage 1: Expand ​

The expand() handler:

  1. Reads the item content (decrypts blob, includes text attachments inline, includes image attachments as base64).
  2. Sets thesis status to expanding.
  3. Sends the content to the chat LLM with the EXPAND system prompt, which instructs the LLM to: clarify as a concrete thesis, generate sub-questions, explore implications, and apply thinking lenses (pre-mortem, first principles, inversion, steelman).
  4. Stores the expansion output in theses.expansion.
  5. Enqueues a Counterpoint job.

Stage 2: Counterpoint ​

The counterpoint() handler:

  1. Reads the item content and the expansion from Stage 1.
  2. Searches the HNSW index for related prior thoughts.
  3. Sets thesis status to critiquing.
  4. Sends the content + expansion + related thoughts to the LLM with the COUNTERPOINT system prompt, which instructs the LLM to: identify weaknesses, flag contradictions with prior thoughts, cite prior art, and write a devil's advocate argument.
  5. Stores the counterpoints in theses.counterpoints.
  6. Enqueues a Synthesize job.

Stage 3: Synthesize ​

The synthesize() handler:

  1. Reads the item content, expansion, and counterpoints.
  2. Sets thesis status to synthesizing.
  3. Sends everything to the LLM with the SYNTHESIZE system prompt, which instructs the LLM to produce a strict JSON object matching the ThesisOutput schema.
  4. Parses the JSON response (with fallback handling for fenced code blocks, preamble text, and malformed JSON).
  5. Stores the structured thesis in theses.thesis.
  6. Sets status to done.

Thesis Status Tracking ​

The ThesisStatus enum tracks pipeline progress:

pending → expanding → critiquing → synthesizing → done
                                                    │
                                      (any failure) → failed

If any stage's job reaches dead status (max retries exhausted), the worker marks the thesis as failed with the error message. The UI shows the pipeline state in real time.

Structured Output ​

The final ThesisOutput contains:

rust
pub struct ThesisOutput {
    pub core: String,                    // One-sentence thesis
    pub problem_statement: Option<String>,
    pub sub_questions: Vec<String>,
    pub supporting_arguments: Vec<String>,
    pub risks: Vec<String>,
    pub prior_art: Vec<String>,
    pub open_questions: Vec<String>,
    pub next_steps: Vec<String>,
    pub confidence: Option<String>,      // speculation | partially-vetted | well-researched
    pub contradictions: Vec<String>,
    pub lenses: Vec<LensResult>,         // pre-mortem, first-principles, steelman
    pub gates: Vec<Gate>,                // 8 readiness gates with open/closed status
}

The 8 readiness gates are: problem-stated, success-criterion, scope-bounded, approach-selected, open-questions-resolved, prior-art-reviewed, risks-acknowledged, first-milestone-identified.

Research Jobs ​

The research() handler:

  1. Reads the item content.
  2. Extracts a focused search query via the LLM.
  3. Sends the query to DuckDuckGo and parses SERP HTML for snippets.
  4. Passes the snippets to the LLM with the RESEARCH system prompt for synthesis.
  5. Creates a new item with the research output.
  6. Links the research item to the source item in the links table.

Briefing Jobs ​

The generate_briefing() handler:

  1. Reads the briefing record to get scope and source item IDs.
  2. Sets status to scripting.
  3. Loads content from the source items.
  4. Sends to the LLM with the BRIEFING system prompt to generate a 2-4 minute spoken script.
  5. Stores the encrypted script.
  6. If TTS is enabled, sets status to voicing and calls the OpenAI TTS API.
  7. Encrypts and stores the audio file.
  8. Sets status to done.

Event Emission ​

The pipeline emits Tauri events at each stage so the frontend can update in real time:

EventPayloadWhen
job-startedJob IDWorker claims a job
job-completedJob IDJob finishes successfully
job-deadJob IDJob exhausts all retries
job-retry-scheduledJob IDJob failed but will retry
thesis-updatedItem ID + statusThesis pipeline state changes
summary-setItem ID + summary textSummary is stored
embedding-setItem IDEmbedding is stored
embedding-migration-progressProgress infoDuring re-embed all
reflection-startedReflection IDReflection pass begins
reflection-completedNew item IDReflection pass finishes, item created
cross-synthesis-completedNew item IDCross-synthesis finishes, item created
confidence-updatedCount of updated itemsConfidence decay pass completes

Background Reflection ​

The reflection_pass() handler runs on a configurable schedule (default every 6 hours). It implements cross-item pattern detection — analyzing recent thoughts collectively rather than individually.

Algorithm ​

  1. Load settings, check reflection.enabled.
  2. Query recent N items since last_reflection_at (content + summary + thesis core).
  3. Require at least 3 items (skip if fewer — not enough data for patterns).
  4. Build a combined prompt with all items numbered by ID.
  5. Call LLM with REFLECTION_SYSTEM prompt requesting structured JSON output.
  6. Parse output for: themes, contradictions, patterns, gaps, connections, narrative.
  7. Store the reflection as a new type:reflection item.
  8. Record in the reflections table for tracking.
  9. If confidence is enabled, run recalculate_confidence() — decay all scores and tag stale items.
  10. Update settings.reflection.last_reflection_at.

Scheduling ​

The reflection schedule is checked in tick_schedules() alongside the daily briefing and weekly review. It fires when Utc::now() - last_reflection_at > interval_hours.

Cross-User Thesis Synthesis ​

The cross_synthesize() handler is triggered when a Hive peer shares a thought that includes thesis data. It finds related local thoughts and produces a cross-perspective synthesis.

Algorithm ​

  1. Check reflection.cross_synthesis is enabled.
  2. Load the peer item's content and thesis.
  3. Embed the peer content and search the HNSW index for similar local items.
  4. Find the best local item that also has a thesis (skip if none match above threshold).
  5. Call LLM with CROSS_SYNTHESIS_SYSTEM prompt providing both perspectives.
  6. Parse output for: agreements, disagreements, novel_combinations, synthesis.
  7. Store as a new type:cross-synthesis item with bidirectional links to both sources.

Hook Point ​

The cross-synthesis job is enqueued in hive_import_feed_item() after the embed job, only when the imported item's ThoughtMeta contains thesis_json.

Memory Confidence & Decay ​

Confidence scoring adds a numerical quality signal to every thought that decays over time, surfacing stale knowledge for review.

Initial Assignment ​

On ingest, compute_initial_confidence() assigns a score based on source:

  • Manual capture: 0.8
  • MCP / API / CLI: 0.7
  • Hive shared: 0.6
  • Telegram: 0.5

Decay Formula ​

decayed_score = current_score * (decay_rate ^ weeks_since_validated)

With the default rate of 0.95/week:

  • After 5 weeks: ~77% of original
  • After 10 weeks: ~60% of original
  • After 23 weeks: items approach the stale threshold

Stale Tagging ​

Items below confidence.stale_threshold (default 0.3) are tagged confidence:stale. Items above the threshold have the tag removed. This runs during each reflection pass.

Validation Reset ​

User interactions that demonstrate engagement with an item reset last_validated_at to now, effectively resetting the decay clock:

  • Pinning an item
  • Any future edit/review interaction

Testing ​

The job queue has unit tests covering:

  • Enqueue then claim returns the correct job
  • Fail with retries schedules backoff then reaches dead
  • Complete marks done and clears last_error
  • Retry dead job makes it claimable again
  • Purge dead removes only dead jobs
  • Backoff grows exponentially with attempt count (capped at 60s)

Built with Rust, Tauri, and a lot of captured thoughts.