The research agent
The agent works out who the people in the CRM are. It is an eve app, it is its own deployment, and it owns every piece of intelligence in the product. The API reports that something happened; the agent decides what it means.
What it's made of
| 🔧 20 authored tools | read_crm_history, search_crm, identify_contact, research_person, enrich_company, record_fact, schedule_recheck, and more. |
| 📄 Prose skills | evidence.md, identity-matching.md, data-boundaries.md, writing-a-brief.md — instructions the agent reads, versioned like code. |
| ⏱️ One schedule | dispatch decides nothing. It leases what is due and starts a session per row. |
| 📦 A sandbox | bash, grep, glob and a /workspace, with deny-all egress. |
It runs itself
The work queue is a database table. claimDue leases rows with FOR UPDATE SKIP LOCKED, so two dispatchers take disjoint work and a run that dies frees its row when the lease expires. Anything that looks like "every N minutes, the oldest ten contacts" belongs in a task's dueAt, not in a cron expression.
When the agent wants another look at somebody it calls schedule_recheck and says why — and the reason is shown to the rep, because an agent that cannot say why it will be back in fourteen days does not have a reason, it has a default.
Nothing about it is request-response. Close the browser and it keeps going, on its own schedule, against its own budget.
Budget
Every session gets a research budget. Every vendor call charges it. Running out is a normal ending — not an error. The budget keeps a single contact from consuming an unbounded number of lookups.
Two dispatch lanes
The dispatcher drains the queue in two independent lanes:
| Lane | Kinds | How it runs | Per tick |
|---|---|---|---|
| Visible | logos, portraits | Directly — no model, no session | 60, six at a time |
| Research | everything else | One eve session per row | 12 |
Neither kind in the visible lane has anything to decide — a logo is a domain in, one call out, write. Routing it through a model session would buy a context window to make no decisions with. Lanes are why a queue of thirty "identify this contact" rows never delays a single logo.
Task priority
One ordered list decides what a rep sees resolve first:
| Priority | Task | Why |
|---|---|---|
| 900 | logo + real name | On every row of the list, before a rep opens anything. One call, seconds. |
| 800 | portrait | The face. |
| 500 | workspace profile | Who we are — every later session opens with it. |
| 300 | requested | A rep pressed Research. |
| 200 | meeting | A meeting is coming. |
| 100 | identify | A new contact. |
| 50 | sweep | The sign-in backfill. |
| 40 | company brief | The written brief. |
| 0 | recheck | Come back in ninety days. |
Starting now instead of on the minute
POST /internal/crm/dispatch drains both lanes on demand, and the API calls it (fire-and-forget) after writing any task row. Add a company and its logo appears; add a contact and the research starts. The cron is a backstop rather than the only door. Overlapping pokes collapse into a single trailing drain so a sync creating forty contacts doesn't launch forty sessions at once.
The framework is the source of truth
The complete eve documentation ships inside the installed package and matches the version exactly. Guessing at the API is expensive in a specific way: it typechecks, builds, and then behaves differently from what was assumed.