Skip to main content
Mirrored from Meerkat Meta main at 26f5ae9da1fd on 2026-11-03; Meta version 0.9.9 at that commit. Meta ships a web-based operator console through its HTTP composition. The console/admin meta_gateway serves it directly. A persistent, SDK-driven rpc_gateway also mounts the console routes, but the two binaries are not interchangeable: SDK hosts drive rpc_gateway over stdio, while meta_gateway accepts its init handshake and then serves console/admin HTTP. The console provides real-time visibility into agents, conversations, events, logs, approvals, and recovery actions. The stock shell is built from shared @console-core and @console-components packages, while app-specific policy stays in runtime config and host code.

Accessing the console

Once the runtime is serving HTTP, open the console at:
Both gateway binaries bind an available loopback port and report the exact http_base_url in their init result. A Rust library host chooses its own bind address. http://localhost:8080/console is correct only when your host or reverse proxy explicitly publishes Meta there. The HTML, JavaScript, and CSS are compiled into the binary via include_str!, so no build step is required at deployment time. The stock CSS carries its fonts, Inter Tight and IBM Plex Mono (SIL Open Font License 1.1), inline as data: URLs, so the console requests no font from another origin and looks the same on a host without internet access. A Content-Security-Policy in front of the console that restricts fonts must allow font-src data:. For development with instant hot-reload, see the development workflow, including the current Vite proxy limitations:

Developer custom panels

Products embedding Console can add panels directly to its sidebar. A panel is trusted application code and is independent of MCP Apps inline widgets.
A mount owns its container and may mount React, Vue or another framework inside it. Return dispose() to release resources, and optionally update(context) to receive context changes without remounting. The host aborts the mount’s signal on disposal. A failing panel shows a local fallback while stock views remain available. Keep the registration array stable across React renders. For the stock served bundle, list developer-hosted modules at the top level of console.toml. Each module’s default export is an array of panel definitions:
The application or reverse proxy serves the module with a JavaScript MIME type on the Console origin. Relative paths resolve against baseUrl. Realm overlays can replace the list, including panel_modules = []. Reload after changing it. This is a frontend loading option; tools do not supply these modules. The application host owns module integrity and should serve versioned, audited assets. Each panel needs a unique namespaced id, a title, and a mount function. Console adds its sidebar button and pane-menu entry and owns tabs, splits and layout restoration. Removing a registration leaves a restored unavailable placeholder. It does not execute missing code or call a tool. ConsolePanelContext supplies baseUrl, experience, readOnly, the host’s authority, current selection, and the panel’s pinned conversation. Use openPanel(id, { intent: "split_right" }) for another registered developer panel. Optional instanceKey, JSON params, and a pure validateParams function allow multiple instances. Layout storage contains selectors only: never put private results or secrets in panel parameters. Set followSelection: true to follow the selected conversation instead of pinning it. Context and pending requests are discarded when the host authority changes. Layout keys include the authority and principal scope; opening and restoring conversation panels checks current visibility. Panels can use context.request({ path, method, body }, signal). The default service uses same-origin authenticated requests, rejects redirects, blocks non-GET operations in view-only mode, and never retries a request. It sends X-Console-Authority and X-Console-Conversation as URI-encoded JSON locators. These headers are not authorization proof: the application must derive the viewer from its authenticated session and enforce access on every request. Panels are trusted application code, so browser checks do not replace server policy. Embedded hosts may inject panelService(request, scope, signal) to use their existing application service, forwarding the scope locators through its own authenticated API. The host still applies view-only and lifetime checks. The request scope includes the original panel conversation, not whichever conversation happens to be focused later. See examples/console-extensions for a module and local stock Console preview.

Inline MCP Apps

Inline widgets use the MCP Apps standard. An MCP server supplies tool metadata, a ui:// HTML resource, and the original tool result. No panel module or Console-specific app manifest is needed. Enable the stock host in config/console.toml:
Run the sandbox on a separate origin, with the exact Console origin allowed:
Both gateway binaries embed the sandbox proxy, so a released gateway is all you need to run it outside a source checkout. Have the gateway write it into a directory, then start it with Node.js. It uses only the standard library, so it needs no npm install:
--write-mcp-apps-sandbox <dir> writes two files, creating the directory and replacing earlier copies, prints each file’s SHA-256 and path, and exits without starting a gateway: The proxy loads src/mcp-apps/sandbox.js relative to its own file. The two files and the Console embedded in the gateway share an internal sandbox protocol, so write them again from the new binary whenever you upgrade the gateway. The Python package and the npm package do not include the proxy. Gateways before 0.9.2 do not embed the proxy. For those, copy the two repository files from the release tag that matches your gateway into one directory, keeping the layout above, and check your copies against these SHA-256 digests (identical for 0.9.0 and 0.9.1): The originating member profile selects its MCP servers through tools.mcp_servers, or uses its existing host tool provider. Global server configuration alone does not grant every member access to those tools. The bundled gateways enable native MCP Apps negotiation when this sandbox URL is configured. Rust library hosts opt in with UnifiedRuntime::builder().mcp_apps(true) and set ConsolePolicy.ui.mcp_apps_sandbox_url on their Console policy. Hosts that create their own external MCP connections must also negotiate the standard UI extension, for example with McpAppsClientServiceFactory; this cannot be added after a connection’s handshake. Headless hosts omit the capability. An unavailable widget retains its ordinary text tool result. The host retains the original tool, arguments, complete result and available UI resource with the canonical session. The declared HTML is preloaded before tool execution; the app receives invocation inputs and results through MCP Apps messages. Reopening a retained view never repeats its original tool call or waits for the current agent turn. The resolved canCallTools capability describes access to the live host action surface; actual calls still require fresh native admission and the original connection. During a continuing model turn, an actor- and run-bound native observation exposes accepted tool results for display without changing committed history. Completed-run replay uses the runtime’s committed session. These observations carry no action authority. UI-only _meta is excluded from model requests and the public transcript. Timeline frames carry locators, not private HTML or metadata. Every lookup rechecks the authenticated viewer and session ownership. Uncached resource reads and app actions also require agent.send, an editable Console and the original live MCP connection with negotiated MCP Apps; replacing or retiring it cannot silently redirect an old widget. View-only users can read the exact retained renderer without fresh member-credential IO. Disabling the sandbox URL also disables the Console’s app HTTP operations. Tool visibility follows _meta.ui.visibility: omitted permits model and app, ["model"] permits only the model, ["app"] permits only the app, and [] permits neither. A model-only launch tool can still own a view. App actions use fresh native authorization, pre- and post-tool hooks and normal execution policy, including review when required. A post-hook refusal withholds the result while preserving already settled effects. View-only users cannot invoke them. App resource reads and tool execution can proceed while the model turn is still running. A response with no session effects or hook notices does not wait for that turn or trigger a session save, so several mounted widgets can poll for fresh data. If the call produces session effects or hook notices, the native actor applies and persists them before publishing the response; this settlement can wait for the active turn. This distinction follows the actual execution outcome, not a tool’s readOnlyHint. Each request still requires fresh admission and the original actor and MCP connection. The current host profile supports inline rendering, resource reads and tool calls. It does not advertise conversation handoff, model-context updates, external links or downloads. The sandbox restricts fetches and frames through CSP and denies sensitive browser permissions. It rejects CSP grants covering configured Console hostnames. Browser support limits network containment: Chromium 140 ignores the emitted standard webrtc 'block' directive. Navigation retirement is best-effort lifecycle handling, with every fresh action still subject to native admission. Serve the sandbox without Console cookies or application APIs; production hosts should use a dedicated HTTPS origin. An ordinary MCP App can render a versioned generated document from its result data today. The app supplies the renderer; the runtime does not impose a component language. Keep generated content inside the app sandbox, separate from trusted developer panels, and route external actions through the same native admission path. Streaming updates and conversation handoff require their own explicit lifecycle and provenance contracts when added.

Runtime contract

The bundled console uses a small set of stable protocol surfaces: /console/modules returns IDs from RuntimeDecisionState.modules. It is a configuration inventory, not confirmation of successful startup, current liveness, or health. The console contract version is 0.5.0 in console/src/lib/contract.ts and docs/rct/console-rest-sse-contract-v0.5.0.json. The runtime JSON-RPC contract is separately versioned by META_CONTRACT_VERSION. Gateway custody also differs. rpc_gateway installs a SQLite console timeline store only when its init request configures persistent_state; long-lived --persistent process mode alone does not make the timeline durable. Without persistent state, its timeline stays in memory. The console-only meta_gateway also keeps its timeline in memory. Do not infer timeline durability from the presence of the same browser shell and route names.

Layout

The console uses a configurable workbench layout built on the shared ConsoleWorkbench component:
The left column is navigation. It can show stock controls such as topology, roster, logs, health, and approvals, plus app-defined buttons. Clicking an agent opens chat; details and profile-style data belong in roster surfaces. Agents can be grouped by configured selectors. The default useful pattern is labels first, then structural fallbacks:
A sidebar button can open a stock control, link to an href, or dock an operator panel. A panel is a same-origin path (one leading /, at most 2048 UTF-8 bytes, no whitespace, control characters or backslashes, and no . or .. segment in any spelling, %2e included) that the console shows in a docked frame. The path is used exactly as written; surrounding whitespace is refused, not trimmed. It cannot be combined with control or href; an invalid button is dropped.
An operator panel is trusted code, not an isolated frame. A same-origin page runs with the viewing operator’s console session and can do anything that operator can do in the console. Configure only pages you would trust with that session. The path comes only from static operator configuration, and the console reads it from the current configuration whenever the panel opens.
The console and flow editor documents are served with Content-Security-Policy: frame-ancestors 'self', so only their own origin can frame them. A deployment that frames the console from another origin must change before Meta 0.9; same-origin framing (for example behind a reverse proxy on the console’s origin) and top-level use are unaffected. A panel page that a reverse proxy serves outside the gateway must send its own framing policy; the gateway’s header does not cover responses it does not serve. Operator panels depend on a few host constraints:
  • The console sets no frame-src restriction, so same-origin panel paths stay framable. A host’s own same-origin review page can be configured as a panel.
  • The panel frame is not sandboxed, so a host page’s form posts keep their real Origin. A sandbox without allow-same-origin would send Origin: null and break same-origin form admission.
  • A page served with frame-ancestors 'none', such as a secret-entry page, cannot be framed and must not be configured as a panel.
Each agent row shows:
  • The agent’s display name or, for an agent without one, its identity in words (router:main reads “Router main”; a generated id reads as the agent’s kind and the id’s last six characters, such as “Reviewer 4e5f60”)
  • A second line with what the agent is (its role, else its group) and what it is doing in words, or how long ago it was last active, for example Router · working or Router · active 3 min ago
  • A pin action on hover, and beside it, when you point at the pin or reach the row with the keyboard, a copy action: pinned agents move to a Pinned section at the top, and the copy action copies the agent’s identity, which the row’s tooltip also shows

Ordering and grouping the roster

Two controls under the search box set how the roster lists agents:
  • Group: Category lists agents under their categories and subgroups (the [agent_list] grouping above); None lists every agent in one list. Either way, Pinned stays on top and an agent spawned by another, or a side chat, stays nested under it.
  • Sort: Manual keeps your own order; Name sorts by display name, with numbers in order (Agent 2 before Agent 10); Last activity lists the most recently active agents first and agents with no known activity last.
In Manual, drag an agent by the grip that appears at its left edge on hover, or focus the agent and press Alt+↑ or Alt+↓; a screen reader hears each move. An agent moves among the agents that share its place: its category and subgroup, or the agent it is nested under, and its nested agents move with it. One order serves every view: it orders each category, the flat list, and Pinned. Until you move an agent, each category keeps its default order and the flat list follows the categories in their order; the first move keeps that order for every agent, and a move changes only where the moved agent stands, in either view. A new agent appears at the end of its place, and a renamed agent keeps its place, since the order is kept by identity. Agents do not move while you sort by name or last activity, or while a search or an activity filter is on. Categories and subgroups reorder the same way: drag a header, or focus it and press Alt+↑ or Alt+↓. Last activity is the newest time the console’s frame log recorded the agent acting (a live runtime event, a message sent to it or a tool application), which /console/experience reports as last_activity_ms, or a newer event the console has since seen. History the console rebuilds, lifecycle records (respawn, retire, reset, delete), interaction failures and memory events do not count. Reading it wakes no agent. The browser keeps these preferences in local storage, per signed-in user and console: the manual order, the Group and Sort choices and the pins follow the user and are kept when the [agent_list] configuration changes. The order of categories and subgroups and which of them are collapsed belong to that configuration and start over when it changes. Pins an earlier console kept carry over once. Non-addressable agents (e.g., Gate, Health Monitor) appear dimmed and cannot be messaged. An identity-first agent whose identity binding moved (after a reset or a delete), and that the console could not collect again within 2 s, has state unknown and offers no send or retire until a later read collects it. Its records carry an unobserved marker with reason refresh_pending (see GET /console/experience). The topbar shows the mob as unknown, with an amber dot, when the health snapshot does not say whether the mob runs; a stopped mob’s dot is red.

Approvals

Open Approvals from the sidebar or Needs you to inspect current requests. Needs you also counts host-provided attention items when the console configuration names [[attention.sources]]. The inbox lists them after the approvals, as cards with one Open link to the host’s page, which owns the decision. A source that cannot be read is named and hides nothing else (see host-provided attention items). A source with a decide_href gives a read-only console a Decide in link on each pending approval, in the inbox and in an agent’s chat, to the host page that decides it. Expand Complete request details before deciding. Stale or unavailable approval data does not mean there are no pending requests; use Refresh approvals to reload it. Read-only consoles cannot submit decisions. Where the connection offers no approvals (a gateway without meta/gating/pending, or an aggregate console, whose inbox lists host attention items only), the Approvals header reads Not available on this connection instead of pending and automatic counts. Risk tiers display in words with their code: R0 Minimal, R1 Low, R2 Elevated and R3 High. A request’s chip reads “High risk”, with the tier code in its tooltip, and an audit row reads “Risk: High (R3)”. A missing tier reads “Unknown”; an unrecognized one reads “Unknown” followed by the recorded value. The current Meta R2 path normally allows with audit. It does not run the proposed native model reviewer. An approval record does not confirm execution: the caller remains responsible for enforcing the returned outcome and carrying out any permitted action. See Gating for current tier, timeout, and decision behavior. The stock console submits decisions with approver_id: "console-ops-lead". There is no approver selector in this inbox. A request pinned to a different approver needs a host-provided approval surface that authenticates and maps the approver. Configure action tiers through the host’s gating configuration. For SDK-launched stdio gateways, see gating_config_path. That option does not configure unrelated HTTP or library entrypoints.

Dock — Chat panels

The center area is a multi-panel tabbed workspace. Each panel contains a ConversationPane with a ConsoleComposer. Tab strip at the top:
  • Click a tab to switch panels
  • + to create a new empty tab
  • × to close a tab
Split panels — click the split controls (← → ↑ ↓, visible on hover) to divide a panel. This lets you chat with multiple agents side-by-side. Composer at the bottom of each panel:
  • Textarea with Message {agent}... placeholder
  • Send button (↑) — also supports Enter to send, Shift+Enter for newline
  • Footer shows: target agent, member ID, profile, and status

Side chats

Fork to side chat in a conversation’s header branches the agent’s conversation into a side chat: a new member that starts from the agent’s committed transcript and opens in a pane beside the agent. You continue the side chat on its own; the agent’s session, transcript and queued work stay as they were.
  • The side chat runs with the agent’s role, profile and model overrides and tools, and inherits the agent’s labels.
  • Its audience is exactly the agent’s: every agent check on a side chat is the agent’s, so whoever may see the agent, send to it or retire it may do the same with its side chats, a deny on the agent hides them, and a grant on the side chat’s own name grants nothing (see Side chats).
  • It is never wired to a peer, whatever the agent’s role: no other agent can address it, and none lists it as a peer. The runtime seats it exempt from the mob definition’s wiring rules (auto_wire_orchestrator, role_wiring), so no agent seated after it is wired to it either, and the definition-wiring reconcile and the topology’s declared edges leave it out, as they leave out every member exempt from those rules. A host’s own edge discovery policy is never handed one either.
  • It is listed under its agent in the sidebar, marked Side chat, and is kept until you close it: across a console reload and a gateway restart. It never idle-retires. Close side chat in its pane retires it, which, as every check on a side chat, needs agent.retire on its agent; the agent is not touched. meta/reset_all retires side chats with the other members that have no registered identity. A host that reconciles the whole mob against a declared roster (UnifiedRuntime::reconcile, without identity-first authority) retires them too, since no declared roster names them; the stock gateway does not.
  • A side chat is never respawned or reset: its pane offers neither, and meta/respawn, meta/respawn_member and meta/reset refuse it (side_chat_restart_refused), since its successor would be no fork of the agent. An agent’s mob_respawn tool refuses it too, and meta/reset_all reports it as a member it could not reset where a host declares a baseline member under its name. Close it and fork the agent again.
  • A side chat is never adopted as an identity: mob/adopt_member_identity_declaration refuses it (side_chat_adoption_refused), since an adopted identity is rebuilt from its declaration, which no fork made.
  • The action is offered only to a viewer who may spawn members (agent.spawn) and send to the agent (agent.send), and only where the gateway’s sessions persist or its composition did not record whether they do. It is disabled while the agent works and enabled when it is idle. An agent seated and never woken has no conversation to fork: the fork is refused, and the agent is not woken for it. An agent the console’s visibility policy hides cannot be forked, nor can an agent whose lineage already fills the access rules’ walk (the agent and seven ancestors), by any viewer, an admin included.
  • Under runtime_options.member_auth_binding, an agent whose session records another binding or none (one created before the launch named the binding) cannot be forked: the runtime would seat the side chat off the binding. A side chat forked before a launch named the binding is refused at its rebuild under that launch; close it and fork again.
  • Instructions the agent activated (meta/member_activate_instruction) are inherited when they are in effect in the agent at the fork. Any other inherited instruction stays inactive, and the side chat’s pane names it: the side chat cannot answer until a host activates it.
The fork takes the agent’s conversation as it is now. Side chats are meta/fork_member on the console RPC.

Schedules

An agent’s details pane (Details in the roster, or Inspect agent in its conversation’s header) lists the agent’s schedules: messages the Console sends to the agent later, as the person who defined them. A viewer who may message the agent adds one with New schedule: the message, an optional name, and when to send it, either once at a date and time, repeating every so many minutes, hours or days (the first send one interval from now unless you set a start), or daily at a time of day, on the weekdays you pick, in a named time zone. Each schedule shows when it sends, its next send while it is active and, to a viewer who may message the agent, who it sends as, with Edit, Pause or Resume, and Delete, which asks you to confirm. An edit keeps the timing unless you change it.
  • Each send is a Console send by the schedule’s sender, judged when it goes out: the Console must still admit the sender as it would admit them then (sign-in where it is required, the allowlist, the trusted issuer, and no sessions revoked since the schedule was defined), and the send needs what a message sent then would need, agent.send on the agent under the access policy at that time, a writable console, and an agent that takes console messages. A refused send fails that time only; the schedule keeps its later times.
  • Creating, editing or resuming a schedule makes you its sender. On a console without sign-in the sender is the console’s shared account, as it is for a message you send there; once the console requires sign-in, such schedules stop sending until someone signed in edits them.
  • A change that races another change of the same schedule is refused and the list is read again: check the schedule and repeat the change.
  • A schedule repeats at most once a minute.
  • Viewers who may see the agent but not message it see its schedules read-only, and so does everyone on a read-only console or on a gateway that does not offer schedule changes; the pane says why.
  • An agent whose input is governed takes no schedule (a governed send needs its human present, and a scheduled send has none), and an agent that takes no console messages cannot be scheduled.
  • The pane lists the schedules defined in the Console only. The agent’s own schedules (its schedule tools) are not listed, and the agent’s tools neither see nor change the Console’s.
The section appears where the gateway keeps console schedules. A gateway that sends none of them (a candidate launch, for one) lists them read-only. Schedules are the meta/schedule/* methods on the console RPC (see Console schedules).

Local action feedback

When a host supplies a recognized native refusal or audit fact, the Console shows feedback for the affected action. A permitted sibling or later reply can still appear in the same run. Custom workspace hosts should retain ConversationMessageEntry.operationFeedback when transforming shared timeline entries. The reusable ConversationTranscript renders this metadata automatically. A custom renderer can display its title, detail and supplied toolCallId or operationId; preserve those identifiers verbatim. The shared projection classifies supported typed payloads, rather than interpreting arbitrary tool messages as decisions. Cards cover recognized current typed payloads. An unknown cause or malformed payload does not acquire a permission or confinement classification. Unrecognized tool results use the ordinary tool-result display and may show raw output; no dedicated fallback card is promised for every event. A missing card is not a permission decision or evidence of success. Preserve unfamiliar payloads in the host’s authorized diagnostics. Keep run state attached to the controller’s runtime lifecycle events. A local feedback card does not end a run. Rendering these cards does not configure native authorization, process confinement or durable audit; the host supplies those runtime capabilities separately.

Voice conversations

When the gateway has an authenticated OpenAI Live configuration, a blue waveform button appears beside Send. Voice is not offered without OpenAI authentication, in a view-only console, or for an agent you cannot message. Microphone access requires HTTPS or localhost and your browser’s permission. Source builds must enable the openai-live Cargo feature. Public GPT Live requires a compatible OpenAI API-key binding; a ChatGPT OAuth login alone does not enable this feature. Voice is an explicit gateway registration, not a setting in console.toml. For meta_gateway, supply console_voice in the init parameters alongside your auth_config and enable persistent_sessions:
The optional summary block bounds the concurrent context summary (see “Context preparation” below). Every field is optional: model defaults to the background agent’s own text model, max_input_bytes to 65536 (64 KiB) and max_output_bytes to 4096 (4 KiB); zero is rejected. The input bound degrades rather than fails: when the newest message alone exceeds it (a large tool result, say), that one whole message is summarised; only Meerkat’s 4 MiB capture ceiling refuses a snapshot outright. In Rust the same knobs are PublicLiveRegistration.summary (ConsoleVoiceSummaryConfig); the SDK builders take them on OpenAiLiveGatewayConfig:
The binding must already be configured in the gateway’s Meerkat host config. The principal must match the authenticated console subject. For an SDK-driven rpc_gateway, place the same console_voice registration under runtime_options and use the gateway’s persistent session configuration. The SDK builders expose this as .consoleVoice(config) in TypeScript and .console_voice(config) in Python, using OpenAiLiveGatewayConfig. A Rust host that embeds UnifiedRuntime through its builder registers the same thing with .console_voice(PublicLiveRegistration) (plus .live(...) for the external channel) and serves build_reference_app_router_with_live(decisions); the decision state must require app auth, for example parse_console_auth_config with provider: "oidc" for a Google-fronted console. See the Unified Runtime guide. Credentials stay on the server. The console checks readiness for the selected agent before offering voice and checks it again before microphone capture; registration or model availability alone does not enable the button. Click the waveform button to start a gpt-live-1 conversation with the selected agent as its background agent. Audio connects without waiting for a summary of that agent’s context; it does not change the background agent’s text model. The voice strip above the composer names the voice agent and shows separate waveforms for your microphone and the live model’s audio. The microphone is requested during the click, alongside the gateway readiness check (Allow microphone access); a negative readiness releases it again without opening a channel. Startup then distinguishes Starting voice from Connecting audio. As soon as the microphone is open the primary status reads Listening, you can talk; nothing about context gates it. Context preparation is a secondary hint beside that status: Context arriving while the initial summary is read, generated, and sent (the tooltip names the stage), then Context supplied once the provider acknowledged it, which does not mean the model has recalled or spoken it. Context failures stay visible without disabling the connected audio; a failed status read is shown separately and retried. Context preparation is a separate, tool-free summary request using the background agent’s credentials; it does not change that agent’s model or reasoning settings. It reads the most recent 64 KiB of the serialized transcript (older turns are dropped whole; a size bound, not a content heuristic; a newest message larger than the window is kept whole), asks for compact notes rather than replayed logs, uses low or disabled OpenAI reasoning where the model catalog supports it, and caps the result at 4 KiB. The public GPT Live transport delivers the summary in UTF-8 fragments of at most 500 bytes, each waiting for its own provider receipt, so the 4 KiB cap bounds delivery at nine fragments; the prompt itself aims for about 200 words. The summary model defaults to the background agent’s own model because Meerkat’s catalog exposes a support tier, not a speed tier; set summary.model to a faster model on the same provider when the agent’s model is slow to first token. A produced summary is cached per member, keyed by the source session’s transcript cursor and a digest of the exact summarised window, so reopening voice on an unchanged transcript reuses it without another model call. The defaults come from measurement: with the previous 4 MiB input and 16 KiB output bounds a 40-turn session took 4 to 6 seconds to summarise; the 64 KiB window keeps that cost flat as histories grow. Meerkat orders the captured history and newer conversation updates, and cancels or fences context work when the call closes or changes. A context job from an old call cannot update a replacement call. The microphone and speaker controls are independent. Muting speakers silences local playback, not the conversation; the model’s output waveform continues to show incoming audio. End the conversation with the close button. Voice stays with its original agent when you select another chat, switch tabs, or open an operator panel. Starting voice on another agent ends the previous voice conversation before opening the new one. Text composers remain usable throughout, including for the agent currently in voice mode. A gateway may also register live: true beside console_voice so an external client (for example an embodiment daemon opening meta/live/open over {base}/live/ws) shares the same live host. The two take turns: the newest engagement wins. When the external client opens a channel, the console call ends with “Voice moved to the external live channel”, and readiness for the console reports available: false with reason: "external_live_active" and the holder until that channel closes. Starting voice again from the console takes the path back and closes the external channel with a typed superseded_by_console_voice reason. With GPT Live client delegation, typed messages go to the background agent; the composer labels that destination during voice. Its canonical context and results are mirrored into the voice conversation. This follows OpenAI’s typed-input contract: typed text is user data for the backend, not a live-model instruction or a native spoken turn. Chats with other agents do not change the voice target. For local session-backed agents, console messages use explicit conversational user admission, preserving the console interaction identity in canonical history. Internal events and recalled memory are not relabeled as human input. Voice ends automatically after 15 minutes of silence, not after 15 minutes of total call time. Text messages do not extend that audio inactivity window. Ending voice or closing the console releases the browser’s microphone and playback resources. The close button immediately mutes microphone transmission and playback, retaining the muted WebRTC transport for up to five seconds while the server finalizes. Transport and capture resources are then released even if finalization fails; an unconfirmed close remains retryable. If Meerkat provides a recovery connection, the console reconnects to the same voice agent without changing your mute settings; unrecoverable audio connection loss ends voice.

Voice acceptance tests

The offline browser lane uses real local WebRTC peers without paid provider calls. The opt-in live lane starts a separate authenticated console and uses real gpt-live-1, real gpt-5.5 background/peer calls, and original synthetic speech fixtures instead of capturing a person’s microphone:
The live runner builds the voice-enabled gateway through scripts/repo-cargo. Set META_VOICE_GATEWAY_BIN to an already-built current gateway to reuse it. Missing credentials fail the selected live lane; they do not produce a skip that looks like a passing acceptance run. The runner does not use an existing console or record microphone input. Coverage includes spoken requests, typed-value recall, peer results arriving after an initial answer, and close/reopen while background work remains pending. Expected native transcript content must have fresh non-silent audio nearby; packets, old audio peaks, or mirrored voice text masquerading as a background answer are not sufficient. Late ordinary background facts may reach the new voice conversation through current canonical context, but old receipts must not control it. The public API does not expose pending canonical-mirror acknowledgements. Exact close while an append awaits its provider acknowledgement is therefore covered separately by deterministic Meerkat owner tests. The paid console lane does not infer that hidden state from an unfinished background task. The optional summary-latency benchmark makes three billed gpt-5.5 calls and checks a five-second summary budget while retaining exact context values:

Signals rail - event feed

The right column shows user-meaningful signals: requests, assistant replies, peer messages, image events, relevant tool activity, and alert levels. Low-level transport envelopes, scaffold prompts, raw checksum tokens, and raw tool metadata are filtered out before reaching the user-facing view.

WorkGraph panel - Tree and Graph views

The WorkGraph panel (sidebar nav entry, visible when the runtime wires a WorkGraph service) reads meta/workgraph/snapshot and offers two views of the same data via the Tree | Graph toggle in the panel head. Tree is the default: an indented item list with claim/close actions plus the attention-binding and recent-event sections. Graph draws a read-only layered DAG - roots leftmost, one column per parent-edge depth, solid arrows into parents, dashed amber blocks edges, node borders/dots in the item-status palette (pulsing blue in progress, green completed, red failed, amber blocked, struck-through cancelled). Drag to pan, wheel to zoom, Fit resets, clicking a node shows its id/status/owner detail underneath. The view caps at 200 nodes and reports the overflow; all mutations stay in the tree and attention sections.

Agent grouping

Agents are grouped in the sidebar by console_config.agent_list. Selectors support labels.<key>, label:<key>, raw label keys, and direct fields such as group, subgroup, role, kind, identity, member_id, and agent_id. Prefer mob member labels for domain-specific grouping:

Well-known labels

Console configuration

The stock console can be shaped by config/console.toml in the conventional workspace layout. meta_gateway loads that file automatically; rpc_gateway accepts runtime_options.console_config_path, and the TypeScript builder exposes .consoleConfig("config/console.toml"). The parsed config is projected through GET /console/experience as console_config.
Keep this config view-level. It should decide presentation, ordering, visibility, labels, defaults, and links. It should not decide runtime authorization, routing, or whether an agent exists.

CSS customization

The console uses the shared @console-components stylesheet with --cc-* CSS custom properties. Override these on a wrapper element with data-cc-theme="dark" (or "light").

Key tokens

Theming

Without configuration or saved preferences, the stock console defaults to light mode and the Rams variant. console_config.appearance can supply different defaults; saved user preferences take precedence. Shared-component hosts select light or dark mode with data-cc-theme="light" or "dark" on their .cc-theme-scope root. The visible console canvas and sidebar surfaces use theme/variant tokens rather than fixed colors. For example, #131316 is the graphite-dark canvas color, not a universal body background. The shared stylesheet’s fallback sidebar safe-top and left/right padding are 10px and 12px/12px; the stock host overrides them with the values in the table.

Resizable panels

Both the sidebar and activity rail are resizable by dragging the edge dividers:
  • Sidebar divider — drag the right edge of the sidebar (min 180px, max 420px)
  • Activity rail divider — drag the left edge of the activity rail (min 200px, max 480px)
The resizers use pointer capture for smooth dragging and show a subtle glow line on hover.

Experience metadata

The console fetches its initial state from GET /console/experience, which returns agent data, topology, and health information. The key field for the sidebar is agent_sidebar.live_snapshot.agents:
Identity-first status and inspect payloads use lowercase wire enums for addressability: "addressable" and "internal_only".

Messaging protocol

1

Seed the timeline

Panels load recent visible frames with GET /console/timeline?identity=identity:alice&mode=recent&limit=.... Older history is paged with before; live continuation uses latest_cursor.
2

Send message

Identity-addressed panels call meta/console/send through POST /console/rpc:
Returns { "interaction_id": "...", "identity": "..." }.origin is the caller’s free-form identifier for audit and routing. origin_kind is optional and typed: operator, operator_probe, connector, scheduler, policy, flow or system. It is stored on the user_input frame, and the transcript labels the turn from it (“Operator”, “Operator probe”, “Scheduled turn”, …). Any other value is refused. Automated checks such as release-gate probes should send operator_probe; the stock console sends operator. A declared origin_kind is part of the idempotent request, so a retry must repeat it.idempotency_key must be unique per send; the stock console mints one UUID per send. An idempotent retry reuses the same key in the same caller/runtime/identity/origin namespace with identical content and effective handling_mode (default queue). The caller is the authenticated principal (anonymous callers of an open console share one local account), so another user’s identical key is a different send. An open console without an access controller does not resolve tokens, so all of its callers share that one account; isolating users needs require_app_auth or an access controller. The original acceptance is returned and the turn is not run again. Changed content or handling mode in that namespace is rejected with HTTP 409 / JSON-RPC -32009 idempotency_conflict. A different identity or origin is a different send namespace, not a guaranteed replay.A retry of a send the gateway accepted but stopped before submitting is dispatched then, once, into the session the send recorded; a retry of a send the runtime already admitted only settles it. meta/console/recover_send (POST /console/send/recover) reports a send’s status and what the runtime holds for it, for the caller’s own sends only.skill_refs is optional: typed, source-pinned skills selected for this turn ([{"kind": "structured", "source_uuid": "...", "skill_name": "..."}], the same shape as meerkat turn/start). The addressed member resolves them natively and commits a durable SkillContext; the console never renders skill bodies into the message. A nonempty selection is part of the idempotent request: a replay must repeat the same selection in the same order, and a changed selection under the same key is -32009 idempotency_conflict. Only local session-backed members accept a selection.The legacy POST /console/send route still exists for compatibility. New console clients should prefer JSON-RPC.
3

Receive response frames

The timeline stream delivers canonical console frames:
This is the SSE wire envelope: type wraps a frame with kind and payload. The frontend normalizes these into its internal event/data representation; that normalized object is not the SSE wire shape.Correlate a send with its terminal by interaction_id on frames whose source.kind is console_event. Session-history backfill re-emits past turns as frames with source.kind = "session_history"; they carry the persisted interaction id of the turn they replay and are never fresh completions, so clients MUST exclude them from live turn handling (the rule is stated with the frame shape under meta/console/query_timeline in the JSON-RPC reference).If a requested cursor is no longer replayable, the stream returns 409 replay_unavailable; refetch a recent page and resume from its latest_cursor.A replay from a cursor is sent as it is read, one page at a time. If the log stops holding what follows while the stream replays (it was cleared), the stream sends a replay_unavailable event and ends: handle it as the 409. If a later page cannot be read, the stream ends after the frames it sent: reconnect with the last frame’s cursor as Last-Event-ID.

Console routes

Primary RPC methods on /console/rpc:
  • meta/console/send - identity-addressed console send
  • meta/console/query_timeline - timeline replay query
  • meta/console/list_identities and meta/console/inspect_identity - identity-first inspection
  • meta/blob/upload - multipart blob upload
  • meta/reload_member - non-destructive cold reload of one identity’s live member (same session, same generation); the repair for a member whose sends fail with the reload-required class. Gated by the agent.respawn action. meta/member_health is the matching read (agent.view): lifecycle state, last delivery error class, and the actor-loop probe verdict with the open stall_id, from in-process reads that answer during a stall.
  • meta/identity/warm - builds one identity’s member without a turn and writes its tool record, with the result and error codes of the stdio gateway’s meta/identity/warm. Gated by the agent.respawn action.
  • meta/retire, meta/respawn, meta/reset, and meta/reset_all - operator lifecycle actions. meta/retire releases the member, which stays seated on its session at its generation. A member that no identity holds and no host adopted, such as a side chat, is retired by Core instead: it leaves the roster with the members it spawned. reset_all resets every identity the identity runtime knows, whatever its state (generation advance, one identity_reset lifecycle frame each, listed under reset): each reset is Core’s rotation, which seats a dormant identity Core holds no member for on a fresh session too, and starts a broken one over. It retires only members with no registered identity, such as raw delegates (listed under retired_delegates); a member that is retiring appears in no list. A registered identity on a runtime without a session bridge fails the preflight with identity_reset_requires_session_bridge and nothing is touched. An identity whose own reset fails is listed under failed, the identities beside it are still reset, and the call answers -32000 with the full body in error.data
  • meta/send_message - legacy member-addressed send path
  • meta/fork_member - forks an agent’s conversation into a side chat (see Side chats); gated by agent.spawn for the side chat and agent.view and agent.send for the agent, and offered where sessions persist or the composition did not record whether they do
  • meta/schedule/list, meta/schedule/create, meta/schedule/update, meta/schedule/pause, meta/schedule/resume and meta/schedule/delete - an agent’s console schedules (see Schedules); the list is gated by agent.view and the writes by agent.send for the agent, and each send is a Console send by the schedule’s sender
  • meta/topology/query, meta/topology/plan, meta/topology/apply, meta/topology/operation/get, and meta/topology/audit/query - optional, revisioned connection management and separately permissioned durable audit
The identity routes take the durable identity or its current generated runtime id, rt:<identity>:<generation>. After a reset, the predecessor’s runtime id is refused with -32000 and error.data.kind stale_identity_runtime_binding, naming the current binding (registered_runtime_member_id, registered_session_id). A hidden identity’s runtime id answers identity_hidden_by_policy, and a runtime id of an identity the runtime does not know answers identity not found. meta/console/inspect_identity refuses a stale runtime id the same way before it looks the identity up in the console aggregator. A runtime id whose identity’s binding cannot be read right now is never let through: it is refused as unreadable, retryable, with error.data.kind identity_history_unavailable. The code is -32052 when the route’s identity lookup or its runtime id check meets the failed read, and can be -32000 when the route’s own status read fails after its lookup, or when the id names a live member whose binding cannot be read. Topology management is opt-in at the runtime. With the default disabled policy, the stock console remains a passive topology viewer and does not show the Connections tab. Read-only mode exposes searchable connection state with server-provided denial reasons. Editable mode offers only pairwise actions authorized for both endpoints; bulk controls appear only when a host supplies an explicitly bounded action and the runtime advertises topology.bulk. The reusable TopologyPanel keeps the authority-local and bilateral CAS contracts distinct. Stock JSON-RPC hosts supply the scalar query revision. Same-process coordinator hosts also supply the exact authority_revisions map as authorityRevisions; mutation callbacks receive that map as expectedAuthorityRevisions and must pass it to the bilateral coordinator as expected_revisions. Cross-authority hosts must never collapse that map to the scalar local revision or route the intent through the local JSON-RPC adapter. They may use topologyAuthorityRevisionToken as the component’s scalar view token when the coordinator snapshot has no scalar revision, but never as the coordinator CAS value. Editable mode requires a durable runtime state path; Meta fails closed rather than accepting reconnect/suppression intent that would disappear on restart:

Authentication

Coarse console access is governed by ConsolePolicy:
require_app_auth defaults to true. The SDK builders expose the opt-out as consoleAuthRequired(false) (TypeScript) and console_auth_required(False) (Python); rpc_gateway reads it as runtime_options.console_require_app_auth. When require_app_auth is true, protected console JSON, RPC, blob, and stream requests are checked against the AuthPolicy. The shell, static assets, and /healthz remain public. See authentication for the exact HTTP surface boundary. When read_only is true, the console can inspect identities, timelines, logs, and topology, but chat sends, uploads, lifecycle controls, and other write RPCs are denied server-side. For per-principal and per-agent authorization, install Meta’s optional AccessController from config/access.toml or the runtime builder. When enabled, its ABAC policy is deny-by-default with deny-overrides across console REST, JSON-RPC, and SSE. JSON-RPC denials use code -32030 with data.kind = "access_denied"; REST and SSE return HTTP 403. mob.observe authorizes mob-level observation but does not grant agent.view for every agent. To observe everything, a principal needs both mob.observe and an applicable agent.view grant. In Console access, open Groups and choose Inspect access beside a member to prefill that exact subject in Preview. Select an action and optional agent, then choose Evaluate to ask the access controller. The shortcut does not evaluate automatically or change policy, and remains available to authorized administrators on a read-only connection. A preview is an observation, not permission for a later action or a native tool grant. Upstream hosts that already know a user’s ACL can also force the stock console UI into view-only mode without changing Meta auth: set window.__META_CONSOLE_READ_ONLY__ = true before loading the console bundle, or append ?console_read_only=true (also accepted: meta_console_read_only=true or view_only=true) to the console URL. This is a frontend-only affordance for per-user UX; direct RPC calls are still governed by the server-side ConsolePolicy.read_only setting. Console mutation endpoints assume non-ambient credentials: pass bearer tokens explicitly in the Authorization header or auth_token query parameter for SSE/browser bootstrap cases. If a host proxy converts console auth into cookies or another browser-ambient credential, that host must add origin and CSRF protection before exposing mutation routes such as /console/rpc, /console/rpc/multipart, or /console/send.

Build system

The console has two build modes: The production build resolves @console-core and @console-components as local workspace packages via esbuild aliases. The browser bundle and CSS are embedded in the gateway binary.

Development workflow

The checked-in Vite proxy targets port 63220, but currently omits /console/timeline (including /stream), /console/identities, and /blobs. Full-contract development through Vite requires adding proxy entries for those paths to the same backend. Alternatively, use the embedded console at http://127.0.0.1:63220/console for full-contract validation; rebuild the production console assets and Rust host to test frontend changes there. When using Vite, edits in console/src/ or packages/ appear instantly in the browser.

Architecture

The shared packages (console-core and console-components) avoid app-store and Electron/Desktop-bridge coupling. console-components owns presentation; console-core includes a Meta HTTP/SSE transport using web APIs and a controller that accepts injected host transports. These are private workspace packages, not independently published npm API promises.

See also