12 KiB
@elizaos/plugin-inbox
Unified cross-channel inbox triage with unresolved-item tracking, snooze, archive, and follow-up watcher for Eliza agents.
Purpose / role
Adds the inbox-zero workflow to an agent: a single INBOX umbrella action (op-based dispatch), INBOX_TRIAGE + CROSS_CHANNEL_CONTEXT providers that surface unresolved threads to the planner each turn, and a registered /inbox view for human review. Aggregates threads across email, Discord, Telegram, WhatsApp, Slack, X, Farcaster, iMessage, and similar non-SMS channels. Android SMS stays in @elizaos/plugin-messages.
This package owns the triage domain carved out of plugin-lifeops: the persisted queue, queue operations, providers, schema, migration, and terminal/app view registration. It also owns the cross-channel aggregation domain (src/inbox/aggregate.ts: channel normalization, buildInbox thread grouping, resolveInboxRequest, LLM priority orchestration, and the cached read-through InboxDomain) plus the LLM priority scorer (src/inbox/priority-scoring.ts). @elizaos/plugin-personal-assistant keeps the transport route (GET /api/lifeops/inbox), the life_inbox_messages cache tables in app_lifeops, and the Gmail/X connector projections — it composes the InboxDomain by injecting those through the typed seams (InboxMessageCache, PriorityScoringSettingsLoader, GmailInboxSource/XDmInboxSource) and keeps behavior-identical re-export shims at the old import paths.
Plugin surface
Action
INBOX(src/actions/inbox.ts) — single umbrella action with op-based dispatch. Accepted ops:list,search,summarize,triage,reply,snooze,archive,approve.list/search/summarizefan out through per-platform fetchers;triagereads the persisted unresolved queue;replydrafts/sends via MESSAGE triage adapters;snoozehides entries until an ISO timestamp;archiveruns connector archive and resolves on success;approvesends the stored draft or suggested response.
Providers
INBOX_TRIAGE(src/providers/inbox-triage.ts) — position14. Emits the owner's pending urgent/needs-reply queue and recent auto-replies fromInboxRepository.CROSS_CHANNEL_CONTEXT(src/providers/cross-channel-context.ts) — position-3. Emits recent unresolved activity for the current counterparty across other channels.
Routes
GET /api/lifeops/inbox/triage— list unresolved triage entries, optionally filtered byclassification,limit, andincludeSnoozed.POST /api/lifeops/inbox/triage— classify/persist inbound messages throughInboxService.triage.POST /api/lifeops/inbox/:id/reply— draft or send a connector-backed reply.POST /api/lifeops/inbox/:id/snooze— setsnoozed_until.POST /api/lifeops/inbox/:id/archive— archive through the connector adapter and resolve on success.POST /api/lifeops/inbox/:id/approve— send the stored draft/suggested response.
Schema
inboxSchema(src/db/schema.ts) —pgSchema("app_inbox")with the three inbox-triage tables carved out of PA'sapp_lifeops:life_inbox_triage_entries— per-thread triage decisions + draft replies.life_inbox_triage_examples— owner-labeled few-shot classification examples.life_email_unsubscribes— email unsubscribe attempts + outcomes. Registered via the pluginschemafield;InboxMigrationService(src/inbox/migration.ts) does the non-destructiveapp_lifeops -> app_inboxcopy (skip if source missing / target non-empty, never drop the source) and repairs olderapp_inbox.life_inbox_triage_entriestables by addingsnoozed_untilif missing. PA auto-registers this plugin (ensureLifeOpsInboxPluginRegistered) so the schema exists + the migration runs whenever PA is loaded. The gmail sync/projection tables (life_gmail_*,life_inbox_messages) are NOT part of this domain — they stay PA-owned inapp_lifeops.
View
inbox—InboxViewcomponent, path/inbox, bundle atdist/views/bundle.js. Shows the cross-channel inbox surface using the shared app client.
Layout
src/
index.ts Public API barrel
plugin.ts inboxPlugin definition (action + providers + schema + view)
types.ts TriageDecision, ThreadSummary, channel + decision enums
actions/
inbox.ts INBOX umbrella action — fan-out + triage queue ops
routes/
inbox-routes.ts Triage read/write + reply/snooze/archive/approve HTTP routes
providers/
inbox-triage.ts INBOX_TRIAGE provider
cross-channel-context.ts CROSS_CHANNEL_CONTEXT provider
inbox/
service.ts InboxService — classify/curate/search/list/digest/resolve/snooze
repository.ts InboxRepository — raw SQL over app_inbox.life_inbox_triage_*
migration.ts app_lifeops -> app_inbox copy + additive repair
types.ts InboundMessage, TriageEntry, TriageClassification, etc.
triage-classifier.ts LLM classification of inbound messages
aggregate.ts Cross-channel aggregation domain: builders,
resolveInboxRequest, LLM-score orchestration,
cached read-through InboxDomain (host seams:
InboxMessageCache, PriorityScoringSettingsLoader,
Gmail/X sources)
priority-scoring.ts LLM inbox priority scorer (batched, cached,
concurrency-capped)
db/
index.ts re-exports schema.ts
schema.ts drizzle pgSchema('app_inbox') + 3 tables
components/
inbox/
InboxView.tsx Minimal React inbox view (placeholder)
inbox-view-bundle.ts Vite bundle entry — re-exports InboxView
Commands
bun run --cwd plugins/plugin-inbox typecheck # tsc --noEmit
bun run --cwd plugins/plugin-inbox lint # biome check src/
bun run --cwd plugins/plugin-inbox test # vitest run
bun run --cwd plugins/plugin-inbox build # build:js + build:views + build:types
bun run --cwd plugins/plugin-inbox build:js # tsup
bun run --cwd plugins/plugin-inbox build:views # vite build (overlay bundle)
bun run --cwd plugins/plugin-inbox build:types # tsc declaration emit
bun run --cwd plugins/plugin-inbox clean # rm -rf dist
Config / env vars
None. Channel credentials are read from each provider plugin (plugin-discord, plugin-telegram, etc.).
How to extend
Add a new op: add the name to SUBACTIONS in src/actions/inbox.ts, add a case to the handler or executeInboxQueueOperation, and extend the action parameters array when the op needs new inputs. Queue ops should go through InboxRepository and, for connector dispatch, the shared MESSAGE triage service.
Add a provider: create src/providers/<name>.ts exporting a Provider, then add it to the providers array in src/plugin.ts.
Add a service: define the class in src/service.ts, add it to the services array in src/plugin.ts, and export it from src/index.ts so callers can resolve it via runtime.getService.
Conventions / gotchas
@elizaos/plugin-sqlmust be loaded first. The schema registration relies on the runtime'sruntime.db. The plugin declares this independencies: ["@elizaos/plugin-sql"].- No Android SMS. SMS routing intentionally stays in
plugin-messages. Do not add SMS channel handling here. - Schema name is
app_inbox(notinbox) to avoid collisions with any host-appinboxtable the runtime might also surface. - Snooze is additive.
snoozed_untilis append-only schema growth onlife_inbox_triage_entries; migration repairs old targets and maps oldapp_lifeopsrows withNULL AS snoozed_until. - Two build steps. The JS/types build (tsup + tsc) and the Vite views build are separate. The views bundle (
dist/views/bundle.js) is what the view registration'sbundlePathpoints to. Both must be run for a complete build. - Peer deps. React 19 and react-dom 19 are peer dependencies. The host app must provide them.
- See the root
AGENTS.mdfor repo-wide architecture rules, logger requirements, ESM/module standards, and the cloud-frontend visual-review gate (if any of this plugin's UI ends up incloud-frontend).
⛔ NON-NEGOTIABLE — evidence, trajectories & real end-to-end tests
The binding, repo-wide standard is AGENTS.md. Read it. Nothing in this package is done until it is proven done — a reviewer must confirm it works without reading the code, from the artifacts you attach. This applies to every feature, fix, refactor, and chore here. "Tests pass" is not proof; "CI is green" is not proof.
- Record AND read model trajectories. Capture the actual inputs and outputs of the model
from a live LLM — not the deterministic proxy, not a mock: the prompt, the
providers/context, the raw model output, every tool/action call, and the result. Then open
the trajectory and review it by hand. A captured-but-unread trajectory is not evidence
(
packages/scenario-runner/bin/eliza-scenarios run <scenario> --report <out>). - Real, full-featured E2E — no larp. Every feature ships detailed end-to-end tests that drive the real path end to end. Not the happy "front door" only: cover error paths, edge/empty/invalid input, concurrency, roles/permissions, and adversarial input. A test that asserts against a mock/stub/fixture standing in for the thing under test does not count. If the real model/device/chain/connector/account is hard to reach, make it reachable — that is the work, not an excuse to mock. If the existing tests here are shallow or mocked, fixing them is part of your change.
- Screenshots + logs at every phase, plus a complete walkthrough video/run-through of
the entire feature or view, start to finish (
bun run test:e2e:record). - Manually review every artifact the change touches — never just the green check: client
logs (console + network), server logs (
[ClassName] …), the model trajectories in and out, before/after full-page screenshots, and the domain artifacts listed below for this package. - No residuals. No shortcuts. The goal is not "done" — it is everything done. Clear every blocker by the hard path: build the real architecture, stand up the real model/device/service, actually test it. Never leave a TODO, a stub, a stepping-stone, or a "follow-up." When unsure, research thoroughly, weigh the options, and ship the best, highest-effort, production-ready version. Keep going until every possibility is exhausted.
Artifacts → attached inline in the PR (MP4 video, JPG screenshots, logs in <details>); attach each evidence type or
explicitly mark it N/A with a reason — never leave it blank. If develop moved and changed
behavior, re-capture evidence; stale proof is worse than none.
Capture & manually review for this package — platform connector:
- A real (or sandbox-account) round-trip on the platform: inbound message → agent → outbound reply, captured as logs and a screenshot/recording of the actual conversation.
- The raw inbound event/webhook payload and the outbound API request/response, with IDs mapped correctly (
stringToUuid/createUniqueUuid). - Attachments, threads/replies, edits, multi-account, and rate-limit/error paths — not just a single text ping.
- The agent trajectory for the turn the connector drove.