Agent deep dive
Someone sends ai: what's the weather tomorrow in a group and a few seconds later the bot answers with real data — what happened in between? This page follows that path through the runtime that the AI overview doesn't cover in detail.
Haven't enabled AI yet? Start with the AI overview. Want the AI to call your functions? See Agent tools and Skills.
Find by question
| I want to know… | See |
|---|---|
| How a turn runs, how many iterations, when it times out | How a turn runs |
| Show "thinking" progress, or cancel midway | Showing progress · Cancelling a running turn |
| How lots of tools fit without flooding the prompt | On-demand tools |
| Hand long tasks to the background | Sub-agents and spawn_task |
| Where history is stored, how to reset, what happens when context gets long | Sessions and session trees · Context compaction |
| What the AI remembers, and who can change it | Semantic memory |
| Which tools the AI has built in, and file / network boundaries | Built-in tools |
| Have the AI do things on a schedule | Schedules and the assistant persona |
| Tune parameters for one model | Per-model tuning |
| Several Agents dividing work in a group or repository | Workroom Kernel |
How a turn runs
Once a message meets the trigger rules, the Agent Host calls zhinAgent.process(text, commMessage, tools) to run one turn:
The model may call tools several times and keep reasoning on the results; each round is an iteration. Things to remember:
- Iteration limit:
maxIterationsdefaults to 15 (DEFAULT_CONFIG.maxIterations) and can be tuned per model — see Per-model tuning. - Three layers of timeout: one turn on the trigger side is limited by
ai.trigger.timeout(default 60000ms); the whole Agent turn defaults to 120000ms (DEFAULT_CONFIG.timeout); tool pre-execution has its own 15000ms cap (preExecTimeout). - Messages that arrive while a turn is running: one conversation runs one turn at a time, and later messages follow these defaults (
ai.agent.inboundQueue) so there are no duplicate replies, no lost messages and no crossed turns:- Same sender adds more: if the running turn has not replied yet, it is stopped and both messages are answered together in a fresh turn (with
groupMode: fifothe message queues instead). If it already replied, the message queues. If the sender already has a queued message, the new one merges into it instead of producing another reply. - Anyone else: always queued in arrival order. A member never interrupts or merges into another member's turn, so permissions never cross over.
- Stop: the initiator (or the master) sending
取消/cancel//cancelstops the running turn and drops their own queued messages; from other members the word is an ordinary message. - Limits: at most
maxQueuedTurns(default 5) queued turns per conversation, beyond that a short "try again later" reply; messages queued longer thanttlMs(default 180000ms) are skipped with a notice. - Only plain-text messages are merged; messages with images, files, quotes or forwards always get their own turn. Queue waits, merges and replacements appear on the
zhin tracetimeline (admission). - Workroom turns, and setups where the Host supplies
resolveTurnIntentor a message carriesturnIntent, keep their explicit intent semantics.
- Same sender adds more: if the running turn has not replied yet, it is stopped and both messages are answered together in a fresh turn (with
- Automatic model fallback: if the preferred model fails, the candidate chain falls back to other available models of the same provider.
- Consistent behavior: the main Agent, sub-agents, background workers and
AIService.runAgentall use the same agentLoop.
The tools available to a turn = plugin tools + tools from ai.mcpServers + built-in tools + on-demand meta tools + schedule_* + bash + Host extension tools (such as voice_stt / voice_tts).
Showing progress
While a turn runs, the Agent pushes status to the chat through Activity feedback. By default that's a static "thinking…"; with thinking transparency enabled, users see fragments of the model's actual thinking:
ai:
agent:
thinkingPreview: true # show the LLM's actual thinking (truncated), default false
thinkingPreviewMaxLength: 200 # max characters shown, default 200When a turn needs several tool iterations, each starts with an iteration_start event and the status updates to progress like Processing [2/15].... Plugins can listen to the TurnEvent stream for progress:
| TurnEvent type | Description |
|---|---|
iteration_start | A new iteration begins, with iteration (current round) and maxIterations |
thinking | An LLM thinking fragment (streamed continuously when thinkingPreview is on) |
Cancelling a running turn
Users can send 取消, /cancel or cancel in chat to stop the current turn: ZhinAgent aborts the model call and tool execution via PromptController.cancelSession() and replies "cancelled". With nothing running, it replies that there's no task in progress.
In the TurnEvent stream, a user cancel appears as turn_cancelled (code: 'cancelled'); a timeout appears as turn_cancelled too (code: 'timeout').
Turn usage
A completed text or IM Agent turn accumulates the provider-reported usage of all responses returned through its completion port: tool selection, the final answer, context compaction, progress and exhausted-turn summaries, and format correction. Responses rejected by reply validation still count. Transport failures that return no usage are not assigned an estimated billing usage. This total is not a provider invoice reconciliation. User and group quotas remain disabled by default.
On-demand tools
With many tools, putting every tool's parameter schema in the prompt would fill the context. Zhin loads tools lazily: a turn keeps only a few meta tools resident, and the model searches and loads others by name when needed.
The resident core set (alwaysLoadedTools) has two groups:
- Interaction and orchestration:
ask_user,spawn_task, and the meta toolsdiscover,load_tool,load_skill. - Workspace files:
read_file,list_dir,glob,grep,edit_file,write_file. The model calls these directly most often, so keeping them resident saves an "unknown tool" round trip. They are still subject to the turn's tool filters and exec policy; for example, a turn started by a group member gets no file-writing tools.
bash is not resident because whether it can run depends on the turn's shell isolation policy. Setting alwaysLoadedTools adds to this default list; set alwaysLoadedMode: replace to replace it entirely.
Plugin tools are deferred by default. A plugin author can set alwaysLoaded: true in defineAgentTool to keep a few frequent tools resident (at most 8 per turn, ignored under alwaysLoadedMode: replace); see Keep a tool resident. Resident schemas are sent on every turn, so the smaller the set, the less context and tokens it costs.
Other tools send no parameter schema. The system prompt lists them in a ## Deferred tools section with their name and a description of at most 60 characters, up to 40 entries; beyond that it points the model at discover. The model can call load_tool(name) to get the schema, and calling the tool directly also loads it. The index only contains tools this turn may use: tools removed by policy (such as bash without shell access) never appear.
Skills use progressive disclosure: the system prompt lists each skill's name and the first line of its trigger description (at most 96 characters, up to 40 entries), and the specialist agent catalog works the same way. A skill's full instructions enter the context only after load_skill, or when intent matching activates it.
The context cost of each turn's first model request is estimated per segment and stored in the turn trace's context_budget field, which zhin trace last shows: system prompt, tool schemas, deferred tool index, skill catalog, specialist agent catalog, loaded skills, memory, bootstrap, history and the current input, plus the largest prompt sections. These are estimates (CJK ≈ 1 token per character, other text ≈ 4 characters per token); real usage is still in the model call records.
| Meta tool | Purpose |
|---|---|
discover | Search tools / skills by keyword (`kind: tool |
load_tool | Load a tool's definition into this session so it can be called |
load_skill | Load a skill's full text and unlock the tools it declares |
Tuning (ai.agent.deferredTools):
ai:
agent:
deferredTools:
maxLoadedPerSession: 12 # max tools loaded per session (default 12)
discoverTopK: 5 # results returned by discover (default 5)
alwaysLoadedTools: [ask_user, spawn_task, discover, load_tool, load_skill]
mcpServers:
icqq: { alwaysLoaded: [send_msg] } # resident tools for a specific MCP serverReserved built-in names (bash, read_file, spawn_task…) can't be overridden by plugins; for non-reserved tools with the same name, the later registration wins and a warning is logged.
Sub-agents and spawn_task
The main Agent can use spawn_task to hand complex or slow work to a background sub-agent without blocking the conversation.
ai:
agent:
maxParallelSubagents: 5 # parallel sub-agent cap (default 5)
toolExecution: tiered # parallel | sequential | tiered (default)
subagentAutoContinue: true # wake the main Agent to continue when a sub-task finishes (default true)
subagentDirectImDelivery: false # also post the sub-task summary straight to chat (default false)
subagentTools: [] # extra tools for sub-agentsMain spawn_task parameters:
| Parameter | Description |
|---|---|
task | Task description: goal, scope, expected output |
agent | Sub-agent name (must exist in ai.agents or agents/<name>/agent.json) |
wait | true waits synchronously; the result returns as the tool result in this turn |
context | fork (include the parent session's recent messages) / fresh (empty context) |
tools / skills | Tools and skills the sub-task needs |
- One turn can start several
spawn_tasks; independent sub-tasks should run in parallel. Intieredmode, read-only tools and spawns run in parallel while writes andbashrun sequentially. - Sub-agents get a restricted tool set by default:
read_file/write_file/edit_file/list_dir/glob/grep/web_search/web_fetch/bashplus the on-demand meta tools — not everything the main session has. Add more withai.agent.subagentTools. - Which sub-agents the main Agent may dispatch is controlled by
ai.agents.<name>.permission.task(glob → allow / deny). - When an async task finishes, the result goes back to the main Agent first (written into the main session and continued automatically), which then composes the reply to the user.
- Sub-agents are declared with
agents/<name>/agent.jsonplus three core Markdown files and discovered at startup. See Agent tools and Skills · Define a sub-agent.
Sessions and session trees
With a database available, conversations persist to these tables (in memory otherwise):
| Table | Contents |
|---|---|
agent_sessions | Agent session metadata (including the session tree's parent_id / active_leaf) |
agent_messages | Agent turn messages |
conversation_events | Chat messages, quote relations and notices (idempotent append) |
conversation_event_cursors | Unread event cursors per Agent session |
Set ai.sessions.useDatabase: false to force memory mode.
One chat conversation can also branch into a session tree, managed with chat commands:
| Command | Purpose |
|---|---|
/compact | Compact the current session's context manually |
/tree / /tree N | View / switch session branches |
/reset | Reset the session |
Send clear / 清空 / 重置 | Archive and clear this conversation's AI context |
Masters and permitted users also have /models, /health, /cmd, /endpoints, /bindings, /tools and /mcp.
Context compaction
When a conversation nears the model's context window it's compacted automatically (ai.agent.compaction):
ai:
agent:
compaction:
enabled: true
auto: true
keepRecentTokens: 20000 # token budget for recent messages (default 20000)
minKeepCount: 2 # minimum messages kept (default 2)
thresholdRatio: 0.7 # share of the context window that triggers auto-compaction (default 0.7)Compaction has two steps:
- Above
contextWindow × 0.6of messages, a light trim drops redundant tool results and the like. - When message tokens exceed
contextWindow × thresholdRatiominus what the system prompt and tool definitions use, the model writes a[Previous conversation summary]that replaces the old history. The trigger is never above the older "window minus 13k buffer" rule and never belowkeepRecentTokens × 1.25. This applies to plugin-runtime sessions such as QQ as well.
The summary keeps unfinished work, user preferences and the conclusions and commitments already given to the user. After a limit of consecutive failed auto-compactions, auto-compaction stops to avoid repeated cost.
"Already told" ledger
Each session remembers what was already sent to the user, up to 8 entries of at most 120 characters each:
- the lead sentence of each final reply is recorded as a conclusion;
- sentences that promise future action ("I'll…", "我会…", "稍后…") are recorded as commitments;
- each progress update (#718) is recorded as progress.
Progress updates and final replies share this one record, and a repeated statement keeps only its newest copy. The next turn gets it as a short [Already told the user] block inside [Turn context], so the model neither repeats itself nor contradicts what it said, and compaction is told to keep these items in its summary. The ledger is visible only within its own session and lives in process memory; after a restart, the persisted history and its compaction summary carry the same facts.
Prompt caching
The system prompt is ordered stable-first: the per-turn git status is the last section (§12_git_status), and time, session and other volatile details live in the user message's [Turn context]. Cache-read tokens reported by the provider are recorded in the turn trace, and zhin trace last shows the prompt cache hit count and its share of input tokens.
Semantic memory
With ai.memory.semantic.enabled, memory_upsert / memory_search store memory in the memory_entries database table. Each entry has:
| Field | Meaning |
|---|---|
scope / scope_key | Visibility: global, platform, session (this conversation), user (the current initiator) |
kind | user_fact (stable fact), preference, task_state (temporary, expires), episodic (something that happened) |
source / source_turn / trust_level | Which turn wrote it and at which trust level. Filled in by the runtime; the model cannot set it |
confidence | 0–1 |
created_at / updated_at / expires_at / last_used_at | Created, updated, expiry, and last recalled times |
history | Previous versions kept when content is replaced (up to 5, newest first) |
Write rules:
- A group member (
group_member) can write only to their ownuserscope or the currentsession, neverglobal/platform. Without ascopetheir writes default touser; owner and trusted members still default toglobal. - An existing
user_fact/preferencecannot be overwritten from a lower trust level, so a group member cannot change what the owner recorded. Writes from the same or a higher level win, and the old value moves tohistory. task_statealways expires: 24 hours by default,ttl_hoursat most 168. Bind it to a task withtask_id; when the host'sisTaskActive(onSemanticMemoryRuntime) reports the task gone, the state is no longer recalled.- File memory (
data/memory/**/MEMORY.md) is unchanged and stays readable;renderMemoryEntriesMarkdown()exports structured memory as Markdown.
On upgrade, an existing memory_entries table gains the new columns at startup (ALTER TABLE … ADD COLUMN) and keeps its rows: old entries read as user_fact with an empty trust level, protected like a trusted member's.
Retrieval and per-turn recall
memory_search and per-turn recall rank with BM25 (Latin text by word, CJK by single characters and bigrams; no segmenter needed). At the start of each turn the Agent searches the scopes the initiator can see (this session, their own user scope, the current platform, global) with the current message and adds the best matches to the turn context under Relevant memory, labelled as data, not instructions. How much is injected depends only on topK and maxTokens, not on how many entries exist. Expired, deleted, and other users' user entries are never recalled. Recalled or searched entries update last_used_at.
ai:
memory:
semantic:
enabled: true
recall:
enabled: true # default true; false keeps only the memory_search tool
topK: 5 # entries injected per turn, at most 20
maxTokens: 400 # token budget for the recall block (estimated)Viewing and forgetting
- Any user can send
/memoryto see what is stored about them (their ownuserscope, up to 20 entries),/forget <key or id>to forget one entry, or/forget allto forget everything. They only touch the sender's own memory; without semantic memory enabled these commands are not intercepted. - Hosts and the Console view, edit, and delete memory in any scope through
SemanticMemoryRuntimelist/upsert/delete.
Markdown file memory (MEMORY.md) is still injected whole within its character budgets; this release does not change it.
Built-in tools
| Category | Tools |
|---|---|
| Execution | bash |
| Files | read_file, write_file, edit_file, list_dir, glob, grep |
| Network | web_search, web_fetch |
| Interaction | ask_user |
| Tasks | spawn_task, todo_read, todo_write |
| Memory / search | memory_search, memory_upsert, knowledge_search, inspect_conversation_reference |
| Media | generate_image; incoming images go straight to vision-capable models |
| Meta | discover, load_tool, load_skill |
| Schedules | schedule_list, schedule_add, schedule_remove, schedule_pause, schedule_resume, schedule_preview |
File boundary: file tools work only inside the project directory authorized for the current turn. Relative paths are rooted there; absolute paths must stay inside it; ~, ../ escapes and symlinks pointing outside are refused. Only normalized paths that pass the check reach the tool; glob / grep never start a shell subprocess.
Network boundary: network tools get HTTPS access only with ai.agent.execPreset: network; readonly and other presets never open the network implicitly. web_fetch re-checks protocol, domain and SSRF for the initial URL and every redirect; private, link-local, CGNAT and multicast addresses are refused, and the actual connection is pinned to the checked address.
Todos: todo_read / todo_write act only on the current session's plan and accept no chat_id or path. State is written atomically to .zhin/todos; .zhin/ as a whole is runtime-private and can't be read or listed by file tools.
Schedules and the assistant persona
The Assistant runtime turns "do things on a schedule" into a ready-made feature: jobs persist in data/schedule-jobs.json, ScheduleJobEngine runs them on time, and results go back to chat.
assistant:
enabled: true
profile:
enabled: true
file: assistant.profile.yml # relative to the project root; this is the default
events:
enabled: true # expose the POST /api/assistant/events entry point
defaults:
notifyOnFailure: falseWorkspace bootstrap files (SOUL.md, AGENTS.md, TOOLS.md) may write a slot; prompt assembly replaces it with the current agent's ai.agents.<name>.nickname, or Zhin when none is configured, so one SOUL can serve several bots with different nicknames:
# Who I am
I'm {{nickname}}, a long-time member of these groups.assistant.profile.yml declares the persona and routines, synced into scheduled jobs at startup:
version: 1
persona:
soul: You are a thoughtful life assistant.
routines:
heartbeat:
enabled: true
everyMs: 1800000
prompt: Check todos and report.
morningBrief:
enabled: true
scheduleKind: solar # solar | lunar | workday | freeDay | holiday
cron: "0 0 8 * * *"
tz: Asia/Shanghai
prompt: Write today's morning brief.- Built-in routines are
heartbeat(interval),morningBrief(08:00 by default),bedtimeCheck(22:00 by default) andweatherReport; custom keys work too. - For mainland-China working days use
workday(it accounts for adjusted working days), not solar1-5. - Users can also ask the AI in chat to manage jobs with
schedule_add/schedule_list/schedule_preview; external systems can inject events viaPOST /api/assistant/eventsand list jobs withGET /api/assistant/jobs. From the command line: CLI · schedule.
Per-model tuning
Override execution parameters per provider / model (currently maxIterations). Merge order: built-in defaults → providerPatterns (with * wildcards) → exact models keys:
ai:
agent:
modelHarness:
providerPatterns:
"open*": { maxIterations: 7 }
models:
"gpt-4o": { maxIterations: 8 }
"openai:gpt-4o": { maxIterations: 9 }Workroom Kernel
A Workroom lets several Agents divide work by role on multi-step tasks in a group, channel or GitHub repository: an Orchestrator understands requests and assigns work, Executors carry it out, Reviewers check it. Unlike ordinary chat, every step is recorded in an append-only journal, so it is auditable and recoverable.
Neither ordinary chat nor spawn_task creates a Workroom automatically — only explicit Project commands such as /work do.
Core rules
WorkroomKernelaccepts only explicit, Project-scoped commands and uses a versioned append-only Journal as the single source of truth for Runs / Tasks / Assignments.- There is no generic state-transition tool that would let a model fake "executed / accepted". Command adapters must hold an authenticated Project capability and connect the Scheduler, Executor and Acceptance ports separately.
- Human messages first enter a content-free Project Inbox by full space address, then the Orchestrator arbitrates them into tasks.
conversation.agentidentifies the Orchestrator owning that Inbox, but can never bypass arbitration to turn a message straight into Task state. - Once ordinary discussion is persisted, a one-time handoff continues that Orchestrator turn and returns the Agent reply directly. That turn uses a Project-scoped session and a trusted Project / Proposal envelope: it doesn't read the ordinary group-chat session tail, doesn't expose
spawn_task, and always replies through the Catalog Orchestrator Endpoint's unified outbound chain. - Replying to a projected card or
@-mentioning a named member produces an exact TaskInput only when there is exactly one active Assignment; otherwise it asks for clarification. - Only an explicit
/workcreates Runs / Tasks, through the governed planning entry point.
Declaring a Workroom in Console
Use Console's Workrooms menu to declare the Project boundary, Agent members and roles, and the group, channel or GitHub repository it binds. Workroom definitions live in the persistent runtime Catalog, not in the ai config file; saving takes effect immediately without a restart. The old ai.workrooms config is explicitly rejected with a migration hint. The record key is the projectId used by the Workroom Kernel:
{
"projectId": "support",
"name": "Customer Support",
"enabled": true,
"members": [
{ "agent": "zhin", "role": "orchestrator" },
{ "agent": "support", "role": "executor" },
{ "agent": "reviewer", "role": "reviewer" }
],
"conversation": {
"adapter": "telegram",
"endpoint": "support-bot",
"kind": "group",
"id": "10001",
"agent": "support"
}
}| Field | Rule |
|---|---|
members[].agent | Must reference an Agent in ai.agents, with each role using its own binding |
members[].role | One of `orchestrator |
members[].assignmentRoute | { "kind": "local" } pins local execution; { "kind": "remote", "endpointId": "..." } pins a single A2A endpoint. Omitted means local; remote execution never guesses between endpoints. The route is persisted with the Catalog revision / digest and re-checked before claiming |
members[].messageRoute | e.g. { "adapter": "icqq", "endpoint": "reviewer-bot" }: which bot this member speaks through in a shared group. Omitted means the conversation's main Endpoint. Once set, messages received by that bot in the same group join the same Workroom, and the member's progress, milestones and conclusions are posted by it |
sponsors[] | Authenticated principal ids (e.g. owner:<platform-user-id>) allowed to run Project Sponsor controls. This is persistent Catalog authority; self-reported identity in message metadata is never read |
conversation | The ordinary Workroom space |
sponsorConversation | Optional separate `group |
More constraints:
- The
agentof both conversations must be a member with theorchestratorrole. An enabled Workroom needs at least one Orchestrator and an ordinary space. - One Workroom binds one full space address; one Bot / App Endpoint can serve many Workrooms, but ordinary Workroom addresses can't be reused or collide with a Sponsor Room.
- Several Projects may declare the same Sponsor Room, forming a Portfolio-level aggregate room, but each projection is still a separate Project view. In an aggregate room, governance commands look like
/control data-lifecycle project <projectId> ...and Portfolio commands like/control portfolio <portfolioId> project <projectId> ...; when replying to a single Project's card you can omit the Project segment. A room address carries no Project authority by itself: if the explicit Project and the replied card disagree, the card revision is stale, or the Endpoint version drifted, you get a clarification, not an executed command. - At startup or scan time the Host builds Sponsor delivery bindings from the persistent Catalog and current Endpoint capabilities, so the first outbound-only alert doesn't need the room to have received a message first.
A GitHub repository as a Workroom
The GitHub adapter matches repository Workrooms using the stable repo metadata (owner/repo) from webhooks, so every Issue / PR comment thread in one repository joins the same Workroom:
{
"projectId": "zhin-repo",
"name": "Zhin Repository",
"enabled": true,
"members": [{ "agent": "zhin", "role": "orchestrator" }],
"conversation": {
"adapter": "github",
"endpoint": "my-github-bot",
"kind": "repository",
"id": "zhinjs/zhin",
"agent": "zhin"
}
}A GitHub Project Item is an external projection / sync target for a Task, not part of the Workroom's identity. Console's Task page currently reads only Journal and Kernel facts; syncing to Project V2 needs a separate Integration Port, an idempotent task-key ↔ item-id mapping and a conflict policy — Console can't change Task state directly. Full design: GitHub repository Workroom.
Planning tasks with /work
/work never degrades into a single fixed task:
- It creates a Run only when the composition root installs a governed
HumanIngressPlanningPort; otherwise it persists the request and returnsplanning_unavailableasking for clarification. DynamicWorkflowPlanningPortis recommended: the model or Strategy returns only an untrusted structured DAG candidate, while Project / Catalog, Profile, Orchestrator, planning policy, budget and Sponsor Gate authority are injected by trusted ports.WorkflowPlanBuilderre-validates the candidate against the Profile capability ceiling, required / optional steps, dependencies and cycles, Task / attempt budgets and approval gates; onlyWorkroomKernel.admitWorkflowPlan()can atomically write Plan / Task facts.- A Plan Gate first becomes an
approvalBlocker that the ordinary Orchestratorresolve_blockercan't clear. WithWorkroomPlanGateAuthorityPortinstalled, a human Sponsor can submit an exact, replayable decision with/control plan-gate <approve|reject|request-changes|cancel> <runId> <taskKey> <gateId> [reason].
Enabling planning for the first time
In Console's Workrooms → Planning and disclosure, enter a projectId and run diagnostics. Writes need a full token bound to a principalId, and that principal must appear both in the Project's sponsors and in the process-wide trusted Pack publisher list:
http:
tokens:
- token: ${WORKROOM_SPONSOR_TOKEN}
scope: full
principalId: workroom-admin
ai:
workroom:
trustedPackPublishers:
- workroom-admin
disclosure:
modelProviders:
PROVIDER_ALIAS:
endpoint: REPLACE_WITH_CONTRACTED_ENDPOINT
processingRegions: [REPLACE_WITH_CONTRACTED_REGION]
maxConfidentiality: project_internal
external: true
noTraining: REPLACE_WITH_PROVIDER_GUARANTEE
loggingMode: REPLACE_WITH_LOGGING_MODE
maximumRetentionSeconds: REPLACE_WITH_MAX_RETENTION_SECONDS
allowsRedisclosure: REPLACE_WITH_PROVIDER_GUARANTEE
supportsDeletion: REPLACE_WITH_PROVIDER_GUARANTEE- The
REPLACE_WITH_*values are deliberately invalid placeholders and can't be saved as-is. Keys undermodelProvidersmust be theai.providersalias used by the Orchestrator; region, training, logging, retention and deletion fields are governance declarations that must match the provider's actual policy and account settings. With OpenRouter, also enable zero data retention and disable data collection in its Privacy / ZDR settings. - Save the real config, restart the Host, log in to Console with that token, and add
workroom-adminto the Catalog Project'ssponsors. - Once diagnostics pass, click "Initialize orchestration". Console publishes a Capability Pack from the current version's real Agent / Tool / Skill digests, activates the first Project Profile, publishes the default Planning Policy, and has the authenticated Sponsor issue Project Data Governance authority.
- When both Planning and Disclosure show as ready,
/workcan be submitted.
Back up the data-governance root key
On first issue, a self-hosted Host creates .zhin/workroom-data-governance-root-key.json with mode 0600, used to wrap local data keys and sign authority. Include it in encrypted backups: if it's lost, existing Payload Vault content can't be recovered.
Storage backends, Console control planes and remote execution
- Storage backend: the Catalog and Run Journal share one persistence choice. With
ai.sessions.useDatabase !== falsethey use the database tablesworkroom_catalog/workroom_events, and a Database Root Host that isn't ready makes the new version fail to publish; with it explicitlyfalsethey fall back to atomic file event streams in.zhin/workroom-catalog.jsonand.zhin/workroom-journal. The backend is fixed at process start and can't change on hot reload — restart the process to change it, so old and new versions never write two facts sources with no CAS relationship. - Catalog edits: saves use revision CAS; member references follow the current version's
ai.agents, and stale references are rejected. Journal facts of existing Runs are never overwritten by Catalog edits. - Read-only projections: Console's Run list and details are Project-scoped read-only projections (e.g.
GET /api/agent/workroom/runs?projectId=...) and can't change Tasks / Assignments. - Typed control planes (isolated from each other): Profiles and Project Knowledge are published or rolled back through
workroom.profile.*/workroom.knowledge.*RPC; a Portfolio Sponsor submits exact commands toPOST /api/agent/workroom/portfolio/commands; an Effect Sponsor usesPOST /api/agent/workroom/effects/sponsor-decisionsto decide only authorizations bound to an exact Intent, accepting only a closed set ofreasonCodes and storing no free-form explanation; Data Steward / Privacy / Compliance use/api/agent/workroom/data-lifecycle,/overdueand/commandsto query lifecycle projections and run Holds, subject erasure / export and retention purge / reconcile — these endpoints are published only when Root-private role authority, the current Catalog Project and the P12 Console recipient all match. Every endpoint re-derives the principal from the authenticated token and re-checks the current Catalog / Profile / governance revision; identity, approval or discussion self-reported in the request body never grants write access. - Remote execution: the optional Remote A2A Executor reuses the same Assignment lease / fence / event contract. A new claim is allowed only when the persistent Profile, Authority Grant, Workspace, Disclosure and the endpoint's Card / auth / transport snapshots all match exactly; anything missing blocks persistently. The old
ai.remoteAgentspoller no longer exists.
Legacy data
The legacy format (a Journal with embedded payloads) and legacy Runs are no longer supported and have no migration tooling; opening a legacy-format store fails closed. See Legacy concept migration.
Related
- AI overview: enabling AI, models, trigger rules and security policies.
- Agent tools and Skills: writing Tools, Skills, Prompt Sections and sub-agents.
- Speech: speech recognition and voice replies.