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

┌─────────────────────────────────────────────────────┐
│              STRATEGY LAYER                         │
│              Proactive Loop                         │
│  Surveys everything. Spots patterns. Creates tasks. │
│  Escalates stalled work. Self-schedules next run.   │
└──────────────────┬──────────────────────────────────┘
                   │ creates / escalates / reads

┌─────────────────────────────────────────────────────┐
│              EXECUTION LAYER                        │
│              GaiaTasks                              │
│  Does the work. Owns workflows. Writes to VFS.      │
│  Tracks open loops. Acts on triggers.               │
│  Self-schedules for specific events.                │
└──────────────────┬──────────────────────────────────┘
                   │ owns / creates / runs

┌─────────────────────────────────────────────────────┐
│              AUTOMATION LAYER                       │
│              Workflows                              │
│  Single-purpose, scheduled/event-driven routines.  │
│  Inbox triage, meeting prep, reply drafting, etc.   │
└─────────────────────────────────────────────────────┘
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:
/users/{user_id}/tasks/{task_id}/
├── progress.md       ← what's been done, what's next (read first on every wakeup)
├── log.md            ← append-only chronological history of every action and event
├── context.json      ← structured state: status, workflow IDs, conversation ID
├── inbox/            ← incoming signals: email replies, slack messages, webhook data
└── workflows/        ← outputs from sub-workflows that ran for this task
    └── archive/      ← compacted older workflow outputs (see VFS Growth below)
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:
{
  "task_id": "task_abc",
  "title": "Schedule meeting with Rahul",
  "status": "waiting",
  "created_at": "2026-03-13T10:00:00Z",
  "active_loop_ids": ["loop_1"],
  "owned_workflow_ids": [],
  "primary_conversation_id": "conv_123"
}
Open Loop schema by channel:
// Email
{
  "id": "loop_1",
  "task_id": "task_abc",
  "user_id": "user_xyz",
  "description": "Reply from rahul@company.com re: meeting availability",
  "channel": "email",
  "watch": {
    "sender": "rahul@company.com",
    "thread_id": "gmail_thread_xyz"
  },
  "deadline": "2026-03-16T10:00:00Z",
  "if_unresolved": "draft_followup_email",
  "resolved": false,
  "resolved_at": null,
  "resolved_by": null
}

// Slack
{
  "channel": "slack",
  "watch": {
    "slack_user_id": "U012AB3CD",
    "channel_id": "D987ZY654",   // DM channel ID
    "after_ts": "1710000000.000" // only messages after this timestamp
  }
}

// GitHub
{
  "channel": "github",
  "watch": {
    "event_type": "pull_request_review",   // or "pull_request_merged", "issue_closed"
    "repo": "org/repo",
    "resource_id": "pr_412"               // PR number or issue number
  }
}

// Calendar
{
  "channel": "calendar",
  "watch": {
    "event_type": "invite_accepted",      // or "event_started", "event_updated"
    "event_id": "cal_event_abc",
    "attendee_email": "priya@company.com" // optional: specific attendee
  }
}

// Linear
{
  "channel": "linear",
  "watch": {
    "event_type": "issue_status_changed",
    "issue_id": "LIN-412",
    "target_status": "Done"              // optional: only resolve on specific status
  }
}

// Generic webhook
{
  "channel": "webhook",
  "watch": {
    "source": "composio",               // which integration
    "trigger_name": "contract_signed",  // Composio trigger name
    "match_fields": {                   // key-value pairs that must match in payload
      "document_id": "doc_abc123"
    }
  }
}
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:
ActionAuto-registered expectationDefault deadline
Send emailReply from recipient3 days
Create calendar inviteAcceptance from invitees24 hours
Send contract / documentSigned return or acknowledgment7 days
Open GitHub PRReview from team2 days
Send Slack DMResponse from recipient4 hours
Create todo for userCompletion or acknowledgment2 days
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

1. CREATION
   Agent calls create_gaia_task(title, description, expires_in_days=N)
   → gaia_tasks record created
   → VFS directory created at /users/{uid}/tasks/{task_id}/
   → progress.md, log.md, context.json initialized
   → Workflows adopted or created if needed
   → open loops registered in open_loops collection

2. ACTIVE (waiting / acting)
   Incoming signal (email reply, cron, proactive loop escalation)
   → Task conversation thread resumes (LangGraph checkpointer)
   → Agent reads progress.md → context.json → acts
   → Appends to log.md, updates progress.md and context.json
   → Notifies user only if significant
   → Calls schedule_next_wakeup() or registers conditional wakeup

3. COMPLETION
   Agent calls complete_task()
   → All owned workflows disabled
   → All active open loops closed (resolved=true, resolved_by="task_completed")
   → VFS directory renamed to /users/{uid}/tasks/archive/{task_id}/
   → gaia_tasks record status → "completed"
   → User notified of resolution
   → Primary conversation thread resumed with outcome if still valid
     (fallback: if conversation not found in checkpointer, send notification instead)

4. CANCELLATION
   User says "stop tracking X" or "forget about the meeting with Rahul"
   → Executor calls cancel_task(task_id)
   → All owned workflows disabled
   → All active open loops closed (resolved_by="cancelled")
   → VFS archived (not deleted)
   → gaia_tasks status → "cancelled"
   → No notification sent (user initiated this)

5. EXPIRATION
   expires_at TTL reached (checked by maintenance scan)
   → If task still has unresolved open loops:
     → Set gaia_tasks.status → "escalating" (prevents re-escalation on next scan cycle)
     → Enqueue synthesis run: "Task expired with unresolved loops — decide: extend, notify, cancel"
     → Loop decides: extend deadline, notify user, or auto-cancel
     → On loop completion: status transitions out of "escalating"
   → If no unresolved open loops (task is just stale):
     → Auto-cancel with log entry
     → User notified: "Task 'X' expired after N days with no activity"
   → VFS archived

6. ESCALATION (from proactive loop)
   Loop detects stalled task or expired open loop deadline
   → Injects escalation context into task's next wakeup
   → Task agent decides: draft follow-up, notify user, extend deadline, or cancel
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

Incoming Gmail event
  → GmailTriggerHandler fires
  → find_workflows() runs (existing behavior)
  → find_matching_open_loops("email", event_data) runs in parallel   ← new
       → queries open_loops: {channel:"email", resolved:false, user_id:uid}
       → matches sender + thread_id (or sender alone if no thread_id)
       → if matches found:
           → mark each loop resolved
           → store email in each matched task's inbox/
           → wake each matched task's conversation thread with context

Maintenance scan (every 30 min)
  → queries: { resolved: false, deadline: { $lt: now } }
  → for each expired loop:
       → check if_unresolved action
       → deterministic (no LLM): draft_followup_email, send_reminder, cancel_task, notify_user
       → requires synthesis run: escalate_to_loop
  → no LLM needed for deterministic actions

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:
=== PROACTIVE CONTEXT ===
Time: Thursday 2pm IST | User active: 2h ago | Tier: active

ACTIVE TASKS (3):
  "Schedule meeting with Rahul" — waiting 3 days, open loop EXPIRED
  "Manage inbox" — active, triage ran 12 times today, 3 todos created
  "Follow up with a16z" — waiting 5 days, open loop EXPIRING SOON

CALENDAR (next 48h):
  4pm today: Design review — Priya, Rahul (30 min)
  9am tomorrow: Investor call — a16z (1hr)

EMAIL DELTA (since last run 3h ago):
  NEW: Email from Rahul re: design review
  AGING: Your reply to contracts@acme.com — 3 days pending

TODOS:
  DUE SOON: "Q1 report" — due in 2 days
  OVERDUE: "Review PR #412" — 1 day overdue

GOALS:
  "Ship v2.0 by March" — next node: Deploy staging, stalled 5 days

INTEGRATIONS:
  GitHub: 2 PRs awaiting your review
  Slack: 3 unread DMs (1 from @priya)
  Linear: Sprint ends in 2 days, 3 issues open

MEMORY HIGHLIGHTS:
  "Prefers morning focus time, no interruptions before 10am"
  "Priya is the design lead, reports to user"
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: 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 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:
What it seesNext synthesis runWhy
Investor call tomorrow 9amTonight 10pmPrepare brief overnight
Expired open loop, Rahul 3 daysIn 4hCheck if follow-up was sent, escalate if not
Nothing actionable, quiet weekDay after tomorrowRoutine check-in
User hasn’t been active in 3 daysIn 3 daysDormant — lower 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

TierConditionSynthesis cadenceMaintenance cadence
ActiveSeen < 24h2-3x/dayEvery 30 min
DormantSeen 1-7 days1x/dayEvery 2h
InactiveSeen 7-30 days1x/week1x/day
ChurnedSeen > 30 daysPausedPaused
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:
memories_result, knowledge_result, active_tasks = await asyncio.gather(
    _get_memories(user_id),
    _get_knowledge(user_id),
    _get_active_tasks_summary(user_id),  # Redis 60s cache
)
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

User: "Schedule a meeting with Rahul"
  → Executor judges: multi-step, expects response → create GaiaTask
  → Creates /users/uid/tasks/task_abc/ with progress.md, context.json
  → Sends email to Rahul
  → Registers open loop: channel=email, sender=rahul@, thread_id=xyz, deadline=+3d
  → Appends to log.md: "Sent scheduling email to Rahul at 10am"

━━━ 3 days pass, no reply ━━━

Maintenance scan
  → open loop deadline passed, if_unresolved = "draft_followup_email"
  → Generates follow-up draft, sends it
  → Updates log.md, context.json (new open loop registered for follow-up)
  → Notifies user: "Rahul hasn't replied. I sent a follow-up."

━━━ Rahul replies ━━━

Gmail trigger fires
  → find_matching_open_loops("email", {sender: rahul@, thread_id: xyz})
  → Marks open loop resolved
  → Stores email in task inbox/rahul_reply_1.json
  → Resumes primary conversation thread (conv_123)
    (fallback: if conv_123 not found in checkpointer → send notification instead)
  → "Rahul is available Thursday 3pm or Friday 2pm. Want me to send an invite?"

Flow 2: Cross-Domain Synthesis

Synthesis run at 7am
  → Gathers full context
  → Sees: investor call at 9am, no prep task, Priya sent Slack DM, her PR waiting
  → Creates GaiaTask: "Prep for investor call"
    → GaiaTask self-schedules: wake 5 min after call ends (10am ARQ job)
  → Sends one notification: "Morning: investor call in 2h (brief ready),
    Priya messaged you on Slack and has a PR waiting for your review"
  → Schedules next synthesis run: tomorrow morning (not 5-min precision — task handles that)

━━━ Call ends at 10am ━━━

Task self-wakeup fires (ARQ job at 10:05am)
  → Agent reads progress.md, sees call just ended
  → Captures action items from any notes, drafts follow-up
  → Notifies user: "Call wrapped. I've drafted follow-up notes — want me to send them?"

Flow 3: Inbox Management Task

User: "Manage my inbox going forward"
  → GAIA creates GaiaTask: "Inbox Management" (no expiry — long-running)
  → Adopts system workflow "Inbox Triage" (runs every 15 min)
  → Creates workflow "Follow-up Tracker" (runs 1x/day)
  → Creates workflow "Weekly Inbox Review" (runs Sunday 6pm)
  → Task VFS accumulates: triage logs, follow-up drafts, weekly summaries

Each triage run
  → Appends to task's workflows/triage_{date}.json
  → After 7 days: older files moved to workflows/archive/, summary written
  → Emails needing reply registered as open loops automatically

Proactive loop (synthesis, weekly)
  → Reads task's progress.md (bounded — compacted)
  → Sees inbox health trends
  → "Your inbox has 15 emails waiting >3 days. Want a batch-reply session?"

MongoDB Collections

New Collections

gaia_tasks
task_id, user_id, title, description
status: active | waiting | stalled | completed | cancelled | expired | escalating
primary_conversation_id
owned_workflow_ids: List[str]
active_loop_ids: List[str]        # references to open_loops collection
vfs_path: str                     # /users/{uid}/tasks/{task_id}/
created_at, updated_at, completed_at
expires_at                        # TTL — checked by maintenance scan, not MongoDB TTL index
                                  # (we need custom expiration logic, not auto-delete)
open_loops (separate collection, not embedded — enables efficient indexed queries)
loop_id, task_id, user_id
description
channel: email | slack | github | calendar | linear | webhook
watch: {channel-specific fields — see watch schema above}
deadline: datetime
if_unresolved: draft_followup_email | send_reminder | escalate_to_loop | notify_user | cancel_task
resolved: bool
resolved_at: datetime | null
resolved_by: str | null  ("signal_match" | "task_completed" | "cancelled" | "expired" | "manual")

Indexes: { user_id, resolved, deadline }  ← maintenance scan query
         { channel, resolved, user_id }   ← trigger handler query
proactive_runs (one document per user, upserted on each run — not a run history log)
user_id
mode: maintenance | synthesis
next_run_at, next_run_reason
last_run_at, last_run_summary
last_context_snapshot             # for delta diffing
consecutive_no_op_count
activity_tier: active | dormant | inactive | churned
actions_taken_this_run: int       # reset at run start, checked by action tools
queued_notifications: List[dict]  # held when dormant (max 50, FIFO drop)

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.