# Feature Coverage

Every framework capability and how a composed agent reaches it. This table is the **authoritative, exhaustive** answer to "can the config do X?" — answer from it, do not grep the `@mono-agent` package source to confirm. `config` = declarable in `mono-agent.config.json`; config fields may be JSON-only. Environment-variable overrides are optional: only fields with a documented `MONO_AGENT_*` mapping accept one, so consult the generated config reference's `Env override` column (`--` means none, as for `channels.plugins`) instead of inferring one. `cli` = a `mono-agent` CLI flag/command, `auto` = always on when the app runs, `code` = programmatic escape hatch only, `dev` = development/test tooling. A capability that is absent here, or marked `code`, is not reachable through config — that is the answer, not a cue to read source. The final column maps config-bearing rows back to the repo's canonical registry; multiple ids in one row are an intentional aggregation. The repo's `docs/reference/feature-registry.md` (framework checkout only) and the documentation site at <https://mono-agent-docs.vercel.app/> are longer-form human-facing mirrors of this same table.

The default configured agent may also advertise the operator-only v1
`contextImport` capability. It is not a config toggle and must be detected from
`GET /v1/info`; `historyAppend`/`deliverVerbatim` is a separate legacy surface
and is not evidence of import support. Import performs no cron or model turn and
accepts at most 32 KiB of decoded UTF-8 text with a source-qualified 512-byte
idempotency key and a 4096-byte conversation id.

## Runtime

| Capability | Coverage | Where | Registry config ids |
| --- | --- | --- | --- |
| Model backends: Pi-only `<provider>:<model>` (OpenAI, OpenAI-Codex, Copilot, Anthropic, OpenRouter, OpenCode-through-Pi, Ollama, LM Studio, ...); a legacy `pi:` prefix is canonicalized away | config | `runtime.model` | `runtime.multi-backend` |
| Subagent delegation via the Agent tool | config | pi runtime only. Requires BOTH `subagents.enabled: true` and `Agent` in `tools.allowedTools`. Each definition needs exactly one of `prompt` or `promptPath`; omitted `allowedTools` means a read-only default set and `"*"` is rejected. Subagents are capped by `maxConcurrent` (5) and `maxPerTurn` (20), never receive Agent/AskUser/channel-send tools, and cannot spawn subagents | `runtime.subagents` |
| Same-model retries before failover | config | `runtime.retry.primaryAttempts` (default 2) gives the primary a second attempt before the chain advances; per-route `runtime.fallbacks[].attempts` opts a backup in. Only transient provider failures retry — context overflow and bad credentials still advance. Set `primaryAttempts` to 1 to disable | `runtime.retry` |
| Backup models on retryable provider failure | config | `runtime.fallbacks[]`, each route owning optional exact effort (omission = provider default) | `runtime.fallback-models` |
| Effort, max turns, workspace | config + cli | `runtime.effort` (`none` / `minimal` / `low` / `medium` / `high` / `xhigh` / `max` / `ultra`; `mono-agent init --effort <level>`). Reasoning-capable models map `ultra` to LOW; models without reasoning use OFF. `max` degrades to `xhigh` unless the resolved model advertises it. `mono-agent doctor` validates effort against the model's advertised levels and warns, naming the nearest supported level, when a configured value is outside that set. Ranking above `max` only prevents keyword downgrade. `runtime.maxTurns`, `runtime.workspace` | `runtime.effort`, `runtime.max-turns`, `runtime.workspace` |
| Tool-permission posture | config | `runtime.permissionMode` (validated and forwarded, not consumed by the Pi runtime) | `runtime.permission-mode` |
| Continuous provider sessions with idle eviction and optional daily rollover | config | `runtime.session.{mode,idleTimeoutMs,rollover,rolloverTimezone,rolloverNotice}` | `runtime.provider-sessions` |
| Per-channel run admission/execution bounds | config | `concurrency.maxConcurrentRuns`, `concurrency.maxPendingRuns` | `runtime.concurrency` |
| Local providers (Ollama / LM Studio / OpenAI-compatible) | config | `providers.<id>.{type,baseUrl,enabled,trustPublicUrl,apiKeyEnv,models,maxAdvertisedModels}`; the legacy `providers.local[]` array is still accepted and migrated on load | `runtime.local-providers` |
| Provider catalog (declares supported providers, widening model selection to each provider's full catalog) | config | the `providers` map, keyed by provider id — `providers.piAuthPath`, `providers.piNative`, and `providers.local` are reserved keys, never provider ids, and `providers.entries` is the resolved in-memory shape, not a JSON key. `ollama` and `lmstudio` are zero-config autodiscovered via localhost; a route naming any other provider that is not a Pi builtin fails config load until the map declares it with a `baseUrl` | `runtime.providers` |
| Pi OAuth/API-key credentials and web-console re-authentication | config + cli + auto | `providers.piAuthPath`; `mono-agent auth login`; Agent settings lists only effective used providers and, when the protected operator capability is present, offers Pi-native GitHub/OpenAI device code, Anthropic redirect/code paste-back, or masked provider prompts through the same owner-only locked/no-clobber Pi-store transaction. Detection and successful live-request verification are separate; no `--device-auth` flag exists and Codex CLI `~/.codex/auth.json` is not covered | `runtime.pi-credentials` |
| Pi-native transport, retry, and durable provider-session tuning | config | `providers.piNative.{transport,piMaxRetries,maxRetryDelayMs,piSessionsRoot}` | `runtime.pi-native-tuning` |
| Prompt-cache diagnostics (metadata-only request fingerprints in run artifacts, offline token-weighted cache summary) | config | `providers.piNative.promptCacheDiagnostics` (`MONO_AGENT_PI_PROMPT_CACHE_DIAGNOSTICS`), default `false`; summarize with `scripts/summarize-prompt-cache.mjs` | `runtime.prompt-cache-diagnostics` |
| Tool-output bloat guard, cost tracking | auto | built into every run | — |
| Context handling / auto-compaction | config + provider | `runtime.compaction.*` (matching `MONO_AGENT_COMPACTION_*`) controls adaptive Pi compaction. Omitted budgets derive from the active model window; reactive recovery re-prompts exactly once only after a verified reduction, and persistent `context_limit` advances to a configured fallback | `runtime.context-compaction` |
| Structured output (JSON schema) | code | harness `runtimeOptions.outputSchema` | — |
| Live input steering | auto + code | built into ordinary Slack and Telegram turns and the web console's single server-authoritative Send path, with exact host-operation ownership, separate native acceptance, exact transcript consumption, uncertainty/no automatic retry, durable UUID receipts, and confirmed safe-preview `Steered` activity; custom hosts use `runtimeOptions.liveInput` | — |
| Tool approval gates (risk tiers, timeouts, always-allow) | code | `createMonoRuntime({ onToolApprovalRequest, ... })` — needs a host UI | — |
| Fully custom runtime | code | `startMonoAgentApp({ runtime })` | — |

## Context, skills, memory

| Capability | Coverage | Where | Registry config ids |
| --- | --- | --- | --- |
| Public name plus identity and optional soul documents; wizard Role has one explicit destination and created/preserved outcome | config + cli | `agent.name`, `context.identityPath`, `context.soulPath`; guided Role is `IDENTITY.md` → `## Role`, and an existing identity is never overwritten | `agent.public-name`, `context.identity`, `context.soul` |
| Selected skills from a skills root | config | `context.skillsRoot`, `context.selectedSkills` | `skills.selected-activation` |
| Generated project memory skill with progressive disclosure | config + cli | init selects `mono-agent-memory` under `./skills` with `context.skillDisclosure: "index"`; drift and retired-skill migration: `mono-agent install-skill --project --check\|--update` | `app.managed-project-skills` |
| Version-matched semantic documentation companion for the global composer skill | cli + tool | `mono-agent install-skill` pairs `mono-agent-docs` with every available Codex/Claude target by default; `--no-docs-mcp` opts out; `mono_agent_docs` uses `action: "search"` for ranked 2–3k excerpts and `action: "read"` for anchored windows up to 10k, internal-link targets, and exact continuation actions | — |
| Per-skill byte cap | config | `context.skillMaxBytes` | `skills.byte-capping` |
| Conversation history (owner-only durable store) | auto | 64 messages per exact conversation id independent of `runtime.maxTurns`; aggregate defaults 256 MiB / 10,000 conversations / 365 inactive days; staged atomic publication and post-commit pruning; custom store via code | — |
| Lite memory (FTS keyword recall + rapid-log capture; no external deps) | config | `memory.mode: "lite"`, `path`, `maxBytes`, `writeMode` | `memory.lite` |
| Semantic embedding provider selection for Journal/BuJo | config + cli | `memory.embeddings.{provider,endpoint,model,dim,apiKeyEnv}`; guided init supports exclusive Ollama or LM Studio discovery and proof | `memory.embeddings-config` |
| Journal memory (hybrid recall BM25+vector + static canonical salience; needs configured embeddings) | config | `memory.mode: "journal"`, `path`, `memory.embeddings.{provider,endpoint,model,dim,apiKeyEnv}` (`provider: "ollama" \| "lmstudio" \| "openai"`; exclusive, no cross-provider fallback) | `memory.journal` |
| BuJo memory (journal + LLM capture/reconcile ADD/UPDATE/SUPERSEDE/NOOP + entity graph + auto-scheduled consolidation; needs embeddings + an app-level `memory.llm`) | config | `memory.mode: "bujo"`, `path`; selected Ollama/LM Studio/OpenAI embeddings are independent from explicit `memory.llm` with `provider: "ollama"` (`model`, optional `endpoint`) or `provider: "agent-host"` (`model` is a runtime model ref) — see `docs/memory/index.md` | `memory.bujo` |
| Supermemory external backend (server-side extraction/consolidation; async ingestion; explicitly installed plugin) | config | `memory.backend: "supermemory"`, `memory.writeMode`, `memory.supermemory.{baseUrl,apiKey,apiKeyEnv,container,timeoutMs,exposeMcpServer}`; install the exact matching `@mono-agent/memory-supermemory` version | `memory.backend-supermemory` |
| BuJo consolidation auto-scheduler (projection-only `index.md` refresh + empty `future-log.md` stub + duplicate-group reporting; in-app, no external cron needed) | config | `memory.consolidation.{enabled,cron}` (five-field UTC, default `0 */2 * * *`, no hashed `H`); env `MONO_AGENT_MEMORY_CONSOLIDATION_CRON`, `MONO_AGENT_MEMORY_CONSOLIDATION_ENABLED` | `memory.bujo-consolidation` |
| Memory maintenance CLI | cli | `mono-agent memory <subcommand>` from the agent folder (stats/today/show/search/top/audit/inspect/rebuild/rollback/…). The standalone `memory-bujo <subcommand> <root>` CLI and bin were removed; routine BuJo consolidation runs via the in-app scheduler | — |
| Config-aware memory preview CLI (stats/today/show/search/top plus metadata-only audit; remains available when the live recall tool is disabled; local search warns and falls back to FTS-only when embeddings are down) | cli | `mono-agent memory stats\|today\|show <date>\|search <query>\|top\|audit [--limit <n>] [--json]` | — |
| Memory liveness check (managed tier/provider/model/dimension identity; provider-native typed discovery plus real finite-vector/dimension probe for Ollama or LM Studio; declared auth env; BuJo LLM config + consolidation cadence; no cross-provider fallback) | cli | `mono-agent validate` | — |
| Memory write modes and per-turn BuJo capture | config | `memory.writeMode`: `disabled`, `append-host-summary`, or `capture`; capture requires `memory.mode: "bujo"` | `memory.write-mode`, `memory.per-turn-capture` |
| Auto-provisioned targeted read-only `MemoryRecall` tool exposed for every configured memory backend; no chat LLM | config | `config.memory.recallTool.enabled` (`MONO_AGENT_MEMORY_RECALL_TOOL_ENABLED`, default on; explicit false opts out of both explicit memory-read tools) | `memory.recall-tool` |
| Bounded `MemoryJournal` broad chronological retrieval over curated canonical local memory; Lite/Journal/BuJo only, Supermemory unsupported; no embedding/chat call | config + auto | Shares `memory.recallTool.enabled`; restrictive `tools.allowedTools` must name `MemoryJournal` (or its canonical MCP name/server wildcard), deny wins | `memory.journal-browse` |
| Agent-callable `Remember` tool that durably stores one explicitly stated fact; deterministic, append-only, no chat LLM; bujo backend and writable stores only; allowlist-gated and rejects credential-bearing text | config | `config.memory.rememberTool.enabled` (`MONO_AGENT_MEMORY_REMEMBER_TOOL_ENABLED`, default on for the bujo backend; explicit false opts out); a restrictive `tools.allowedTools` must name `Remember` | `memory.remember-tool` |
| In-app memory LLM call timeout | config | `memory.llm.timeoutMs` (`MONO_AGENT_MEMORY_LLM_TIMEOUT_MS`, default 60000) | `memory.llm-timeout` |

## Tools, MCP, sandbox

| Capability | Coverage | Where | Registry config ids |
| --- | --- | --- | --- |
| Allow-all tool policy (omitted / `["*"]` = all tools; `[]` = none) | config | default `tools.allowedTools`; the harness no-policy safety net is `failClosedToolPolicy()` | `tool-policy.allow-all` |
| Built-in tool allow/deny lists (deny wins, even under allow-all; pi doesn't deny external MCP tools) | config | `tools.allowedTools`, `tools.disallowedTools`; managed built-ins are Read/Write/Edit/Glob/Grep/Exec/Bash/NodeRepl/WebFetch/WebSearch | `tool-policy.allowlist`, `tool-policy.denylist`, `runtime.builtin-tools` |
| Additional managed file-tool roots while process sandboxing is off | config | `tools.filesystem.readableRoots` adds roots for Read/Glob/Grep; `tools.filesystem.writableRoots` adds roots for Write/Edit and also makes them readable. Lexical and realpath containment reject traversal and symlink escapes. Native sandbox roots remain authoritative when enabled; these fields do not constrain shell command contents | `tool-policy.filesystem-roots` |
| Pi-native Exec/Bash background process jobs with exact-thread wake | config + cli + auto | `processJobs.*` opts in (default off; unsupported on Windows). Only controller-ready exact Slack, Telegram, and web-console turns gain optional `background`; request lineage diagnostics remain visible at exhaustion (default depth 4, cap 64). Background-only `wake_on_completion` defaults true; explicit false keeps terminal card updates without a wake. Exact sentinel-only replies suppress delivery; narration and rich content stay visible. Ambiguous wake receipts are unknown and never auto-replayed. Host-owned queue/runtime/output/depth/retention caps, owner-private records, inherited POSIX process groups, restart interruption, unforgeable-depth normal-tool wakes, Slack/Telegram same-message lifecycle updates, fail-closed live store health, `mono-agent jobs list\|get\|cancel`, and durable web cards are built in. Busy pre-turn wake admission remains pending without consuming its retry budget. JSON-only `unsafeAllowUnprotectedState` is an explicit sandbox-off/all-Pi trusted-host posture: it suppresses ProcessJobs and clear-sessions SRT while retaining registry, lease, private-root, and provider-zero route invariants, and warns that ProcessJobs state plus the operator secret are model-accessible. Commands that daemonize into another group or session are unsupported | `agent-app.process-jobs` |
| Pi-native `Monitor`/`MonitorStop` streaming watches with per-batch conversation wakes | config + cli | `monitors.*` opts in (default off; additionally requires `processJobs.enabled`, whose protected private-state root, registration proof and origin binding it reuses; unsupported on Windows). A monitor keeps a command alive after the turn returns, treats each stdout line as one event, coalesces within `coalesceMs`, and wakes the exact originating conversation per batch plus once at the end, steering an in-flight run or queuing its own turn and holding no provider slot. Command preparation, workdir rules, environment cleaning, sandbox seam and output redaction are shared with `Bash`, so there is no separate command allowlist. Tool fields default to `wake_on: batch`, `dedupe: none`, `min_wake_interval_ms: 0`. Opt-in consecutive batch dedupe ignores ANSI redraw controls only; meaningful whitespace and timestamps remain significant. The host clamps the interval to `monitors.maxWakeIntervalMs` (default/cap 300000) and reports the effective policy. Exit-only permits only default dedupe/interval and sends one terminal wake with a bounded tail. First and terminal wakes bypass the interval; cancellation must never automatically recreate a watch. CLI/web expose durable suppression and follow-up/steered/unknown wake counters, not invented model costs. `monitors.maxChainDepth` defaults to4, cap64. Capacity is counted independently of `processJobs.*`; one wake per monitor is in flight at a time, pending batches are bounded in lines and bytes with oldest-drop accounting reported to the model, and a sustained firehose stops the watch with `rate_limited` plus one terminal wake. A pre-dispatch refusal is re-offered with the same batch under a fresh sequence; every other delivery failure is never replayed. Restart marks live monitors `interrupted`, terminates the incarnation-matched group, and owes exactly one recovery wake without re-running a model-authored command. The fenced envelope declares the turn host-raised and its content untrusted, and a `NOTHING_TO_REPORT` reply is suppressed. Availability is Telegram, Slack, and existing user-created web conversations; cron, webhook, web:new, TUI-direct and A2A never receive the tools. `mono-agent monitors list\|get\|cancel` is the operator surface | `agent-app.monitors` |
| Local-first public-web research | config + auto | `tools.web.coordination: "host"` shares private admission/cooldown/quota state across agents under one OS user (default `process`); `mono-agent web-control status` inspects it. Broad primary queries and WebFetch `start_line`/`max_lines` avoid repeated work; Codex keeps a 10% quota reserve. `tools.web.search.maxRequestsPerRun` defaults to four actual provider requests per logical run, with cache/coalescing/cooldown/quota skips free. `tools.web.search.backend`, provider-scoped `searxng.endpoint` / `ollama.*`, and `codex.model` select strict providers or public no-credentials fallbacks; `auto` tries explicitly configured Ollama → configured SearXNG → Codex → keyless. Cooldowns advance immediately and tell the model not to sleep or retry. `tools.web.fetch.{render,browserCommand}` keeps deterministic static extraction as the default and optionally enables isolated agent-browser rendering; `render: "always"` is an explicit browser-first tool call under an `auto` config ceiling, and transient-fetch retry behavior is automatic rather than config-bearing | `runtime.web-research` |
| MCP servers (stdio/sse/http) from a JSON file | config | `tools.mcpConfigPath` | `tool-policy.mcp-servers` |
| Generated reply files and Pi-native MCP Apps | config + auto | `PublishReplyFile` is included under allow-all or must be named in a restrictive `tools.allowedTools`; it publishes confined files through owner-private integrity storage for native Slack/Telegram upload and web download. MCP Apps appear only when every configured runtime route supports the Pi-owned bridge, and render only in the hardened web console. Files and Apps share a 20-part run cap; retention follows `artifacts.retention.maxAgeDays` | `agent-app.rich-replies` |
| Durable origin-bound continuations for trusted stdio/loopback-HTTP MCP services | config + auto | `tools.continuationServers` + `continuations.*`; interactive claims pin a bounded immutable origin snapshot before commit, exact rollover buckets are preserved, v3 state is restart-safe, and unavailable/legacy snapshots use a fixed zero-model fallback | `agent-app.durable-continuations` |
| Adapter-derived send tools for enabled Slack/Telegram adapters | config | auto-available under allow-all once the channel is enabled; a **specific** `tools.allowedTools` must include `SlackSendMessage` / `TelegramSendMessage`; valid `slack.*` / `telegram.*` config and existing adapter allowlists provide credentials and destination bounds; confirmed message posts are idempotently recorded in destination history | `agent-app.adapter-send-tools` |
| Interaction bridge for adapter-send history, structured blocking asks, and MCP progress | config + auto | `interaction.bridge.{host,port}`, `interaction.askUser.timeoutMs` (default 600000; `null` disables automatic expiry), `interaction.progress.enabled`; env `MONO_AGENT_INTERACTION_BRIDGE_HOST`, `MONO_AGENT_INTERACTION_BRIDGE_PORT`, `MONO_AGENT_ASK_USER_TIMEOUT_MS` (`none` disables automatic expiry), `MONO_AGENT_PROGRESS_ENABLED`. It auto-starts for configured Slack/Telegram send tools, when `AskUser` is allowed, when an `interaction` block or interaction env override is configured, or when `interaction.progress.enabled` resolves true and `tools.mcpRequestContextServers` names at least one opted project stdio MCP server. AskUser presents all questions together on web and sequential native controls on Slack/Telegram. | `interaction.bridge` |
| Sandbox on/off + srt engine (Pi-owned tools) | config | `sandbox.mode` | `sandbox.mode` |
| Network policy (none/localhost/allowlist/all) | config | `sandbox.network.{mode,allowlist}` | `sandbox.network-policy` |
| Filesystem scopes (readable/writable roots, deny-write globs) | config | `sandbox.readableRoots`, `sandbox.writableRoots`, `sandbox.denyWrite` | `sandbox.filesystem-scopes` |
| Fallback behavior when srt is unavailable | config | `sandbox.fallback`, `sandbox.unsafeAllowHostProcess` | `sandbox.fallback` |
| Request-scoped policies only tighten, never widen | auto | harness merge | — |

## Channels

| Capability | Coverage | Where | Registry config ids |
| --- | --- | --- | --- |
| Webhook (sync/async HTTP invoke + status polling + optional bearer) | config | `webhook` section; `apiKey` protects invoke/status and is required with non-loopback opt-in; endpoint overrides at `webhook.endpoints[].{model,effort,maxRunMs}`; endpoint `maxRunMs` wins over the `webhook.maxRunMs` fallback and `0` disables that endpoint watchdog | `webhook.http-invoke`, `webhook.run-watchdog` |
| OpenAI-compatible API (/v1/models, /v1/chat/completions, SSE, bearer) | config | `openaiApi` section; sampling fields remain request metadata, while non-default values are ignored with a `runtime_warning` and runtime config stays authoritative | `openai-api.chat-completions` |
| Telegram long polling, chat allowlist, and group trigger boundary | config | `telegram` section; `telegram.groupMode` (`any` or `mention`) and `telegram.stripMentionText` | `telegram.long-polling` |
| Telegram runtime/command/reaction/button/file interactivity | config + code | built-in per-chat `/model` and `/effort` use configured primary/fallback models (no Telegram config key); `telegram.commands[]`, `telegram.reactions`, `telegram.quietHours`; `AskUser`, non-blocking `TelegramSendMessage.reply_options`, and `TelegramSendFile` | `telegram.interactive` |
| Telegram inbound audio transcription | config | `telegram.transcription.{endpoint,model,language,timeoutMs}`; opt-in OpenAI-compatible transcription endpoint for voice notes, audio files, and round-video attachments | `telegram.transcription` |
| Slack (Socket Mode, channel allowlist, mention handling, native runtime controls) | config + code | `slack` section; optional `slack.unfurlLinks` / `slack.unfurlMedia` control native agent message previews while omission preserves Slack defaults; built-in Block Kit controls use configured primary/fallback choices through DM-wide/thread-local `@agent /model` and `@agent /effort`, plus channel-wide workspace commands `/<bot>-model` and `/<bot>-effort` derived from `auth.test.user` (Slack app registration + `commands` scope; no mono-agent config key) | `slack.socket-mode` |
| Slack speaker names (who sent each turn) | config | `slack.resolveUserNames` (default `true`) resolves the sender's display name and handle via `users.info`; requires the `users:read` bot scope; best-effort, so a missing scope leaves turns unnamed instead of failing them; the resolved name is durable (stored turn + memory label) | `slack.speaker-names` |
| Surface awareness (which channel/DM the turn is in) | config | Always on for Slack and Telegram: the Session block states the surface kind, its id, and the per-message character budget. `slack.resolveChannelNames` (default `true`) adds the Slack channel NAME via `conversations.info` and requires `channels:read`/`groups:read`; best-effort, so a missing scope leaves the surface named by kind and id. Surface ids are model-visible by design — pair with an explicit `slack.allowedChannelIds` allowlist if `SlackSendMessage` is enabled. Interactive console turns (web console thread, terminal TUI) are told which console they are on and given the thread's own conversation id verbatim (`web:<threadId>`), so an agent can quote it to host-side tools and operator commands that bind background work (Monitor, process jobs, task records) to that exact thread; cron/webhook/API turns still disclose nothing | `channels.surface-awareness` |
| Slack thread/channel turn context (what was said before the agent was triggered) | config | `slack.threadContext.{enabled,maxMessages,requestLimit,timeoutMs,includeBotMessages}`, defaults `true`/`15`/`15`/`4000`/`true`; reads `conversations.replies` in a thread and `conversations.history` otherwise; requires `channels:history` / `groups:history` / `im:history` / `mpim:history`; one request per turn with a per-channel rate-limit breaker; best-effort, so a missing scope or rate limit sends no transcript instead of failing the turn | `slack.thread-context` |
| Slack global/message shortcuts | config | `slack.shortcuts[]: {callbackId, prompt, channelId?, ackText?, threadReply?}`; JSON-only | `slack.shortcuts` |
| Slack App Home actions | config | `slack.homeTab: {enabled?, headerText?, buttons?:[{actionId, label, prompt, channelId?, ackText?, threadReply?}]}`; `enabled` defaults to `false`, `buttons` defaults to `[]`; JSON-only | `slack.app-home` |
| External channel plugins | config | `channels.plugins[]: { package, id?, label?, config? }`; package must export `createChannelDriver(options)` or a default driver factory | `channel.plugins` |
| WhatsApp (Baileys, QR login, group mention/any triggers) | config | `channels.plugins[].package: "@mono-agent/whatsapp-adapter"` plus plugin `config.{enabled,allowedChatJids,allowAllChats,groupMode,botJids,mentionTextAliases,stripMentionText}` | `whatsapp.baileys` |
| Facebook Messenger (signed Meta webhook + Send API, PSID allowlist) | config | `channels.plugins[].package: "@mono-agent/messenger-adapter"` plus plugin `config.{enabled,allowedUserIds,allowAllUsers,host,port,webhookPath,apiVersion,allowNonLoopback,proactiveMessagingType,proactiveTag}`; the three credentials are environment-only and rejected in JSON — `MONO_AGENT_MESSENGER_PAGE_ACCESS_TOKEN`, `MONO_AGENT_MESSENGER_APP_SECRET`, `MONO_AGENT_MESSENGER_VERIFY_TOKEN` | `messenger.graph` |
| A2A provider (Agent Card, JSON-RPC + REST, streaming, bearer, configurable request-body limit, opt-in durable dispatch identity) | config | `channels.plugins[].package: "@mono-agent/a2a-adapter"` plus plugin `config.provider` (including `maxRequestBytes` and `idempotency.{namespace,stateDir,retentionMs,maxRecords}`), `config.agent`, `config.skill`; `config.enabled` is canonical | `a2a.provider` |
| A2A consumer settings (remote agent URLs, timeouts) and calls | config + code | same A2A plugin entry's `config.consumer`; calls via `sendA2AMessage({ idempotencyKey })` or `createA2AConsumerResponder({ idempotencyKeyForRequest })` | `a2a.consumer` |
| TUI stream endpoint (operator console transport) | config | `tui.{enabled,host,port,basePath,allowNonLoopback,apiKey}`; default on, loopback; `/v1/info` capability-gates exact AskUser and agent-owned cron routes without changing wire schema 1 | `tui.stream-endpoint` |
| Cron jobs (five-field expressions, timezones, stable job-id-seeded `H`; agent-app pins overlap to skip) | config + code | `cron.jobs[]`, including per-job `model` / `effort`; single-job `MONO_AGENT_CRON_*`, or one markdown file per job in `cron.dir` / `MONO_AGENT_CRON_DIR` (default `cron/`); `cron.operatorActions.enabled` / `MONO_AGENT_CRON_OPERATOR_ACTIONS_ENABLED` opt in to authenticated, confirmed, idempotent run-now and runtime enable controls (default off; never rewrites config); queue/replace controls are programmatic-only `startCronAdapter` options | `cron.scheduled-prompts` |
| Cron per-run watchdog | config + code | `cron.jobs[].maxRunMs` or `maxRunMs` frontmatter; programmatic adapter fallback via `startCronAdapter({ maxRunMs })` | `cron.run-watchdog` |
| Per-request runtime model and effort overrides | config + code | `cron.jobs[].{model,effort}`; `webhook.endpoints[].{model,effort}` plus request body `{model,effort}` (request wins); Telegram `/model` and `/effort`; Slack Block Kit selectors through thread-local `@agent /model` / `@agent /effort` and channel-wide `/<bot>-model` / `/<bot>-effort`, all over configured primary/fallback models. Model-only turns inherit effort per route: primary = `runtime.effort`, configured fallback = its pinned effort or provider default, other advertised model = `runtime.effort` only when admitted | `runtime.per-trigger-model` |
| Native final-answer notification for cron/webhook | config | Per job/endpoint `notify`; explicit `notifyConversationId` wins, otherwise inference occurs only with exactly one notify-capable Telegram/Slack/Messenger candidate. With 0 or 2+ candidates delivery is skipped with a warning. Artifact-derived candidates use a 30-second post-scan cache. Telegram/Slack/Messenger artifact commits invalidate it immediately; runs using default synthetic `cron:`/`webhook:` ids do not. Other artifact changes appear after expiry and the next scan. Cron model-exhaustion notices require an explicit `notifyConversationId` and never infer; `notifyFailureCooldownHours` rate-limits them. | `channel.native-notify` |
| Channel message texts / stream tuning (welcome, debounce, ...) | code | channel driver overrides | — |
| Custom transports | config + code | implement `ChannelDriver` and expose it through `channels.plugins[]`, or pass it via `startMonoAgentApp({ drivers })` | `channel.custom` |

## Observability, operator surfaces, composition

| Capability | Coverage | Where | Registry config ids |
| --- | --- | --- | --- |
| JSONL run artifacts (events + summaries; strings capped; non-numeric values under sensitive-looking object keys are redacted; numeric values under matched keys are retained; retained free text is scanned for a closed set of high-confidence credential shapes) | config | `artifacts.dir`, `artifacts.retention`, `artifacts.memoryRetention` | `observability.jsonl-artifacts` |
| Trace-source registry (heartbeat manifests `mono-agent status` reads) | config | `traceability.{registryDir,sourceId,sourceLabel,heartbeatMs,staleAfterMs,globalDiscovery}` | `observability.trace-registry` |
| Phoenix trace viewer (best-effort terminal-batched OTLP exporter; independent local JSONL has bounded terminal snapshots and can lose RAM-buffered events on crash) | config | `observability.exporters` (phoenix entry) | `observability.phoenix-exporter` |
| Operator console (live chat with thinking/tool/telemetry insight, run replay, config view, and owner-authenticated process-job routes when enabled) | cli | `mono-agent tui [--agent <label>]`; agents serve the `tui` stream endpoint by default (`tui.enabled`, loopback); `mono-agent jobs` uses a separate owner capability | — |
| Always-on multi-agent web console (persistent conversations and same-thread quotes, one server-authoritative UUID-bearing Send path with durable read-only receipt recovery, agent-maintained semantic titles with permanent manual-rename precedence, durable process-job cards, live active-agent skill discovery with canonical `$skill-name` insertion, fixed compact/expanded rail, offline-agent filtering that always preserves pinned/selected agents, durable Web Push, exact structured AskUser reconciliation, stable read-only per-cron channels with bounded keyset history, attachments, per-conversation model/effort selection, SQLite-persisted per-agent defaults for new web conversations with one-click config revert, per-run requested/attempted/executed route and effective-effort attribution including nested subagents, remembered process-job/Monitor revival selection, reasoning/tools, hidden telemetry-backed cumulative context usage, cancellation) | cli | `mono-agent web start\|stop\|restart\|status\|logs\|run`; stable cron routes are `/agents/:sourceId/cron/:jobId`, read next-run only from capable agents, and expose confirmed mutations only when the agent has an operator key and `cron.operatorActions.enabled`; process-job cards are source/thread-bound and update through the owner-authenticated operator proxy without a live browser; standalone process-job/Monitor wake turns re-read the thread snapshot while an active Web Send targets the exact owned run or visibly queues on an older operator; fallback reasons and retries are bounded/sanitized before browser projection; skill autocomplete/browse reads the running agent's bounded registry and never sends on selection; bare `mono-agent web` is read-only status/help; default `0.0.0.0:5050`, `--loopback` narrows to `127.0.0.1`; `--theme` selects the shell and `--name <label>` replaces the hostname-derived PWA/tab/rail label on start/restart/run, `--name -` restores the hostname default, and managed selections are persisted; package `@mono-agent/web`; no app authentication, so LAN/Tailnet reachability is the access boundary | — |
| Setup presets (saved answer-sets: generate config + `.env.example` + checklist) | cli | `mono-agent presets list\|show <id>`, `mono-agent init --preset <id> --yes` (the `recipes` command and `--recipe` alias were removed) | — |
| Interactive setup wizard (preset/custom; exact `IDENTITY.md` → `## Role` prompt/outcome; walks model→channels→memory→tools→sandbox→observability; Journal/BuJo explicitly choose Ollama or LM Studio service root/model/dimension/optional auth env using typed discovery and a real probe; macOS starts the background agent and prints manual edit/validate/restart/TUI steps) | cli | `mono-agent init` (no flags, on a TTY; `setup` alias); manual embedding entry still requires readiness probe; flags/non-TTY stay scaffold-only; unsupported platforms use manual configuration/foreground start/ordinary TUI | — |
| Tools reporting + no-tools guardrail (allow-all → `All tools allowed`; explicit empty `allowedTools: []` → `waiting`; unknown-tool "did you mean"; send-tool/channel cross-checks) | cli | part of `mono-agent validate`/`doctor`; the wizard's tools step | — |
| Resolved config view (every field tagged env/json/default) | cli | `mono-agent config` | — |
| Scaffold / validate / start / install-skill | cli | `mono-agent init [--model <ref>] [--fallback <ref> [--fallback-effort <provider-default\|level>]]... [--effort <level>] [--auth]\|validate [--consumer <path>]\|config\|presets\|start\|install-skill [--target claude\|codex\|both] [--force] [--no-docs-mcp]`; the legacy CLI `--fallback-models <csv>` flag was removed (JSON/env compat inputs remain) | — |
| Preset capability check (selected preset live?) | cli | `mono-agent validate --preset <id>` | — |
| `.env` auto-loading | cli | automatic; `--env-file <path>` | — |
| Explicit failure objects (no fake success) | auto | harness | — |
| Per-request runtime options, custom memory/history stores | code | `createConfiguredAgentResponder` options | — |
| Multi-agent delegation (`AskCollaborator` loopback MCP tool) | code | `@mono-agent/agent-orchestrator` | — |

Prompt-cache diagnostics: set `providers.piNative.promptCacheDiagnostics` (default
`false`) or `MONO_AGENT_PI_PROMPT_CACHE_DIAGNOSTICS` to retain metadata-only request
fingerprints in existing run artifacts. Use the framework script
`scripts/summarize-prompt-cache.mjs` for token-weighted cache ratios and fingerprint
changes; see [measurement](https://mono-agent-docs.vercel.app/runtime/prompt-cache-measurement/).
