Skip to main content

Proactive Architecture — Design Spec

Status: Design Approved Date: 2026-03-16 Branch: proactive-crons

Problem

GAIA today is passive. It activates when triggered — user message, webhook, cron — then goes dark. Between triggers it has no initiative. More critically, when GAIA takes actions (sends an email, creates a PR, submits a contract), it immediately forgets what it expected to happen next. There is no tracking of open loops. The result: GAIA cannot follow up on its own work, cannot connect a reply to the action that caused it, and cannot escalate when expected outcomes don’t arrive. Two things fix this:
  1. GaiaTask — a persistent, VFS-backed execution entity that tracks multi-step work across sessions, owns the workflows that serve it, and monitors the outcomes it expects
  2. Proactive Loop — a two-mode strategy layer that surveys everything GAIA knows about a user’s life, detects what needs attention, and acts — without being asked

Core Mental Model Shift

The current design is observational: GAIA surveys the world and notices things. The new design is intentional: GAIA tracks what it put in motion and monitors whether those intentions resolved.
“Rahul hasn’t replied in 3 days” is only knowable if GAIA registered — at the moment it sent the email — that it expected a reply. Without intent tracking, the proactive loop can only read inbox noise.
Every significant action GAIA takes creates an open loop. The system’s job is to track, monitor, and resolve those open loops.

Three-Layer Architecture

Each layer only knows about the one below it. The user sees none of this — they receive notifications only when something requires their attention or has been resolved.

Part 1: GaiaTask — The Execution Layer

What It Is

A persistent, stateful agent entity. Not user-facing. GAIA auto-creates a GaiaTask when it judges that a user request requires multi-step work that will unfold across time or channels. Scope is flexible:
  • Small: “Schedule meeting with Rahul” — a few back-and-forth emails, no sub-workflows needed
  • Large: “Manage my inbox” — long-running, owns multiple workflows, accumulates weeks of logs
The same structure handles both.

When GAIA Creates a GaiaTask

GAIA (not the user) judges whether a task is needed. Rule of thumb:
  • Creates a task: any action that expects a response, involves multiple steps, or needs to be monitored over time
  • Does not create a task: one-shot actions with no expected follow-up (add calendar event, web search, answer a question)
The executor agent has a create_gaia_task() tool. It calls it when it judges the action warrants tracking.

The VFS — Living Memory

Each task owns a directory in the VFS:
This is not a DB record — it’s the task’s working memory. When the task wakes up (triggered by an email reply, a cron, or the proactive loop), the agent reads progress.md first (summary), then drills into context.json and log.md as needed. Progressive disclosure: the agent doesn’t load the full history every time, only what it needs. The VFS directory persists after task completion — it becomes permanent memory of what was done. Not deleted, just archived. VFS Growth and Compaction: For long-running tasks (e.g., “Manage inbox” with triage running every 15 min), unbounded file accumulation is a real operational concern. Two mitigations:
  1. Workflow output rotation: workflows/ keeps the last 7 days of files inline. Older outputs are moved to workflows/archive/ and replaced with a single archive_summary_{week}.md. The agent reads the inline files by default; the archive is available on demand via read_task_vfs().
  2. log.md compaction: When log.md exceeds 500 lines, the oldest 80% is summarized into a log_summary_{date}.md and the main log.md is reset to the summary header + the most recent 20%. The full history is not lost — the summaries remain in the VFS — but the agent’s default read stays bounded.

Open Loop Tracking — The Core Innovation

Every significant action creates an expectation. Open loops are stored as a separate MongoDB collection (open_loops), not embedded in context.json. This enables efficient indexed queries across all users without full collection scans — the maintenance scan does { resolved: false, deadline: { $lt: now } } on a single indexed collection rather than unwinding nested arrays across gaia_tasks. context.json holds only a reference list of active loop IDs for context injection:
Open Loop schema by channel:
Each channel’s trigger handler implements find_matching_open_loops(channel, event_data) -> List[OpenLoop]. The matching logic per channel:
  • email: match sender AND (thread_id if present OR any email in thread)
  • slack: match slack_user_id AND channel_id AND ts > after_ts
  • github: match event_type AND resource_id
  • calendar: match event_id AND event_type AND optionally attendee_email
  • linear: match issue_id AND event_type AND optionally target_status
  • webhook: match source AND trigger_name AND all match_fields
Multi-task conflict resolution: When multiple active open loops match the same incoming signal (e.g., two tasks both watching for email from rahul@company.com):
  1. All matching loops are resolved simultaneously — the signal is not consumed by one task only
  2. The incoming signal is stored in each matching task’s inbox/ folder
  3. Each matched task wakes up independently with the signal in its context
  4. Tasks do not know about each other — each acts on the signal from its own perspective
  5. If this causes duplicate actions (e.g., both tasks try to reply), the dedup layer in the notification orchestrator prevents double-sending
Automatic open loop detection: When GAIA’s executor takes an action, it classifies the action type: GAIA registers these automatically — not because you asked it to track something, but because it knows what kind of action it just took.

Workflow Ownership

GaiaTask sits above Workflows. A task can:
  • Adopt an existing system workflow (“Manage inbox” adopts the inbox triage workflow)
  • Create new workflows for its specific needs
  • Enable / disable workflows based on task state
  • Trigger a workflow immediately outside its normal schedule
Workflow outputs flow into the task’s workflows/ VFS folder. The task agent reads these when it wakes up — it knows what its sub-workflows have done.

Lifecycle

Agent tools (Phase 0):
  • create_gaia_task(title, description, expires_in_days) — creates task + VFS
  • update_gaia_task(task_id, status, notes) — updates state, appends to log
  • complete_task(task_id, summary) — marks done, archives VFS
  • cancel_task(task_id, reason) — cancels task, archives VFS
  • list_active_tasks() — returns lightweight list for context injection
  • read_task_vfs(task_id, path) — progressive disclosure drill-in

Self-Scheduling

A task can schedule its own wakeup in two ways:
  • Time-based: “wake me in 3 days” → ARQ job scheduled
  • Conditional: “wake me when I receive email from rahul@company.com” → registered in open_loops, resolved when the watch condition fires
Both co-exist. A task can say: “wake me in 3 days if Rahul hasn’t replied, but also wake me immediately if he does reply.”

Part 2: Open Loop System — The Infrastructure of Intent

What It Does

The open loop system is the bridge between GaiaTask and incoming signals. It answers one question continuously: did the things we expected to happen, happen? Maintenance mode of the proactive loop handles deadline scanning — no LLM required. Channel trigger handlers handle real-time resolution.

Resolution Flow


Part 3: The Proactive Loop — The Strategy Layer

Two Modes

The proactive loop runs in two distinct modes. This is critical for cost and latency at scale. Maintenance Mode (frequent, no LLM, cheap):
  • Runs every 30-60 min per active user
  • Scans open loops: which are past deadline?
  • Scans tasks: which are stalled (no activity in N days)?
  • Executes deterministic actions: draft follow-ups, send reminders, escalate
  • No LLM call unless an action requires generating text
  • Cost: near zero per run
Synthesis Mode (less frequent, full LLM, expensive):
  • Runs 2-3x/day for active users, 1x/day for dormant
  • Full context snapshot assembled (pure code, no LLM cost)
  • LLM call with strategy prompt
  • Cross-domain pattern detection
  • New task creation
  • Strategic nudges, morning briefs, weekly reviews
  • Cost: 1 LLM call per run
Temporal chaining (e.g., “run 5 min after meeting ends”) is handled by the task’s own self-scheduling, not the synthesis loop. The synthesis loop is not precision-scheduled to the minute — it runs at day-level cadence. Tasks handle minute-level event chaining by self-scheduling ARQ jobs with precise timestamps.

Context Snapshot (Synthesis Mode)

Assembled in parallel, pure code. LLM sees:
Critical innovations:
  • Delta awareness: only shows what changed since last run — not everything from scratch
  • Cross-domain synthesis: calendar + email + tasks + goals + GitHub + Slack in one view
  • Task-aware: sees all active GaiaTasks and their open loop status
  • Memory-informed: memories from the memory engine injected, so it knows Priya’s role, preferences, etc.

What the Loop Can Do (max 3 actions per run)

Create a new GaiaTask: “Investor call is tomorrow — no prep task exists. Create one.” Escalate an existing task: “Schedule meeting with Rahul has an expired open loop. Inject escalation into task’s next wakeup.” Send a cross-domain notification: “You have a 4pm with Priya, she sent a Slack DM an hour ago, and her PR is waiting for your review.” — one notification, not three. Trigger a workflow immediately: “Sprint ends in 2 days and 3 issues are open. Run the Linear triage workflow now instead of waiting.” Send a synthesis brief: Morning: “Today: 3 meetings, Q1 report due tomorrow, Rahul still hasn’t replied.” Action budget enforcement: The budget (max 3 actions per synthesis run) is enforced at the tool level, not just in the prompt. A per-run action counter is stored in the proactive_runs record (actions_taken_this_run: int). The tool checks this counter on every call and returns an error if the budget is exhausted, forcing the agent to call schedule_next_run() instead. The counter is reset at the start of each run.

Self-Scheduling — The Heartbeat

The synthesis agent picks its own next wakeup cadence: Note: minute-precision event chaining (e.g., “check in 5 min after the meeting ends”) is handled by the task’s self-scheduling, not the synthesis loop. Three-layer guarantee the loop never dies:
  1. Agent is required to call schedule_next_run() — enforced in prompt
  2. If it forgets — post-run wrapper auto-schedules 24h fallback + logs warning
  3. Supervisor cron (hourly) — rescues any user whose next_run_at is >48h stale
Bounds: min 30 min (no tight loops), max 7 days (no silent death). Clamped silently.

User Activity Tiers

Return detection: “Return” is defined as any authenticated API call (chat message, app open, any endpoint hit). A last_seen_at timestamp on the user record is updated on each request. The proactive loop compares last_seen_at to activity tier thresholds. Return digest: When a dormant/inactive user returns → immediate synthesis run triggered (not the next scheduled one) with prompt: “User returned after N days. Surface the 3-5 most important things. Do not flood.” Queued notifications (see below) are collapsed into this digest rather than sent individually.

Anti-Spam

  • Action budget: max 3 actions per synthesis run. Enforced by tool-level counter in proactive_runs.actions_taken_this_run (see above).
  • No-op backoff: 5 consecutive no-action runs → synthesis interval doubles. Resets on any action.
  • Quiet hours: user-configurable (default 10pm-7am). Injected into context.
  • Dedup: notification hash checked before sending. Same notification not sent twice within 24h.
  • Notification hold: when dormant, notifications are written to a queued_notifications array in the proactive_runs record instead of being dispatched. On user return, the queued notifications are passed to the return digest synthesis run as context. The synthesis LLM decides which ones to surface (max 5) and which to discard as stale. The queue is capped at 50 entries — oldest are dropped when cap is exceeded.

Part 4: Context Injection — How the Agent Sees Tasks

Every agent call (chat and workflow) already gets user memories injected via get_memory_message(). GaiaTask adds a third concurrent fetch:
Progressive disclosure in practice:
  • Every call: lightweight list — task titles + status + any expired open loops
  • When a task is being executed: progress.md loaded into context
  • On demand: agent can call read_task_vfs(task_id, path) to drill into specific files
The agent in a chat session can see “oh, there’s an active task for scheduling with Rahul, and its open loop expired yesterday” — and proactively mention it to the user without being asked.

Part 5: Key Flows End-to-End

Flow 1: Meeting Scheduling

Flow 2: Cross-Domain Synthesis

Flow 3: Inbox Management Task


MongoDB Collections

New Collections

gaia_tasks
open_loops (separate collection, not embedded — enables efficient indexed queries)
proactive_runs (one document per user, upserted on each run — not a run history log)

Build Order

Phase 0 — GaiaTask Foundation

gaia_tasks collection. VFS directory creation at /users/{uid}/tasks/{task_id}/. progress.md/log.md/context.json structure. Six agent tools: create_gaia_task, update_gaia_task, complete_task, cancel_task, list_active_tasks, read_task_vfs. Context injection in get_memory_message(). Agent can track multi-step work from chat and cancel tasks on user request. No open loop watching yet.

Phase 1 — Open Loop Tracking

open_loops collection with indexes. Watch schema per channel. Auto-registration in executor tool calls (email send → register expectation). find_matching_open_loops() in Gmail trigger handler. Maintenance scan ARQ task (every 30 min) with expired loop scanning. Deterministic if_unresolved actions. Follow-up drafting. Full meeting scheduling end-to-end.

Phase 2 — Workflow Ownership

GaiaTask can create, adopt, enable, disable, trigger workflows. Workflow outputs flow to task workflows/ VFS folder with 7-day rotation. “Manage inbox” as first large-scale task owning multiple workflows.

Phase 3 — Proactive Loop (Maintenance Mode)

proactive_runs collection. ARQ sweep (every 1 min). execute_maintenance_run() — pure code, no LLM. Stalled task detection (uses task activity timestamps; workflow-staleness detection deferred to after Phase 2 delivers workflow-to-VFS output flow). Expired open loop escalation. Activity tiers. No-op backoff. Return detection via last_seen_at.

Phase 4 — Proactive Loop (Synthesis Mode)

Full context snapshot assembly. execute_synthesis_run() via call_agent_silent(). Self-scheduling with schedule_next_run(). Strategy prompt. Action budget enforced by tool-level counter. Cross-domain notifications. New task creation from loop. Post-run safety net (24h fallback). Supervisor health cron (hourly). Queued notification hold + return digest.

Phase 5 — Rich Context + Intelligence

Delta diffing (last_context_snapshot). find_matching_open_loops() extended to Slack, GitHub, Calendar, Linear, webhook channels. VFS log compaction (500-line threshold). Memory-informed decisions. Goal awareness. Quiet hours.

Phase 6 — Learning + Preferences

User proactive preferences (enabled, quiet hours, max actions, preferred channels). Dismiss/act ratio tracking. Per-user escalation tuning. Weekly review and goal check-ins.

What This Unlocks

Things that genuinely cannot exist without this system:
  • Follow-up that never drops: Every email sent, every PR opened, every contract sent is tracked. GAIA follows up without being asked.
  • Cross-domain synthesis: “You have a meeting with Priya in 2h, she messaged on Slack, and her PR needs your review” — no single integration sees all three.
  • Temporal chaining: Tasks self-schedule for 5 min after a meeting ends to capture action items. Synthesis loop handles day-level strategy; tasks handle minute-level event chaining.
  • Goal awareness: “Ship v2.0: next node is Deploy staging, no progress in 5 days” — goals become live-tracked, not just aspirational.
  • Inbox as a managed task: Not just triage-and-forget but a living task that tracks reply rates, escalates aging threads, runs weekly reviews.
  • Return digest: Came back after 4 days? One synthesis notification with the 5 things that actually matter, not 50 queued pings.