Skip to content

Latest commit

 

History

History
510 lines (439 loc) · 31.4 KB

ARCHITECTURE.md

File metadata and controls

510 lines (439 loc) · 31.4 KB

Architecture

sf-agent is built on eve, a filesystem-first agent framework: capability comes from which folder a file lands in. This doc is the map. For the framework's own guides, read node_modules/eve/docs/.

The shape

agent/
  agent.ts              Runtime config (model = claude-sonnet-5).
  instructions/         Always-on system prompt, composed alphabetically.
    00-identity.md        Who the agent is ("Computer"), who Steven is.
    05-voice.md           Tone & persona (sincere + authoritative/ironic, dense, no snark/corny).
    10-behavior.md        Operating rules (proactive capture, confirm side effects).
    20-memory.md          How to use the memory tools.
    30-calendar.md        How to use the calendar tools.
  channels/             How you reach the agent.
    telegram.ts           Conversational chat + inbound images (vision).
    discord.ts            Slash commands + the bot's presence (owner-gated).
    eve.ts                Default HTTP API, locked to localDev + vercelOidc.
    cron.ts               Secret-protected reminder trigger (external cron).
    gmail.ts              Gmail push (Pub/Sub) + watch-renew endpoints.
    presence.ts           Home/away updates from the Pi (records a fact).
    discord-ingest.ts     Batched Discord messages from the Pi's gateway listener → triage.
    wake.ts               The Pi's clock calling back to run a real agent turn.
  tools/                Typed actions the model can call.
    capture.ts            Quick-capture inbox.
    list_inbox.ts         List inbox notes with ids.
    delete_note.ts        Remove a handled note.
    current_time.ts       Authoritative local time (host clock + DST-aware offset).
    set_location.ts       Set/clear a travel timezone override (auto-expires).
    recall.ts             Search notes + facts + tasks + Discord digests + email log.
    discord_recap.ts      What people were talking about on Discord (per-channel digests).
    remember_fact.ts      Save/update durable profile facts.
    list_facts.ts         List all profile facts.
    add_task.ts           Add a task or recurring chore.
    list_tasks.ts         List open/all tasks.
    complete_task.ts      Finish a task; recurring chores roll forward.
    reopen_task.ts        Reopen a task wrongly assumed done.
    list_calendar_events.ts    Read Google Calendar.
    create_calendar_event.ts   Write to Google Calendar (approval-gated).
    delete_calendar_event.ts   Delete a Google Calendar event (approval-gated).
    harmonic_matches.ts   Camelot-wheel compatible keys (DJ).
    list_work.ts          List work from the GitHub board.
    add_work_item.ts      Add a card to the GitHub board.
    set_work_status.ts    Move a card on the GitHub board.
    set_work_schedule.ts  Set the recurring work rhythm (days/hours/meetings).
    set_work_day.ts       One-off work-day override (PTO/WFH/holiday).
    work_schedule.ts      Read the schedule + a day's computed work status.
    search_email.ts       Search Gmail.
    read_email.ts         Read one email's body.
    send_email.ts         Send email (approval-gated).
    modify_email.ts       Archive / mark read / unread.
    attention_status.ts   Per-surface policy + what crossed each surface and what was done.
    configure_attention.ts  Mute/notify/escalate/capture per surface (enforced in code).
    schedule_wake.ts      One future moment — a real agent turn, or plain text.
    manage_wakeups.ts     The agent's recurring entries (incl. brief + review).
    remind_me.ts          Delayed plain-text reminder via the home Pi worker.
    remind_when.ts        Reminder or wake on getting home / leaving (presence).
    list_reminders.ts     Everything on the clock: one-shots, presence, recurring.
    cancel_reminder.ts    Cancel a pending one-shot by id or key.
    lighting_status.ts    Inspect live LIFX state, automation, and configuration.
    set_lights.ts         Direct per-light/all-light power and HSBK control.
    manage_lighting_schedule.ts  Edit the Pi's recurring lighting entries.
    lighting_effect.ts    Themes, drift, scenes, party mode, and flashes.
    configure_lighting.ts Tune daemon and per-light behavior.
    check_parking.ts      One-shot parking-camera check (Wyze cam via the Pi).
    watch_parking.ts      Timed parking watch on the Pi; Telegram ping when a spot opens.
    get_weather.ts        Real weather via Open-Meteo (no key): current + forecast.
  skills/               On-demand procedures (loaded when relevant).
    plan_a_trip.md        Family-aware travel planning.
    dj_set.md             DJ sets, harmonic mixing, crate.
    freelance.md          Client/project ops.
    gamedev.md            Devlog, backlog, playtests.
    work.md               GitHub board + day-job work schedule.
    gmail.md              Email: read, search, triage, send.
    lighting.md           Home-lab lighting themes + tuning.
    autonomy.md           Attention policy + scheduling its own thinking.
    new_baby.md           Second-daughter prep: checklists, leave, logistics.
  subagents/            Specialist child agents.
    planner/              Example: decompose a fuzzy goal into a plan.
  lib/                  Shared, import-only code (never enters the sandbox).
    store/                The durable-memory backbone (see below).
    attention.ts          Per-surface policy: mute / notify / escalate, enforced in code.
    triage.ts             The one triage path every input surface runs through.
    clock.ts              The agent's clock on the Pi (wakes + recurring entries).
    horizon.ts            Look ahead; put precise moments on the clock.
    recurrence.ts         Next-due computation for recurring chores.
    time.ts               Pure tz toolkit (DST-aware offset, local "today", quiet hours).
    location.ts           Effective tz: travel override (set_location) or home.
    work-schedule.ts      Resolve work rhythm per day (weekly pattern + overrides).
    task-reconcile.ts     Assume-done pass: decay overdue, roll recurring.
    google.ts             Service-account Calendar access (JWT, zero-dep).
    gmail.ts              Gmail via OAuth refresh token (read/send/manage/watch).
    github.ts             Projects v2 GraphQL (one board, zero-dep).
    telegram.ts           Direct Bot API send (zero-token pings).
    remote.ts             Transport to the home Pi worker + the parking camera.
evals/                  Scored behavior checks (eve eval).
docs/                   This folder.

The root agent's name comes from package.json name (sf-agent). Identity is always path-derived — no file sets its own name.

The memory backbone (lib/store/)

defineState from eve is session-scoped — it dies with the conversation. Everything a personal assistant remembers (notes, facts, chores) must outlive the session, so it lives in a small storage layer instead:

  • kv.ts — a five-method Kv seam (get/set/del/keys/mget; set takes an optional TTL for ephemera like dedup marks). Backed by Vercel KV / Upstash over its REST API when KV_REST_API_URL + KV_REST_API_TOKEN are set (zero extra dependencies, just fetch); otherwise an in-memory map that does not persist across restarts — and on Vercel's serverless runtime it won't survive between invocations, so configure KV before relying on memory there. Lists read via one batched MGET, not a round trip per key.
  • repositories.ts — typed repos (notes, facts, tasks, reminders, conversations, activity, attention) that own their key prefixes and JSON serialization. conversations holds rolling per-channel Discord digests — capped at 30 entries per channel with a 30-day TTL, so heavy chatter can never swamp the store. activity is the surface-generic spine: a rolling per-source log of what crossed each interaction surface (every non-spam email, every Discord channel that saw conversation, every arrival home) plus what the agent did with it — skipped, muted, considered, handled, or escalated, with the actions taken and why. Capped at 200 entries per source with the same 30-day TTL. New surfaces write to activity with their own source tag and get recall for free. attention holds the standing per-surface policy (below) — small, no TTL, configuration rather than traffic.
  • index.ts — assembles store once; import it anywhere: import { store } from "../lib/store/index.js";

Swapping to Postgres or another backend is a one-file change (kv.ts) — repos and tools don't move.

Task lifecycle (assume-done)

The project's ethos is to reduce attention, not demand it — so Steven never has to tell the agent a task is finished. Tasks decay on their own:

  • Recurring chores roll forward to their next occurrence once past due.
  • Low-stakes one-offs are silently assumed done a day or two after the due date (TASK_GRACE_DAYS). High-stakes tasks (set on add_task) never are — they keep surfacing until handled.
  • Email correlation: the triage pass closes any open task an incoming email confirms is done (a receipt, a delivery, a confirmation), with a quiet FYI.

The decay + recurring roll live in lib/task-reconcile.ts, run each sweep by the cron channel before reminders are collected. Every auto-close is recoverable with reopen_task. Done tasks are deleted for good after TASK_DONE_RETENTION_DAYS (default 90) so the store never grows unbounded. All recurrence math runs in Steven's local day (never raw UTC), and rolling a long-overdue chore catches up to the next genuinely future date.

Security posture

  • Telegram verifies the X-Telegram-Bot-Api-Secret-Token header, and onMessage drops anything not from OWNER_TELEGRAM_USER_ID.
  • Discord verifies Ed25519 signatures on every interaction, and onCommand drops anything not from OWNER_DISCORD_USER_ID.
  • eve HTTP channel is restricted to localDev() + vercelOidc() — reachable from your own CLI and trusted Vercel deployments, not the public web. Add real auth before exposing a browser UI.
  • Side-effecting actions (sending, booking, buying) are gated with human-in-the-loop approval — needsApproval from eve/tools/approval, as on create_calendar_event. The instructions also tell the model to confirm first.
  • Secrets stay server-side: the Google service-account key and any connection credentials live in tools/env and never reach the model.

Environment

See .env.example. Pull deployed values with vercel env pull.

Var Purpose
TELEGRAM_BOT_TOKEN / TELEGRAM_WEBHOOK_SECRET_TOKEN / TELEGRAM_BOT_USERNAME Telegram channel
OWNER_TELEGRAM_USER_ID Restrict the agent to you; also the brief's target
DISCORD_PUBLIC_KEY / DISCORD_APPLICATION_ID / DISCORD_BOT_TOKEN Discord channel
OWNER_DISCORD_USER_ID Restrict Discord commands to you
KV_REST_API_URL / KV_REST_API_TOKEN Durable store (required on Vercel)
GOOGLE_SERVICE_ACCOUNT_EMAIL / GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY Google service account
GOOGLE_CALENDAR_ID Calendar to read/write (your email address)
OWNER_TIMEZONE Local tz for "today" + quiet hours
APPOINTMENT_LEAD_MIN Minutes before an event to wake (default 60)
HORIZON_HOURS How far ahead the sweep plans wakeups (default 36)
TASK_DONE_RETENTION_DAYS Days to keep completed tasks before pruning (default 90)
CRON_SECRET Bearer secret for POST /eve/v1/cron/reminders (the planning sweep)
WAKE_SECRET Bearer secret for POST /eve/v1/wake (the Pi's clock starting a turn)
TRIAGE_MODEL Cheap triage model for every surface (default anthropic/claude-haiku-4.5)
GITHUB_TOKEN PAT with Projects read/write
GITHUB_PROJECT_ID The agent's board (a copy of your real board) — node id
GOOGLE_OAUTH_CLIENT_ID / _SECRET / _REFRESH_TOKEN Gmail (OAuth user token)
GMAIL_PUBSUB_TOPIC Pub/Sub topic for users.watch
GMAIL_PUSH_SECRET Guards the Gmail push + watch endpoints
PI_WORKER_URL / PI_WORKER_SECRET Home Pi worker (Tailscale Funnel): the clock, lighting, camera, gateway
PRESENCE_SECRET Guards POST /eve/v1/presence (home/away from the Pi)
DISCORD_INGEST_SECRET Guards POST /eve/v1/discord/ingest (message batches from the Pi)
<SURFACE>_TRIAGE_MODEL Per-surface model override, e.g. DISCORD_TRIAGE_MODEL

Extending

Add a tool — drop a file in agent/tools/. Filename = tool name.

import { defineTool } from "eve/tools";
import { z } from "zod";
import { store } from "../lib/store/index.js";

export default defineTool({
  description: "Mark a task or chore as done.",
  inputSchema: z.object({ id: z.string() }),
  async execute({ id }) {
    return (await store.tasks.complete(id)) ?? { error: "not found" };
  },
});

Google Calendar (service account) — wired via lib/google.ts and the list_calendar_events / create_calendar_event / delete_calendar_event tools. No per-user OAuth: a service account signs a JWT locally (zero-dep, Node crypto) and exchanges it for an access token. To set it up:

  1. In Google Cloud (project with the Calendar API enabled), create a service account and download a JSON key.
  2. In Google Calendar → your calendar's Settings and sharing → Share with specific people → add the service account's email with "Make changes to events".
  3. Set GOOGLE_SERVICE_ACCOUNT_EMAIL, GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY (the PEM, newlines escaped as \n), and GOOGLE_CALENDAR_ID (your email address — not primary, which is the service account's own empty calendar).

A service account can't reach Google Tasks on a personal Gmail (no sharing, no domain-wide delegation), so Tasks would need the OAuth refresh-token route. The agent's own add_task / list_tasks cover to-dos in the meantime.

Add a proactive job — not a file. Recurring work lives on the Pi's clock as a named entry the agent edits with manage_wakeups (see below). agent/schedules/ is deliberately empty: a Vercel cron would be a build-time constant the agent couldn't touch.

Add a specialist — a folder in agent/subagents/<id>/ with an agent.ts that exports a description. It inherits nothing from the root, so give it its own tools/skills as needed.

Attention

Every input surface — email, Discord, and whatever comes next — runs through one path in lib/triage.ts, parameterized by a durable per-surface policy in lib/attention.ts. A surface is a name, a normalizer into SurfaceEvent, and a policy record; it is not a second copy of the triage logic. That symmetry is the point: preferences used to stick on email and not Discord because they were two hand-written prompts.

The order is deliberately not all up to the model:

  1. Log — everything that crossed the surface, before any judgment, so recall works for mail and chatter the agent never read.
  2. Mute — deterministic patterns. Matching items are logged and never seen by a model or reported. This is the hard off switch.
  3. Escalate — deterministic patterns (or the triage model asking) promote an item to a full agent turn with every tool and all of memory. Escalation beats muting. It costs a real turn, so the bar is "I can't act correctly without it."
  4. Triage — one cheap structured call, restricted to what capture allows.
  5. Notify — gated in code. notify: "never" outranks whatever triage decided; "always" reports everything read.

Patterns are case-insensitive substrings, optionally scoped (from:, subject:, body:, channel:) or written as /regex/. Steps 2 and 5 exist because "stop telling me about sign-in emails" has to be something the agent can enforce — the old design could only append that sentence to a prompt and hope a small model honored it. guidance is the remaining soft steer, for judgment calls patterns can't express; a legacy <source>_triage_rules fact is folded in as guidance so rules set the old way keep working.

The agent reads all of it with attention_status (policies + the per-item log of what it did and why) and changes it with configure_attention. Defaults ship muting the automated mail nobody wants a buzz for — sign-in alerts, verification codes, 2FA — and every default is editable, including back on.

The agent's clock

The Pi worker holds time, and the agent owns what's on it. Vercel Hobby caps cron at a couple of daily jobs defined at build time, which is exactly the wrong shape: the agent could not add, retime, or remove its own recurring thinking. Two shapes, both durable across a reboot:

  • One-shot (schedule_wake) — fires once at a moment. A reminder sends fixed text; a wake runs a real agent turn via POST /eve/v1/wake, so the agent decides then what to do, with everything it knows at that time — or stays quiet, which is the expected outcome for most wakeups. An optional key makes a wake idempotent: re-scheduling the same key re-times the pending job instead of stacking a duplicate.
  • Recurring (manage_wakeups) — named entries with minute-precise local times and weekday selection, edited by name with the same replace/remove/upsert idiom as the lighting schedule. morning_brief and evening_review live here as ordinary entries, seeded once on a worker that has never had a cron file. "Move my brief to 7:30" or "stop the evening review" is a tool call, not a deploy.

presence triggers take the same fork — remind_when with kind: think wakes the agent on arrival or departure instead of pushing a string.

The sweep no longer notifies. POST /eve/v1/cron/reminders (Authorization: Bearer $CRON_SECRET, driven every 30 min by .github/workflows/reminders.yml) runs the assume-done reconcile and then plans: lib/horizon.ts walks the next HORIZON_HOURS (default 36) of calendar events and due tasks and asserts a keyed wake on the Pi for each — an appointment at start − APPOINTMENT_LEAD_MIN, a task at 9am on its due day. Keys make this safe to re-run, and a meeting that moves drags its wake along with it. A moment landing in quiet hours (10pm–7am) shifts back to 9pm the previous evening rather than being dropped.

That inverts the old behavior, which polled and could only announce that a date had already passed — news Steven already had. The sweep's cadence now affects only how quickly a newly created event gets a wake planned, never when he hears anything. Since every timed nudge depends on the worker, the sweep also reports a worker it can't reach — once a day, not once a sweep.

Domain packs

Steven's recurring contexts are packaged as on-demand skills rather than always-on prompt: plan_a_trip (family travel), dj_set (sets + harmonic mixing, backed by the harmonic_matches tool), freelance (client/project ops), gamedev (devlog + backlog), and new_baby (second-daughter prep: checklists, leave, logistics). The model loads one only when a request matches its description, so they never bloat every turn, and each leans on the shared memory and calendar tools. A pack graduates to a subagent only if it needs its own tool surface or a distinct role.

Observability

Deployed on Vercel, eve tags every workflow run with $eve.* attributes, so the Agent Runs tab traces each session → turn → tool call with token usage — no instrumentation.ts required. To export AI SDK spans to an OpenTelemetry backend (Braintrust, Datadog, Honeycomb, …), add agent/instrumentation.ts:

import { defineInstrumentation } from "eve/instrumentation";
import { registerOTel } from "@vercel/otel";

export default defineInstrumentation({
  setup: ({ agentName }) => registerOTel({ serviceName: agentName }),
});

It runs before agent code, so keep it minimal and install the exporter you use. Set recordInputs / recordOutputs to false if spans shouldn't carry message content.

Quality

evals/ holds eve eval checks — deterministic (completed, calledTool, content includes) plus one LLM-as-judge (closedQA) for reply quality. The judge routes through the AI Gateway and skips cleanly without credentials, so a plain run never errors. .github/workflows/ci.yml type-checks every push and PR; run eve eval --strict in CI when you want soft-threshold misses to fail too.

GitHub project board

The agent fully controls one GitHub Projects (v2) board — a copy of your real board, so the original is never touched. It reads and writes that copy: list_work summarizes current work, add_work_item adds draft cards, and set_work_status moves them (matched by title, so opaque node ids never enter the model's context).

It's three small authored tools over the Projects GraphQL API (lib/github.ts), not a GitHub MCP — a deliberately token-frugal choice, since an MCP pushes a large tool surface into context every turn. The work skill loads only on demand, and list_work returns just titles + status. Set GITHUB_TOKEN and GITHUB_PROJECT_ID.

Gmail

Gmail uses OAuth (a user refresh token), not the Calendar service account — a service account can't reach a personal inbox. lib/gmail.ts refreshes the access token and calls the Gmail REST API; tools are search_email, read_email, send_email (approval-gated — sending is irreversible), and modify_email.

Real-time processing runs through the gmail channel, not polling — and it doesn't ping per email (you already see mail arrive). It quietly keeps tasks and memory in sync, and only pings when something is time-sensitive:

  • Gmail publishes mailbox changes to a Pub/Sub topic; Pub/Sub push-delivers to POST /eve/v1/gmail/push?token=<GMAIL_PUSH_SECRET>. The push carries only a historyId, so the webhook fetches the delta (paginated) and dedupes per message. The stored baseline advances only after a successful sweep, so a transient failure retries instead of dropping mail; an expired baseline (Gmail keeps ~a week) resets with a warning. HTML-only mail is tag-stripped so triage isn't blind to it.
  • Free spam/bulk barrier (no model): Gmail's Promotions / Social / Updates / Forums / Spam categories and mailing-list mail (List-Unsubscribe, Precedence: bulk) are logged as skipped and go no further. Only Spam/Trash are excluded from the log entirely, so "did I get that email from GEICO the other day?" is answerable via recall even for mail the agent never read.
  • Everything else goes to the shared triage path with the email policy — the same code, the same policy shape, and the same activity log Discord uses. See Attention for the ordering. The cheap structured call (Haiku by default) records tasks (with stakes — a bill or deadline is marked high so it never silently decays), saves durable facts, schedules a reminder or a think on the Pi, closes tasks the mail confirms are done, and can escalate to a full turn. It does not auto-reply or write to the calendar — those become tasks.
  • Notification philosophy (identical on every surface, because it's the same code): report what the agent did, never what's happening — Steven already sees his own inbox. Every triage that takes actions sends one compact Telegram report (+ task: …, ⏰ reminder …, ✓ closed: …). A human-notify fires only when a real person is specifically trying to reach him or a deadline would genuinely hurt to miss. Automated mail is muted by default in code rather than argued down in a prompt.
  • users.watch expires after 7 days, so POST /eve/v1/gmail/watch renews it, driven daily by .github/workflows/gmail-watch.yml (no Vercel cron used).

Set the OAuth trio plus GMAIL_PUBSUB_TOPIC, GMAIL_PUSH_SECRET, and optionally EMAIL_TRIAGE_MODEL (or the surface-generic TRIAGE_MODEL).

Remote compute (home worker)

Some work doesn't fit a serverless function — arbitrary-delay reminders, long- running or interactive jobs. Those go to an always-on worker on a home Raspberry Pi (sf-pi-worker), reached over Tailscale Funnel (a stable public HTTPS URL) and gated by PI_WORKER_SECRET.

Most importantly it holds the agent's clock (see above): one-shot jobs — a reminder that sends text, or a wake that calls POST /eve/v1/wake to run a real agent turn — plus the durable recurring entries the agent owns (src/crons.ts). Both survive a reboot; a failed delivery retries and is only abandoned once it's hours stale, because by then it isn't the moment anymore. list_reminders shows everything pending, cancel_reminder drops a one-shot (by id or key), manage_wakeups edits the recurring set. The job protocol is typed and extensible, so new long-running job types are added on the worker side without touching the agent's serverless limits.

The worker also runs a cooperative LIFX lighting daemon on the LAN. Its durable, agent-editable schedule supports arbitrary named entries, weekday selection, minute-precise local times, power-off windows, and independent choices about reclaiming manually held bulbs or interrupting a held theme. Party mode and direct control remain available even when automatic lighting is disabled.

Five focused tools expose the mechanics instead of prescribing routines: lighting_status, set_lights, manage_lighting_schedule, lighting_effect, and configure_lighting. Direct changes can hold per bulb; custom themes retain gentle palette drift; configuration covers avoided colors, timing, brightness, and exclusions. Party mode remains the deliberate takeover exception: it snapshots every participating light, runs fast colors and optional flashes, supports in-place retuning, and restores the exact pre-party state when stopped.

It also runs a presence monitor (ip monitor neigh) watching Steven's phone on the LAN. Arrivals/departures fire remind_when items — either a Telegram message or a full agent wake (kind: think) — and POST to the agent's presence channel, which records a home_status fact and logs the transition to activity.

The worker also fronts the parking camera: a Wyze cam reached through docker-wyze-bridge on the LAN (unofficial — grab a free API key/id from Wyze's developer portal; see the worker's deploy/wyze-bridge.compose.yml). The worker pulls a JPEG frame and runs one cheap vision call (Haiku, direct Anthropic key or AI Gateway key) that returns {open, total, detail}. check_parking asks once; watch_parking starts a timed watch on the Pi that re-checks every couple of minutes and pings Telegram the moment a spot opens (stopping by default). Watches are in-memory — a Pi restart simply ends one.

Finally, it holds the Discord gateway listener (see below).

Discord ingestion

The Discord channel on Vercel is HTTP Interactions only — slash commands in, no passive reading. Autonomous ingestion needs a persistent Gateway WebSocket with the privileged Message Content intent, which a serverless function can't hold — so the Pi worker holds it (src/discord/gateway.ts, zero-dep raw WebSocket with heartbeat/resume). It watches server messages and DMs (skipping bots, optionally filtered to DISCORD_WATCH_CHANNELS), buffers them, and forwards a batch every ~60s to POST /eve/v1/discord/ingest guarded by DISCORD_INGEST_SECRET.

The agent side isn't merely like Gmail's — it is literally the same code, with the discord policy instead of the email one (see Attention). No per-message agent turns: one cheap structured call per batch records tasks, saves facts, schedules Pi reminders or thinks, closes tasks the chatter confirms done, escalates when it can't act correctly on its own, and pings only when a real person is specifically trying to reach Steven. His own messages are labeled so commitments he makes get captured but never ping him. Because Discord's policy groups by channel, the batch lands in activity as one line per channel rather than one per message.

The triage call also writes conversation digests — 1-2 sentences per active channel (topics, participants, outcome) into the conversations store, capped and TTL'd so heavy servers can't swamp memory. That's what lets Steven ask "what were people talking about in #gamedev?" later: discord_recap gives a channel overview or per-channel history, recall searches digests alongside notes/facts/tasks, and the agent points him to the channel for the full thread. Batches are deduped by id, so a worker retry never double-processes. Enable the Message Content Intent in the Discord Developer Portal (Bot → Privileged Gateway Intents) or the gateway closes with code 4014.

Model strategy

Root runs claude-opus-5 for planning and reasoning. Push heavy planning to claude-opus-4.8 and cheap, high-volume subtasks to claude-haiku-4.5 via a subagent. Per-agent model is set in each agent.ts.

Deploy notes

  • There are no Vercel Cron Jobs. Recurring work lives on the Pi's clock so the agent can edit it; the only external timers are two GitHub Actions workflows (the planning sweep and the Gmail watch renewal).
  • Point the worker's AGENT_WAKE_URL at https://<app>/eve/v1/wake and match AGENT_WAKE_SECRET to the agent's WAKE_SECRET, or scheduled wakes degrade to plain Telegram messages.
  • Replace the eve route auth with real auth before any public exposure.
  • Run eve eval --strict in CI so soft-threshold misses also fail the build.