Configuration

corteza logo

Configuration

In a shared Matrix room, say Let's take this offline or use /sidebar [N]. The selected conversation enters the private context as one user-role quotation, with original speaker roles and source-event links retained. It is background for the continuation, not new instructions or the private assistant’s own reply. The bot copies the last N human exchanges (default 5, maximum 20) into your existing private room with source-event links, and redirects active jobs’ progress, approvals and results there. Their worker and workspace stay the same. The source gets one continuation notice. Original messages remain in the source room; the extracted copy is durable and enters the private room’s normal archive. Only configured operators can do this. private_rooms maps operator IDs to destination IDs; sidebar_room and then the bot’s default room_id are fallbacks. The destination must be invite-only with just the operator and bot joined. Delivery is withheld if those checks later fail. A room controlling a remote CLI cannot receive a sidebar until that CLI disconnects. A failed send can be retried; as with other Matrix sends, a crash after acceptance but before recording its ID can duplicate a post.

In a Matrix room, /approve-all [job-id] authorizes eligible remaining approval requests for one active job. /approve-all status [job-id] shows the grant; /approve-all off [job-id] revokes it. With multiple active jobs an ID is required. Only a configured operator who is still in the room can authorize requests. The authorization includes requests normally escalated by the supervisor, is recorded against that operator, and expires when the job ends. It does not override a policy denial, authorize other jobs, or cover credential-path requests: those still require individual human approval. Credential-bearing calls require local review. Only the explicit slash command grants approval; ordinary words and /auto do not. CLI /auto, including through remote control, is unchanged.

Display output masks known credentials from configuration and environment, plus quoted credential-shaped literals. The registry belongs to one session; source expressions and unquoted words are never learned as values. Tool inputs/results, handles, native history, transcripts and journals remain unchanged. These private records can contain secrets. Worker checkpoints and approval request files use owner-only permissions. A Matrix sync_token is a cursor, not a credential.

Room activity defaults to counts only. Set activity_detail: true in the bot configuration to include commands, paths, result previews and model narration. Masking cannot identify arbitrary encodings or unknown unlabelled secrets; it does not replace the summary setting. Approval displays are never masked or truncated: calls containing credentials or too large to display in full require review in a local CLI. Voice deltas are delivered immediately except for a partial match to a known credential, which is held until it can be masked safely.

The CLI shows five-line result previews and separates commentary from tool logs. After 60 seconds without commentary, the next completed tool emits a factual progress update. A blocking request or tool must return before that update can run. Reaching the tool-turn limit reports the last update and how to continue.

How to configure corteza for your workflow.

Config files

corteza reads two JSON files and merges them. Project config overrides global config.

Layer Path
Global tools::R_user_dir("corteza", "config")/config.json
Project .corteza/config.json

Merge semantics: project keys replace global keys at the top level; no deep merge.

Precedence: CLI flags > project config > global config > built-in defaults.

Example project config:

{
  "provider": "ollama",
  "model": "llama3.2",
  "context_files": ["README.md", "PLAN.md"],
  "approval_mode": "ask"
}

Create the directory first:

dir.create(".corteza", showWarnings = FALSE)

CLI flags

corteza [options]
Flag Description Default
--provider <p> LLM provider: anthropic, openai, moonshot, openai_codex, ollama, openai_compatible anthropic
--model <name> Model name provider default
--base-url <url> Endpoint for --provider openai_compatible config / env
--tools <filter> Tool filter: core, file, code, git, r, data, web, chat all
--session <key> Session key; resumes if exists none
--resume Resume most recent session false
--list List sessions and exit false
--dry-run Preview tool calls without executing false
--context-edit Enable conversation context editing for this session false
--trace Print structured tool-call events to stderr false
--help Show help and exit -

Flags override config values for the current run.

Resuming restores the saved provider and model together, even if configuration has changed. Explicit --provider and --model flags still override that pair. Changing only the provider uses its matching configured model or its default; it does not carry the old provider’s model across. The same rules apply to chat(session = ...) and its provider and model arguments.

Unattended work

/auto continues the current task using the conversation and its approved plan. /auto <goal> supplies a new goal; an empty session needs one. Runs continue until completion, a blocker, or a resource limit. --loops N sets an iteration limit. /auto status reports the last stop, and /auto resume restarts that goal in the same conversation.

The startup display names the working-directory scope and whether shell/R execution is enabled. /auto --exec explicitly permits the monitor to assess shell/R calls; a project restriction can still veto it. Without that grant, those tools are omitted. Unclassified package tools are also omitted rather than offered to the model and then stopping the run when selected. Path, credential, policy, and monitor checks still apply to every dispatched call.

When a run stops, auto mode is off. Ordinary messages do not restart it. /permissions controls ordinary tool approvals separately from /auto.

Matrix remote control

/remote-control [--bot @bot:server] [room] hands the current CLI or chat() session to a Matrix room. The model, history, working directory, and approval policy stay in the CLI process. Keep that process running. The existing bot loop carries messages and replies; the CLI does not log in to Matrix or share the bot’s sync cursor or encryption store.

/rc is an alias for /remote-control, with the same arguments. In the attached room, /rc status reports the connection and /rc off disconnects.

Both processes must run a version with remote control, on the same machine under the same user. The bot must have an explicit operators list. With one available bot it is selected automatically; otherwise specify --bot. The default is a private room for the current CLI session, named Corteza: <session-id>. Creating session rooms requires chat.api >= 0.2.0 on the courier. The courier invites its configured operators and enables room encryption before inviting them when its e2ee setting is on. Two CLI sessions in the same checkout get separate rooms. Reconnecting, including after --session or --resume, reuses the session’s room on that bot. The working directory is room context, not its identity.

Set remote_control.bot_id to select a default bot. An explicit [room] or remote_control.room still attaches an existing room accessible to the bot. Rooms with delegated work in flight or another attached CLI are refused.

/clear (also /reset and /new) starts a fresh CLI conversation. While remote control is connected, it creates a new session room and posts its link in the old room. At the local prompt, the new room is created on the next /rc. The previous session and room remain available to resume. Disconnected session rooms ask you to resume the CLI and run /rc; they do not start a separate bot conversation. Explicitly attached existing rooms retain their normal bot behavior.

In the room, send text tasks, /auto [--exec] [goal], /auto status, /auto resume, or /max-tokens [N|default]. The bot relays model replies, tool previews, and errors. Approval prompts require /approve <request-id> or /deny <request-id> from a configured operator. Each answer applies only to that pending request; ordinary conversation cannot approve a tool. Attachments and other slash commands are currently unsupported.

/stop interrupts at the next tool or completed-batch boundary. It does not kill a running shell command or an in-flight model request. Send /remote-control off to stop at that boundary and return control to the terminal. Ctrl+C at the terminal also disconnects; a stopped turn retains the completed tool history. In a managed session room, threaded messages enter the same CLI conversation, including approval and control commands; replies appear in the main timeline. In an explicitly attached existing room, only the main timeline is attached and threads keep their own bot sessions. Other senders cannot control the CLI, but room members can see its public output.

Remote operators act with the CLI’s channel policy. A Matrix session’s supervisor gate is not added to the CLI; /auto still uses its own monitor. Approval prompts include full tool arguments, including file bodies. Treat the configured operators as people authorized to control this CLI session. Room prompts and replies also enter the bot’s normal conversation archive.

The local mailbox lives under Corteza’s user data directory, in remote-control. CORTEZA_REMOTE_DIR can relocate it, with the same setting in both processes. Records remain available for diagnosing failed delivery; the bot retries unsent output without resubmitting model work. An ambiguous send failure or crash before recording its event ID can duplicate a post. If the courier disappears, the CLI returns to its local prompt when it next waits for input or an approval.

Session rooms accumulate: each /clear creates another room. Rooms, local room bindings, mailboxes, and delivery/deduplication markers are retained indefinitely. There is no automatic cleanup or grouping into Matrix spaces yet.

Response output limits

max_tokens is the per-response output budget. /max-tokens shows the active budget; /max-tokens 32768 changes it for the current CLI or chat() session. /max-tokens default clears that session override and any learned budget.

When Anthropic’s unset default of 4,096 tokens cuts a response short, Corteza retries the unfinished response with 8,192, 16,384, then 32,768 tokens. Completed tool calls stay in history and are not replayed. Partial calls are not executed. A successful larger budget is reused for that provider/model in the session.

output_retries defaults to 3 (0 disables recovery; maximum 10). output_retry_max_tokens sets the ceiling. An explicit max_tokens is also the ceiling unless output_retry_max_tokens explicitly permits a larger one. For providers with no known default, recovery requires an explicit starting budget and ceiling. Codex does not support this output-budget setting. Streaming consumers do not retry text they may already have delivered.

Exhausting recovery prints a clear stop and the budget command. An unattended run stops rather than treating the incomplete response as progress; change the budget and use /auto resume. Output cutoffs are separate from context window exhaustion and do not trigger summary compaction. If recovery ends with a partial reply, the rendered text is kept in conversation history and the journal. A later turn can refer to what the user actually saw; unfinished tool calls are still discarded.

Conversation context editing

Context editing is experimental and off by default. Enable it in the global or project config.json:

"context_edit": {
  "enabled": true,
  "cadence": "pressure"
}

Project fields override global fields within this block. cadence is "pressure" (allow edits after the context_warn_pct notice, 75% by default) or "always" (allow edits after each complete tool batch). Existing threshold compaction remains the fallback at context_compact_pct, 90% by default.

/context-edit on|off|status controls the current session in the CLI, chat(), and Matrix rooms. The CLI also accepts --context-edit. R callers can pass context_edit = TRUE, FALSE, or list(enabled = TRUE, cadence = "always") to new_session(). A NULL override inherits configuration. Spawned subagents inherit the parent’s effective setting. /status reports the effective mode, including when an enabled setting is unavailable on an unsupported provider or because the journal is disabled.

On Anthropic and OpenAI chat-completions history shapes, the model receives an edit_context tool and visible IDs on completed tool outputs ([rN]) and earlier assistant text ([mN]). The tool supports:

Visible IDs survive session resume and provider changes. Label-only updates and rejected edits do not suppress archival; applied edits and compaction still skip archival for that turn because its live message positions changed. Invalid context_edit configuration emits a warning and disables the feature.

For an opt-in trial without automatic summaries, add "strategy": "clm" and use "cadence": "always". This disables threshold compaction, forced summary recovery on context overflow, and holder archival. The model must manage the window before it fills; exhaustion reports an error and retains the journal. In a room, later messages can also overflow the same full window. Use /clear to start a fresh working window; the journal retains the previous record. Unsupported providers are skipped in a pure CLM fallback chain. The package default remains the existing threshold-compaction behavior.

Doers can select this policy independently of their conversation session:

"jobs": {
  "doer": {
    "context_edit": {"enabled": true, "cadence": "always", "strategy": "clm"}
  }
}

A doer inherits the conversation’s effective editing settings, with jobs.doer.context_edit taking precedence. Reviewers and approval monitors retain their existing configuration. Doers preserve their complete record in the session journal; they do not file context into a vault.

For a Matrix talker, configure the policy and an explicit vault in the Matrix account’s talker block:

"talker": {
  "enabled": true,
  "context_edit": {"enabled": true, "cadence": "always", "strategy": "clm"},
  "context_vault": "/path/to/this-bots/vault"
}

For an unsplit Matrix bot, put context_edit and context_vault at the account config’s top level instead. Its existing model and tools stay in place; the combined conversation/work session owns filing. In split mode, the talker block overrides those account settings. Workers inherit only the editing policy, never the vault or conversation-owner role. Invalid conversation policies or vault configuration warn and disable context editing for that session. Vaults must have schema.md and must not be adopted read-only.

Set "retain_turns": 10 inside the conversation owner’s context_edit block to protect the previous 10 human-led turns plus the current turn. Each protected turn includes its human message, assistant replies, and tool traffic. Only older entries receive editable IDs; edits are checked again at the checkpoint, so previously issued IDs cannot bypass the retention setting. Tool-result messages do not count as human turns. The default is 0 (the existing eligibility rules). This setting applies to split talkers and unsplit room conversations. Doers, reviewers, and monitors do not acquire a retention floor through inheritance. It limits CLM edits, not threshold compaction in the hybrid strategy.

Only a conversation owner with a configured vault may retire older human turns, labeled [uN]. It can drop them, but cannot replace them with model-written words. The latest human turn, instructions, and current tool batch stay protected. On drop or replace, the checkpoint first files the original entries and the edit’s optional text note into immutable Markdown and provider-native JSON under raw/chats/. Paired tool messages are included as evidence. Replacing a working note also preserves the previous note. Filing uses no extra model call and does not commit or push the vault. It updates the vault index through pensar; it does not generate a wiki synthesis or post a recap.

The journal commits the context change only after filing succeeds. A failed vault write rejects the edit with a tool receipt and continues the turn with the original entries intact; a failed journal commit can leave an unused archive, which a retry reuses. The conversation’s edited history, consumed Matrix event IDs, and transcript ledger restart together from the journal, so backfill skips already-consumed events. Rooms and threads retain separate journals. /clear explicitly clears that saved working window.

Topic retirement is a choice in the conversation owner’s normal turn. It can retain open topics and commitments in its working note while filing completed topics, then use the archive path in the tool receipt to read an earlier topic when needed. This does not create or move Matrix rooms.

Outside the vault-backed conversation policy, all human messages are protected. Messages mixing human text and tool results remain protected in every mode. Instructions and tool schemas are outside the edit surface. The current assistant message and its results cannot be edited until a later batch. Older assistant messages containing non-text blocks are also excluded from message-level editing. Threshold compaction keeps its existing behavior and can still summarize human messages.

Requests queue during tool execution, then apply atomically at the checkpoint after the batch completes and after the compaction check. Stale or overlapping requests are rejected as a batch. Edits require journaling: a failed journal write prevents the live-history replacement. Edit segments store operations, removed/replaced entries, and references to shared immutable entries under <id>.journal/entries/; journal_load() and journal_history() reconstruct the same full record as ordinary segments. CLI and room replies report the edit and estimated history tokens before and after it; structured context_edit events include operations, IDs, cadence, and application status.

This feature does not support the OpenAI Responses/Codex history shape. It does not change instructions, publish data, or run an additional model call.

OpenAI-compatible endpoints

The openai_compatible provider targets any endpoint that speaks the OpenAI chat-completions wire format: OpenRouter, DeepSeek, DeepInfra, a local proxy, or a corporate gateway. It separates three things the named providers hardcode:

base_url is used as given with /chat/completions appended, so include whatever /v1 prefix the gateway expects.

{
  "provider": "openai_compatible",
  "base_url": "https://openrouter.ai/api/v1",
  "model": "meta-llama/llama-3-70b-instruct"
}

The API key is read from OPENAI_COMPATIBLE_API_KEY, falling back to OPENAI_API_KEY. A keyless gateway (an internal proxy that needs no Authorization header) works with neither set. Equivalent one-liners:

corteza --provider openai_compatible \
  --base-url https://openrouter.ai/api/v1 \
  --model meta-llama/llama-3-70b-instruct
# base_url comes from config or OPENAI_COMPATIBLE_BASE_URL
corteza::chat(provider = "openai_compatible",
              model = "meta-llama/llama-3-70b-instruct")

A missing endpoint or model is reported at startup, not at the first request. Cost is usually NA, since gateway model ids rarely match the bundled price table. Requires llm.api >= 0.1.9.

JSON config keys

All keys shown with type and default, current as of corteza 0.6.3. Most defaults live in R/config.R::load_config(); a few (memory keys and the CLI port fallback) live in inst/bin/corteza.

Core

Key Type Default Description
provider string "anthropic" LLM provider: anthropic, openai, moonshot, openai_codex, ollama, openai_compatible
model string or null null Model name (null = provider default; required for openai_compatible)
base_url string or null null Endpoint for openai_compatible (see below)
port integer 7850 MCP server port

Context

Key Type Default Description
context_files string[] [] Extra files to load into the system prompt
context_warn_pct integer 75 Token-usage % to start showing warnings
context_high_pct integer 90 Token-usage % for orange indicator
context_crit_pct integer 95 Token-usage % for red indicator + hint to /clear
context_compact_pct integer 90 Auto-compaction threshold
context_compact_bytes integer 900000 Serialized-history compaction guard for the Codex transport
context_request_buffer_retries integer 3 Forced compact-and-continue attempts after a Codex request-buffer error
journal_enabled boolean true Keep the provider-native history journal (<id>.journal/: a live snapshot plus a segment per compaction) beside the session transcript
audit.enabled boolean true Record turns, tool decisions and room deliveries independently of display verbosity and journal_enabled
audit.limits object see below Bound raw payload capture independently of model context caps
context_edit object {"enabled": false, "cadence": "pressure"} Model-requested conversation edits; cadence is pressure or always, and journaling is required
tool_output_caps object {} Per-tool result caps, e.g. {"game_action": {"max_chars": 400000, "max_lines": 20000}}; other tools keep the 50-line / 5000-char cap
context_include_soul boolean or null null Include SOUL.md (null = saber default)
context_include_user boolean or null null Include USER.md (null = saber default)
instruction_roots object {} Additional named roots for lazily loaded SKILL.md instructions
instruction_disabled string[] [] Exact stable instruction ids to omit

Audit streams are private JSONL files under <journal_dir>/audit/, with one writer per stream. Sessions without a journal use the package data directory’s audit/ folder. Set audit.enabled to false to disable recording; embedded callers may set session$audit_dir or session$audit_enabled explicitly.

Room sends and edits record a delivery start and ending, every returned event ID, destination, and hashes of the generated and posted text. They work without a vault. A missing event ID is unconfirmed; an error retains its condition class. Recording failure never retries a send. Reply, worker-progress and remote-control records link their originating turn where it is available; administrative sends without a turn retain a null reference. The existing conversation transcript still uses its last-event deduplication convention.

New transcript and trace files are created with mode 0600 under private directories. Appending narrows an existing file to 0600 before writing. The trace remains a preview: rows carry truncated and separate argument/result flags with the character limits. Journals retain the original history. The terminal turn record owns usage. Transcript and job-outcome copies are marked derived and link to it; do not add these copies to turn totals. Unknown usage is null. Older transcript zero blocks cannot establish measured zero.

These records are observational: a write failure emits one notice, and recovery writes a gap with the missed-record count and interval. A process that exits before recovery can leave an unresolved ending. Existing job, approval, and journal commit requirements still apply. With llm.api 0.1.9.15, each provider request records the final adapter-built body, raw usage and transport attempts. Older llm.api versions remain supported and report request coverage unavailable. Requests break down a turn’s usage; do not add them to its canonical total.

Payload limits default to 16 MiB per full tool result (result_bytes), 256 MiB per turn (turn_bytes), and 2 GiB per writer’s store (quota_bytes). Requests, message components and tool output are stored by SHA-256 under each stream’s payload store. Explicit omission records name the limit reached or a capture failure. audit_payload_pin() protects incident/evaluation evidence from explicit expiry; pinned bytes still count toward quota. audit_payload_expire() defaults to a dry run with a 30-day horizon. It writes expiry records before deleting unpinned payloads. There is no automatic pruning of payloads or event metadata. The 365-day metadata retention value is planning guidance, not an automatic deletion policy. See vignette("audit-records") for the record format.

Safety

Key Type Default Description
approval_mode string "ask" "ask", "allow", or "deny"
dangerous_tools string[] ["bash", "run_r", "run_r_script", "write_file", "replace_in_file", "base::writeLines"] Tools requiring approval
permissions object {} Per-tool overrides, e.g. {"bash": "deny"}
denied_paths string[] ["~/.ssh", "~/.gnupg", "~/.aws", "~/.config/gcloud", "~/.kube", "~/.docker"] Blocked paths
allowed_paths string[] or null null If set, only these paths allowed

Supervisor

A Matrix room’s tool calls, and those of the jobs it delegates, run with nobody at a prompt. Each call goes through three steps:

  1. Rules in code. They read the call, shell commands and R code included. Credentials, writes outside the project, elevated privileges, other machines, publishing, and git commands that throw work away are put to a person. No model is asked about these.
  2. A monitor, where the bot config sets auto_approve_asks: true. It is a model with read-only tools in its own process, started with the shared and project instructions. It rules on what the rules left open: it approves the call, refuses it with a reason the working agent reads, or passes it to a person.
  3. A person, by reaction in the room, for what steps 1 and 2 sent on. Without auto_approve_asks a person is asked in place of step

No setting approves a call unseen. The rules read text and are not a sandbox: a command can be written so that they miss it, which is what the monitor and the tool policy above are also there for.

These keys are read from the user’s config file only. A project’s .corteza/config.json cannot set them.

Key Type Default Description
supervisor.provider string the working agent’s Provider for the monitor
supervisor.model string the working agent’s Model for the monitor. A small, fast model keeps each ruling to a few seconds
supervisor.thinking string provider default Anthropic thinking.type for the monitor
supervisor.reasoning_effort string provider default Reasoning effort for the monitor
supervisor.timeout_sec number 120 A monitor that has not ruled by then is stopped and the call goes to a person
supervisor.max_turns integer 6 Model turns the monitor may take on one call
supervisor.write_roots string[] [] Directories outside the project that may be written without asking a person, such as a notes directory

Skills

Key Type Default Description
skill_paths string[] [] Legacy executable/file-skill configuration
skill_packages object[] [{"package":"base", ...}, {"package":"utils", ...}] R packages registered as tools
skill_timeout number 30 Default skill execution timeout (seconds)
skill_timeout_max number 1800 Host ceiling for a model-requested supervised run_r timeout
run_r_mode string "in_process" "in_process" preserves the caller’s R workspace; "worker" uses a private persistent callr::r_session with enforceable deadlines
run_r_worker_options object {} Named arguments for callr::r_session_options() when run_r_mode is "worker": env, libpath, cmdargs, arch, …

skill_timeout is the default for ordinary skills and for self-bounded tools such as bash, cmd, and run_r_script. An explicit tool argument still wins. Those self-bounded tools enforce the deadline in processx/callr rather than via R’s process-global setTimeLimit().

The default run_r_mode = "in_process" preserves the historical embedding contract: tool_run_r(code, envir = globalenv()) and existing CLI/chat sessions continue to use the host process. That is the fastest path and shares the caller’s live .GlobalEnv, which is what an interactive session usually wants.

Any session, interactive or unattended, can opt into "worker" instead. Choose it when you want a real wall-clock kill for hang-prone or untrusted R, where setTimeLimit() in the host process is not safe, or crash isolation so a segfault or a runaway allocation takes down a disposable child rather than your session. The cost is per-call serialization and a workspace kept separate from the host’s .GlobalEnv, so it is an opt-in for safety, not the interactive default.

Each worker session owns one persistent callr::r_session: R objects, attached packages, the working directory, helpers, and handles survive between run_r calls, but do not enter the host’s .GlobalEnv. The worker starts with R’s normal default packages (utils, stats, methods, and the rest), so helpers like str(), tail(), and setNames() work exactly as in a standard session; set R_DEFAULT_PACKAGES in run_r_worker_options$env to override. A model may request a different timeout on an individual call; skill_timeout_max is the host-owned ceiling. A host may narrow it further for an expiring external lease.

run_r_worker_options shapes that worker process without touching the host: a private environment or library path, extra command-line arguments, or arch, which callr resolves to R.home("bin")/<arch>/R. A strict host can install a wrapper there that starts R inside a sandbox (bubblewrap, say), so only the model’s R runs confined. env replaces callr’s default (TERM=dumb) rather than extending it.

Every tool result is capped at 50 lines or 5000 characters before it reaches the model (content reads such as read_file get a larger budget), with the full text kept behind a handle. tool_output_caps raises the cap for named tools whose results the model must see whole: a host-supplied game board, a table, a structured report. Past the raised cap the result still stashes to a handle.

For a changing deadline, an embedded host may set session$run_r_timeout_cap <- function() seconds_remaining before calling turn(). The callback takes no arguments and returns a non-negative number of seconds. The standard dispatcher evaluates it before every supervised R call; zero refuses execution. A numeric value also works. This host-owned cap can only shorten the configured or requested timeout, and needs no custom executor.

On a clean interrupt, assignments completed before the deadline remain in the worker for inspection. If the worker cannot stop cleanly, corteza terminates it and reports that its in-memory state was lost. In-process mode rejects the new model-facing timeout argument because it cannot enforce it without risking a late interrupt in unrelated host code.

Diagnostics

Key Type Default Description
dry_run boolean false Preview tools without executing
trace boolean false Emit structured trace events

Rate limiting

Key Type Default Description
rate_limits object {} Per-provider limits

Subagents

Key Type Default Description
subagents.enabled boolean true Enable subagent commands
subagents.max_concurrent integer 3 Max parallel subagents
subagents.timeout_minutes integer 30 Subagent kill timeout
subagents.allow_nested boolean false Allow nested subagents
subagents.default_tools string[] ["read_file", "grep_files", "r_help", "web_search", "fetch_url"] Tools available to subagents when no preset is given
subagents.base_port integer 7851 Starting port for subagent MCP servers

Presets. /spawn --preset <name> picks a fixed tool list at spawn time:

/spawn --tools <comma-list> overrides the preset with an explicit tool list.

Permission model. Subagents have no interactive approval channel back to the parent or user. A child’s tool list is fixed at spawn time — via the --preset or --tools flags on /spawn, or the preset / tools arguments to subagent_spawn(). There is no mid-run path to escalate to a riskier capability: the child’s approval callback denies by default, and the parent cannot grant new tools after the fact. If a task may need shell, write, or network capability, choose a preset or explicit tool list that includes it at spawn time; otherwise the child should report that it is blocked.

Workspace

Key Type Default Description
workspace.enabled boolean true Enable managed workspace
workspace.budget_chars integer 32000 Context budget in characters
workspace.capture_results boolean true Promote large results to handles
workspace.max_result_size integer 50000 Max result size before handle promotion
workspace.scan_globalenv boolean true Scan .GlobalEnv on startup
workspace.scan_max_bytes integer 52428800 Max bytes to scan from .GlobalEnv (50 MB)
workspace.max_object_summary_chars integer 2000 Max summary length per object

Channels

Chat channel

The chat channel requires chat.api (Suggests), which carries the transport adapters. Configured via a separate helper, not the main JSON config.

corteza::bot_configure(
  server = "https://matrix.example.com",
  user = "corteza_bot",
  password = "verysecure",
  room = "#general:example.com",
  model = "llama3.2",
  provider = "ollama"
)

Legacy memory

Key Type Default Description
legacy_memory_tools_enabled boolean false Show memory slash commands
memory_flush_enabled boolean true Auto-flush before compaction
context_include_memory_logs boolean false Include daily logs in context

Slash commands

In-chat commands prefixed with /.

Command Description
/quit, /exit, /q Exit corteza
/status Runtime and session status
/doctor Check provider, git, MCP, context health
/tools List available tools
/diff [ref] Git diff against HEAD or ref
/review [ref] Review local changes with LLM
/config Active runtime configuration
/permissions Tool approval and sandbox settings
/clear Clear conversation (keeps session)
/compact Summarize conversation to free context
/sessions List sessions
/context Live context usage and loaded files
/model List current/configured models and the active provider’s catalog
/model <name> [provider] or /model <number> Switch model, optionally provider; numbers select from the last displayed list
/effort [level \| default] Show or change reasoning effort for this conversation; default restores the configured/provider default
/provider <p> Switch provider
/dryrun Toggle dry-run mode
/trace [N] Last N tool executions
/skill list List installed skills
/skill install <path\|url> Install a skill
/skill remove <name> Remove a skill
/skill test <path> Run skill tests
/spawn <task> Spawn a subagent
/agents List active subagents
/ask <id> <prompt> Query a subagent
/kill <id> Terminate a subagent
/remember <fact> #tags Remember with tags
/remember --global <fact> Remember globally
/recall <query> Search memories
/recall --tags List memory tags
/flush Flush memories to daily log
/last [N] Show Nth most recent tool output
/outputs List recent tool outputs
/help Show help

The model menu is shared by the CLI, chat(), and Matrix rooms. With a version of llm.api that exports list_models(), it queries the active provider’s catalog (OpenAI, Anthropic API, Moonshot, Ollama, or a compatible gateway). Codex subscription entries come from its local models_cache.json and are identified as cached. Catalog membership is not a tool-support check. If discovery fails or is unavailable, the current and configured models entries remain selectable. Refresh the list with /model; numeric selection uses the last displayed list.

/effort reports the current reasoning setting and /effort high changes it for the current conversation. /effort default removes the session override and uses configuration, or the provider default when none is configured. The provider validates which levels its model accepts. In a split bot this changes the talker, leaving the doer’s effort unchanged. These overrides are not global configuration changes; use reasoning_effort (or talker.reasoning_effort for a split bot) for a startup default.

GPT-6 tool calls (including Astra and Sol) require OpenAI’s Responses API. The corresponding llm.api routing update selects Responses with web search off as well as on. Older OpenAI models keep their Chat Completions route when search is off, and openai_codex always uses Responses. Corteza’s context-editing tool currently excludes Responses histories, so CLM edits remain unavailable on that wire. With llm.api’s agent_uses_responses() export, Corteza checks the selected model and search setting before the first request, including fallback candidates. Pure CLM rejects unsupported routes before generation; hybrid mode keeps automatic compaction and omits the editing tool. With older llm.api versions, the preflight follows their search-only OpenAI routing rule.

Instruction skills

Instruction skills are exact SKILL.md files discovered recursively at session startup. Built-in roots are:

Scope Path
Global tools::R_user_dir("corteza", "data")/skills/
Project .corteza/skills/

Only compact metadata enters the system prompt. The agent calls the read-only skill_instructions tool with the displayed stable id when it needs the body, or supplies a relative resource path for a supporting file. The catalog and hashes are fixed for the session; restart or an archival context refresh creates a new snapshot.

Additional roots must be explicit and named:

{
  "instruction_roots": {
    "shared": "../../skills",
    "team": "/srv/team/instructions"
  },
  "instruction_disabled": [
    "shared:deploy/production"
  ]
}

An entry at <root>/analysis/logs/SKILL.md has id <root-id>:analysis/logs. A SKILL.md directly in the root has the root id. Project roots override global roots with the same id; disabled ids from both configs are combined and cannot be re-enabled by the project. Relative paths in global config resolve from the global config directory; project paths resolve from the project working directory. The built-in ids corteza-data and project are reserved. corteza does not implicitly scan ~/skills.

A symlink directly under a root is an alias: <root>/saber -> ~/saber/inst/skills/saber yields id <root-id>:saber, and the target is walked as its own bounded tree. When two roots reach the same SKILL.md, the more specific root owns the entry. Symlinks below the top level must stay inside their root.

Instruction documents are not executable tools. They can teach the model to use tools it already has, but do not add capabilities to the executable tool registry.

Package skills: register R packages as tools via skill_packages in config.

MCP server

Expose corteza tools to external MCP clients. serve() supports two transports:

# stdio transport (Claude Desktop, Claude Code, other MCP clients)
corteza::serve()

# socket transport (corteza CLI, R-to-R clients)
corteza::serve(port = 7850)

# restrict the toolset
corteza::serve(port = 7850, tools = "core")

port = NULL (the default) selects stdio. cwd = NULL (the default) leaves the working directory alone; pass an explicit path to anchor the server elsewhere. Tools execute in a persistent R session, so objects and loaded packages survive across calls.

Claude Desktop config (~/.config/claude/claude_desktop_config.json):

{
  "mcpServers": {
    "corteza": {
      "command": "Rscript",
      "args": ["-e", "corteza::serve()"]
    }
  }
}

Session tuning

new_session() is the programmatic entry point for embedding corteza in your own R code: a Shiny app, a batch script, a custom approval flow, or tests. The CLI and Matrix channels build on this same primitive.

library(corteza)

s <- new_session(
  channel = "cli",
  provider = "anthropic",
  max_turns = 20L,
  verbose = FALSE
)

result <- turn("What packages are loaded?", session = s)

The session is an environment that carries history, the active provider/model, and the approval callback. turn() mutates it in place and also returns the updated session, so you can chain calls or persist s$history between runs.

Session parameters

Parameter Type Default Description
channel string "cli" "cli", "console", "matrix"
history list or null null Prior messages; resumes mid-conversation
model_map list or null null list(cloud = ..., local = ...); falls back to corteza.model_map option
provider string "anthropic" LLM provider
tools_filter string[] or null null Restrict available tools
system string or null null System prompt override
approval_cb function or null null Called when policy returns "ask". Default denies.
max_turns integer 10 Max tool-use turns per call
verbose boolean FALSE Print tool-call progress

Incomplete worker jobs

A worker that exhausts its tool-turn or output budget is recorded as incomplete, with its stop reason and usage, rather than done. Returned cancellation remains cancelled; exceptions remain failed. A model quoting a stop marker does not change the job’s status. Room and CLI notifications show the same outcome and retain the partial reply.

An incomplete attempt releases its checkout lock and does not queue automatic review. Its outcome is immutable. It saves a separate private snapshot of its completed native history and restorable R workspace. Later jobs may overwrite the warm-worker checkpoint, but cannot replace this per-job resume point. Failures to save a usable snapshot are reported without advertising resume.

Use /job resume <id> in the originating room, worker room, or CLI session, or explicitly ask a talker to continue that identified job with job_resume. Only the original requester or a configured bot operator may request a resume; seeing a job alone grants no such right. The worker conversation must be loaded for a cross-room request. Remote delegation and consultation jobs cannot be resumed through this local-worker command. CLI jobs remain owned by their launching process; a new CLI process does not automatically adopt an exited process’s jobs. Bot identities survive restarts.

Resume creates a new linked attempt, acquires the normal checkout lock, and restores the selected history in a fresh worker. It keeps the original tool restrictions, per-attempt limits, and review preference; approvals must be obtained again. The provider and model must match the snapshot. Checkout files are never restored: the worker is told to inspect newer changes before editing. Connections, external pointers, and transient tool handles are unavailable; omitted workspace objects are named in the continuation prompt. Missing or corrupt snapshots fail before a provider request.

Queuing the requested continuation does not require another default approval. Explicit per-tool ask/deny settings and user policy still apply to job_resume; the host also enforces requester, operator, conversation and workspace checks.

Each stopped attempt has at most one continuation. Repeating its resume command returns that continuation’s ID, including after restart. If that continuation also stops incomplete, resume its ID. The ledger retains per-attempt usage and cumulative usage across the chain. No automatic continuation or new spending policy is enabled.

Consulting another room

Set "room_consultation": true in a bot’s account config to expose ask_room(room, question, thread) when it answers a configured operator. This asks another conversation owned by the same bot. No shared room, second bot, Matrix self-mention, or additional listener is needed. The default is off. Use a room name, ID, or project directory, and optionally a destination thread root ID. Ambiguous names require an ID. delegate(room=...) still hands work to the destination’s doer; ask_room asks about its conversation instead.

The destination’s current text context, including human messages, assistant replies, and completed tool text, is captured when the question is submitted. Its conversational model answers in a separate process while both rooms and any doer remain available. The snapshot excludes images and hidden reasoning; it does not retrieve material retired into a vault. Missing context must be reported as missing. The consultation has no tools, web search, CLM edits, or ability to start another consultation. Its snapshot and outcome stay in the durable job ledger. Neither live conversation is replaced with the snapshot.

Questions and answers appear in both rooms and their conversation histories, including the originating thread. Enabling this setting allows operators to share information between the bot’s joined rooms, whose members may differ. Non-operators cannot request it. Plan mode and rooms attached to a remote CLI refuse consultations. job_status and job_cancel work from either conversation. Repeated pending questions reuse the same job. Consultations have a 120 second limit, one active query per destination, and at most 4 active queries per bot. Turning the setting off cancels pending queries. A restart leaves undispatched queries queued and marks interrupted queries indeterminate, without retrying the model call. Ordinary failed-message retries deliver results after an outage.

Delegation between bots

Conversation sessions can ask another configured bot to run a job using delegate_to(bot, task, review). This works with split talkers and unsplit bots. It uses the bot’s existing chat client and poll cursor. No extra Matrix login or listener is started. Configure outgoing destinations in the account config:

"delegates": {
  "helper": {
    "bot": "@helper:example.org",
    "channel": "!project:example.org",
    "transport": "matrix"
  }
}

The receiving account must explicitly allow the requester. An entry may narrow the worker’s tools or require a review:

"delegators": [
  {"bot": "@requester:example.org", "tools": ["read_file", "grep_files"], "review": true}
]

A string entry allows the worker’s usual tools. Both lists default to empty. The originating conversation must be answering a configured human operator. The receiving room determines the project directory, doer configuration, supervision, approvals, and checkout lock. Requests carry a self-contained task of at most 7000 UTF-8 bytes; tasks are rejected rather than shortened. The destination and its room members can read the task and reports.

The requester records a remote job, with no local worker or checkout lock. The worker records an ordinary job, with the same doer/reviewer lifecycle as local delegation. Duplicate delivery reuses that job. Results are authenticated against the recorded peer, transport, channel, and job ID; a late result cannot overwrite a terminal outcome. Results and any later review return to the originating conversation. Long reports are bounded for the transport; the full report stays in the worker’s ledger. job_status and job_cancel are available alongside delegate_to, including on an unsplit bot.

Protocol notices start with [corteza-job v1 ...] and are consumed before the conversation dispatcher. They mention their recipient and use threads only when supported. A bot cannot approve a delegated job’s tool calls, including when it was accidentally listed as an operator. Human approvers can react or send approve REQUEST_ID / deny REQUEST_ID; the prompt supplies the ID. Unverified encrypted senders are rejected. Requests are bounded by hop counts and active root IDs, and a room attached to a remote CLI refuses new jobs.

Unreceived requests expire after 2 minutes. Dispatched requests without an acknowledgment are probed after 2 minutes; accepted jobs are probed every 5 minutes. Restart also triggers a status probe. An unanswered probe becomes indeterminate after 2 minutes; the job is never resubmitted as new work. Cancellation before receipt is remembered so a delayed request cannot run. A completed job is retained in the ledger, including across bot restarts. These settings do not enable automatic recursive delegation by worker models.

Room progress and images

Room turns and delegated jobs show tool counts and failed or refused outcomes. With activity_detail: true, they also show commentary, command or path previews, and short result excerpts in expandable tool groups. The final answer is a separate message. Progress stays in the originating room or thread and uses notices so bots do not answer the activity trail.

Updates to the notice are coalesced by activity_interval in the bot config (15 seconds by default), with a final flush when the turn or job ends. Transports that cannot edit messages receive the completed trail once. Long trails show a bounded tail; full tool traffic remains in the session journal. Plain-text clients receive the same selected detail level without the disclosure widgets.

Image attachments are checked against the configured size ceiling after download. Their PNG, JPEG, GIF, or WebP signatures determine the format sent to the model, even when the uploader supplies the wrong MIME type or filename extension.

systemd service

bot_run() is designed for a systemd user unit.

~/.config/systemd/user/corteza-matrix.service:

[Unit]
Description=corteza chat bot
After=network.target

[Service]
Type=simple
ExecStart=Rscript -e 'corteza::bot_run()'
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target

Enable and start:

systemctl --user daemon-reload
systemctl --user enable corteza-matrix.service
systemctl --user start corteza-matrix.service

View logs:

journalctl --user -u corteza-matrix.service -f

Environment variables

Variable Required for Description
ANTHROPIC_API_KEY Anthropic API key
OPENAI_API_KEY OpenAI API key
MOONSHOT_API_KEY Moonshot API key
TAVILY_API_KEY web_search tool Optional
NO_COLOR CLI Disable ANSI colors
FORCE_COLOR CLI Force ANSI colors
CORTEZA_STATE_DIR Matrix bot Out-of-band signal directory

Set API keys in ~/.Renviron:

ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...

Room attachments

Incoming attachments retain a quoted receipt with their filename, declared type and size, sender, source event, and readability status. The transcript and journal keep that receipt even when the file cannot be read. In an exchange the bot is answering, it posts an explicit notice for unsupported, oversized, or unreadable files. PDFs and other non-image files are acknowledged without downloading or extracting their contents. Successful images still reach the conversation model. Delegation with attach_context = true carries the text and receipt, and explicitly says that image bytes are absent from that text.

Room conversations offer send_room_file(path) to configured bot operators. It sends an existing file to the current room using the host’s chat identity; the model does not load credentials or select another destination. Tool policy and supervision still apply. Credential files and runtime configuration files are refused, and the file limit is 25 MiB. Workers do not receive this tool.

Encrypted Matrix attachments require chat.api 0.1.0.4, mx.client 0.2.1.4, and mx.crypto 0.2.2.2. Uploads contain ciphertext; the file metadata travels inside the encrypted room event. Older adapters report that attachment sending is unavailable. File sends from threads are refused until the transport’s file contract can preserve their destination.

R options

Option Default Description
corteza.model null Default model (overrides provider default)
corteza.local_models c("gpt-oss:120b", "gpt-oss:20b") Candidates for default_local_model()
corteza.max_turns null Default max_turns for chat() (null falls back to 50)
corteza.trace FALSE Enable structured trace events

Set in ~/.Rprofile or per-session via options().


Guide version: 0.6.3