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:- 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
- 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
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
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)
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: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:
-
Workflow output rotation:
workflows/keeps the last 7 days of files inline. Older outputs are moved toworkflows/archive/and replaced with a singlearchive_summary_{week}.md. The agent reads the inline files by default; the archive is available on demand viaread_task_vfs(). -
log.md compaction: When
log.mdexceeds 500 lines, the oldest 80% is summarized into alog_summary_{date}.mdand the mainlog.mdis 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:
find_matching_open_loops(channel, event_data) -> List[OpenLoop]. The matching logic per channel:
- email: match
senderAND (thread_idif present OR any email in thread) - slack: match
slack_user_idANDchannel_idANDts > after_ts - github: match
event_typeANDresource_id - calendar: match
event_idANDevent_typeAND optionallyattendee_email - linear: match
issue_idANDevent_typeAND optionallytarget_status - webhook: match
sourceANDtrigger_nameAND allmatch_fields
rahul@company.com):
- All matching loops are resolved simultaneously — the signal is not consumed by one task only
- The incoming signal is stored in each matching task’s
inbox/folder - Each matched task wakes up independently with the signal in its context
- Tasks do not know about each other — each acts on the signal from its own perspective
- If this causes duplicate actions (e.g., both tasks try to reply), the dedup layer in the notification orchestrator prevents double-sending
| Action | Auto-registered expectation | Default deadline |
|---|---|---|
| Send email | Reply from recipient | 3 days |
| Create calendar invite | Acceptance from invitees | 24 hours |
| Send contract / document | Signed return or acknowledgment | 7 days |
| Open GitHub PR | Review from team | 2 days |
| Send Slack DM | Response from recipient | 4 hours |
| Create todo for user | Completion or acknowledgment | 2 days |
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
workflows/ VFS folder. The task agent reads these when it wakes up — it knows what its sub-workflows have done.
Lifecycle
create_gaia_task(title, description, expires_in_days)— creates task + VFSupdate_gaia_task(task_id, status, notes)— updates state, appends to logcomplete_task(task_id, summary)— marks done, archives VFScancel_task(task_id, reason)— cancels task, archives VFSlist_active_tasks()— returns lightweight list for context injectionread_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
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
- 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
Context Snapshot (Synthesis Mode)
Assembled in parallel, pure code. LLM sees:- 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: Mem0 memories 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 theproactive_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:| What it sees | Next synthesis run | Why |
|---|---|---|
| Investor call tomorrow 9am | Tonight 10pm | Prepare brief overnight |
| Expired open loop, Rahul 3 days | In 4h | Check if follow-up was sent, escalate if not |
| Nothing actionable, quiet week | Day after tomorrow | Routine check-in |
| User hasn’t been active in 3 days | In 3 days | Dormant — lower cadence |
- Agent is required to call
schedule_next_run()— enforced in prompt - If it forgets — post-run wrapper auto-schedules 24h fallback + logs warning
- Supervisor cron (hourly) — rescues any user whose
next_run_atis >48h stale
User Activity Tiers
| Tier | Condition | Synthesis cadence | Maintenance cadence |
|---|---|---|---|
| Active | Seen < 24h | 2-3x/day | Every 30 min |
| Dormant | Seen 1-7 days | 1x/day | Every 2h |
| Inactive | Seen 7-30 days | 1x/week | 1x/day |
| Churned | Seen > 30 days | Paused | Paused |
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_notificationsarray in theproactive_runsrecord 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 viaget_memory_message(). GaiaTask adds a third concurrent fetch:
- Every call: lightweight list — task titles + status + any expired open loops
- When a task is being executed:
progress.mdloaded into context - On demand: agent can call
read_task_vfs(task_id, path)to drill into specific files
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 taskworkflows/ 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.

