Skip to Content

Console

The console is a program section at /p/<slug>/console, not a fifth product. It appears in the rail when program.threads.read_own or program.threads.read passes and is used with program.threads.use. Its status word is preview since 2026-09-15 (contract 11.6: the two phase-2 measurements were registered and the transition was made in one commit); the twin surfaces of R3 that build on it (inbox, board, twin panel) stay designed until the R3 gates pass (roadmap). Pilot positioning, quoted from the contract: “non-GxP decision support with a light validation package”. The ELN stays the scientific record, the QMS stays the SOP record, Trovensa is the record of what agents proposed and what people decided.

Regions

RegionContentWidth at 1440 px and aboveRemembered in
Conversationmessages (role=feed), plan and approval cards, citation markers [n], composer with Stop and slash commands560 to 720 pxnothing
Canvascurrent artifact, version selector, selection-to-edit, promote, version diff, source viewer480 to 640 px, collapsiblecookie console_canvas
Run railsteps, tool calls (name, argument summary, scope, policy decision, permission_exercised, span id), sources read, budget used, SOP binding, model configuration record320 px, collapsiblecookie console_rail

Below 1100 px the canvas and rail become a tabbed drawer; below 768 px a bottom sheet with full-width cards. Mobile reads, approves, declines and stops; artifact editing is desktop-only and the button says why. Cookies hold layout only; everything else is a record.

Thread states: empty, planning, awaiting approval, running, paused (the composer prints “Paused, waiting for a decision”), finished, failed (with the public error code, for example gate_expired, policy_denied, config_mismatch), archived.

Threads and messages

RecordFields (DTO)Rule
Thread (ThreadOut)title, status (open, archived), visibility (private, program), created_by, scope_type (twin for a twin’s own thread, handoff for the program-visible thread an accepted hand-off opens), scope_id, product_slug, launch_context, pinned_context, last_message_at, message_count, redacted_at; schedule was migrated into paused twins and dropped by 0005_twinsprivate by default; the owner reads it with program.threads.read_own; quality reviewers and leads read every thread with program.threads.read; only the owner shares, with a confirmation naming what becomes visible (ThreadShare.confirm_counts)
Message (MessageOut)seq, role (user, agent, system, tool), status (streaming, complete, failed), content_md, content_json, agent_run_id, author_user_id, parent_message_id, citation_count, claims_without_source, feedback (none, helpful, corrected, dismissed), feedback_by, feedback_note, mentions (R3: @name references in a program-visible thread; a private composer says “Share this thread to mention colleagues.”)text passes redaction before storage and never contains retrieved source text; citations reference document_chunks.id; feedback is rendered as labelled icons, not status chips
Agent run (AgentRunOut)phase-1 fields plus thread_id, mode (interactive, background), lane, agent_version_id, plan_hash, usage, trace_id, and for twin runs twin_id, trigger_id, initiated_by (schedule, event, handoff, user)one console_turn run per agent turn; runtime_session_id equals the run id; twin runs (twin_cycle, twin_handoff) live in one private thread per twin and their footer reads “Started by … under Name’s standing plan”, never “Requested by”
Interrupt (RunInterruptOut)seq, kind (plan_approval, write_approval, consent, question, batch_approval), detached, payload, status, resolved_by, resolved_at, resolution (decision, reason, edits diff; for a batch partial and per-item outcomes), permission_exercised, expires_atdefault expiry 24 h for console turns and 72 h for twin proposals (twin_definitions.expiry_hours), extendable; resolution needs program.threads.use plus the write’s own permission, and on a twin run the bound-user rule below
Tool call (ToolCallOut)specialist (from the run token), tool_key, arguments (redacted), proposal_hash, basis_hash (the linked record’s {target_type, target_id, updated_at, status} at proposal time), approved_hash, status, approved_by, approved_at, permission_exercised, resolution_reason, policy_decision, injection_flag, result_ref, otel_trace_id, otel_span_idwrites need an approved row whose approved_hash matches the payload the server writes
Snapshot (RunSnapshotOut)context (chunk ids and hashes, record ids, sop_version_ids, prompt version, allowlist, policy version, permission snapshot), context_hash, model_check (expected_model_config_id, reported_model_id_hash, inference_profile_id, outcome)every run is reproducible from the database; events stay excerpt-free

The composer shows the fixed phrase “Private to you. Quality reviewers and program leads can read this thread for review.” A record created from a thread links (thread_id, message_id, artifact_id, artifact_version, agent_run_id) and its page reads “From thread T-31” or “From a private thread” according to the reader’s rights. Conversations are redacted to tombstones after organizations.settings.thread_retention_months (default 24) while tool calls, interrupts, snapshots, signatures and audit rows persist (thread.redact).

Run modes and plan-first gates

Every run starts with a plan artifact (actions, tools, sources, expected writes, budget). The approved plan (agent_runs.plan_json, plan_hash) is the control flow: a tool outside it is refused with plan_violation and surfaces as a new interrupt. The mode is remembered per user in users.settings.console_run_mode (PATCH /me/settings):

Mode idLabelBehaviour
approve_each_writeRun and approve each writereads run; every write pauses the run for a decision
auto_reads_gated_writesRun with automatic reads and gated writesdefault; reads run within the plan, writes pause
keep_planningKeep planningthe plan is refined without running tools

Approval card

One component handles plan approval, write approval, connector consent and questions. It shows what will be written, which sources it cites, the downstream consequence and the permission exercised (disabled with the permission name when the viewer lacks it). Buttons: Approve (Enter), Edit then approve, Decline (Esc, the reason is stored as evaluation data); Resume re-opens an expired gate; Stop ends the run. Edits are applied by the server to the stored proposal, approved_hash is recomputed, the diff goes into resolution, and the record is written from the server-side payload only; the record page then reads “edited before approval” with the diff. A card that waits more than 15 minutes becomes a background run card and stays actionable. Approval is not a signature: signatures are reserved for approve routes and requirement-set acceptance (signatures).

Batch approval (R3)

A batch_approval interrupt bundles up to 10 write proposals of the same tool and target type, each its own tool_calls row, under one batch_key; one open batch per run. In a console turn a batch arises when the person asks for several records at once and the run waits (attached); in a twin cycle batches are detached. The decision batch card extends the approval card with a checkbox column and a per-row diff. There is no “Approve all”: each item shows an expanded summary (tool, target, sources, permission, consequence, citation coverage) and a checkbox; Approve selected applies to explicitly selected rows, is disabled with the reason named when any selected item lacks the resolver’s permission or its citation coverage is below the program threshold, and turns the button into a two-step control naming the count, the tool and the permission (“Approve 8 checks with review.sops.manage”) before the call. The request is InterruptResolve.items[] (1 to 10, each {tool_call_id, decision, edits?, reason?}); a top-level decision on a batch is refused (422), as is items[] on any other kind. The service mints one batch_id, checks each item’s own ToolSpec.permission (403 batch_item_forbidden, the item failed, the siblings untouched), verifies basis_hash (409 stale_proposal per item), requires a reason on every decline, executes approved items on the call that approves them, and writes one audit row per item. The answer is InterruptResolveResult {batch_id, items[{tool_call_id, status, code?}]}; the card announces “4 approved, 1 declined” through role=status. Undecided items expire one by one; the card then reads “5 approved and written, 5 expired”. At 390 px the list degrades to /me/inbox/[itemId], stepping “item k of N” with the same controls.

Detached interrupts and the inbox resolve path (R3)

A twin cycle files and finishes: open_interrupt(detached=True) inserts the interrupt and its proposals without touching run.status, the container is acknowledged and continues, and the run ends succeeded. Approval later executes the write server-side from the stored arguments through the tool handler under the approver’s principal; nothing resumes a container. Expiry writes nothing, marks the item “Expired, nothing written” and leaves the run succeeded; Resume re-files it on the twin’s next pass. Console turns stay attached and behave as the previous sections describe.

Interrupts on a twin run belong to the bound person: write_approval and batch_approval are resolvable by the bound person or by an active delegate who holds the item’s write permission, plan_approval by the bound person only, and anyone else, including holders of program.threads.read and program.threads.use, gets 403 bound_user_only naming the twin. The resolve path is POST /me/inbox/{item_id}/resolve: it is authorised by owning the inbox item or by an active delegation for its routing, then calls the same interrupt service with the caller’s principal, and never requires reading the private thread. The inbox, the brief e-mail link and the phone all use it; the thread-scoped route POST .../agent-runs/{run_id}/interrupts/{interrupt_id}/resolve keeps working for the bound person under the same rule. Digest-priority filings pass notify=False and create no approval_requested notification; the person meets them in the inbox (“Decisions waiting for you”) and in the daily brief. Every decision writes the audit envelope with interrupt_id, permission_exercised, proposal_hash, approved_hash and, from R3, batch_id, twin_id, trigger_id and delegated_from_user_id. The guides Decide from your inbox and Delegate while away walk through both paths.

Artifacts and canvas

Kinds are a fixed vocabulary rendered by UI-owned components: plan, comparison_table, finding_set, hypothesis_set, protocol_draft, report_draft, deviation_table, sop_diff, milestone_view, summary and, from R3, persona_brief (the daily brief, written by the worker job twin_digest from inbox items with no model call, in the twin’s thread with run_id null). Versions are append-only within a lineage_id; selection-to-edit yields v(n+1) with origin = agent, direct edits yield v(n+1) with origin = human and authored_by_user_id. Promote turns a version into a tool_calls proposal awaiting approval, never a direct write and never a visibility change. Items of kind observation need non-empty chunk_refs and cannot be promoted otherwise. Footer: “Agent-generated draft, unsigned.” followed by “Cites N sources.” and “Model configuration MC-n (recorded <date>)”.

Citations and citation coverage

Every factual sentence carries at least one [n] bound to (document_id, chunk_id, char_start, char_end). The source viewer shows title, provenance, licence code, retrieved_at, snapshot stamp, the cited span and “Open in notebook” or “Open at publisher”; rendering follows the licence codes. Citation coverage is a deterministic count of claim-shaped sentences without a marker (the pattern list lives in web/lib/content.ts claimPatterns); the header reads “All 9 claims carry a citation” or “1 claim without a citation”. It verifies markers, not that the chunk supports the claim; semantic grounding is a judged check in the evaluation suite. “Needs sources” withholds the write gate until citations are repaired or the user overrides with a note.

Record actions, palette and slash commands

Ctrl K gains Ask. On any record page . opens the record actions “Ask about this”, “Check against SOP”, “Prepare a draft from this”, “Compare with…”, “Send to QA” and “Accept into record”, filtered by the viewer’s permissions. The composer accepts /check, /draft, /compare, /brief and /pin. “Ask the agent” replaces the phase-1 agent action button; the run rail replaces the run dialog; legacy run kinds stay callable as canned prompts.

Keyboard and accessibility

role=feed composites for messages and rail; streamed text is announced through a polite role=log in sentence chunks; approval cards receive focus on appearance and return it on resolution; the sticky composer never obscures focus. Single-key shortcuts (a accept, . record actions, Enter, Esc) are active only with focus on the card or record header; users.settings.shortcuts remaps them to alt_chords or turns them off. Agent-origin content never uses the pass tone until accepted.

Notifications

notifications (bell and /me/notifications) fire on approval_requested, needs_input, run_finished, run_failed, sop_changed, batch_completed, connector_reauth and, from R3, handoff_received (only when the hand-off names a person), digest (one per person per day, written after the brief exists), twin_paused, mention and decision_reminder (to holders of program.twins.read only, at most once per item per day). E-mail carries the record type, the human-set program name, the record id and a link only: no titles, no agent text, never a token that acts, no approve-by-reply. Quiet hours (users.settings.quiet_hours and timezone) hold delivery through deliver_after, not the record; the sweep selects COALESCE(deliver_after, created_at) <= now(). run_finished and run_failed are not sent for twin runs; genuine twin failures reach holders of program.twins.read. Reading a notification is audited (notification.read).

Performance budgets (design targets)

Optimistic echo under 100 ms; working indicator under 1 s (the state event is written in the message transaction); INP under 200 ms; SSE keepalive every 15 s; reconnect from Last-Event-ID within 2 s. First-token latency and the cold bound are measured and recorded in CLAIMS-REGISTER.md before any number appears in copy; none appears here.

Source: web/lib/content.ts (consoleSection, consoleCopy, runModes, recordActions, slashCommands, approvalActions, claimPatterns, twinCopy, inboxKinds), web/lib/api/types.ts (InterruptResolve, InterruptResolveResult, InboxItemOut), web/lib/api/endpoints.ts, api/app/db/models/conversation.py, api/app/db/models/agent.py, api/app/db/models/inbox.py, api/app/services/interrupts.py, api/app/services/notifications.py, api/app/auth/permissions.py, docs/lib/content.ts, PRODUCT-CONTRACT.md sections 10.1 to 10.4, 11.4, 11.6 and 11.7, PRODUCT-PLAN.md Part 4 section 3 and Part 7 section 5

Last updated on