---
title: Configuration
---
# 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:
```json
{
"provider": "ollama",
"model": "llama3.2",
"context_files": ["README.md", "PLAN.md"],
"approval_mode": "ask"
}
```
Create the directory first:
```r
dir.create(".corteza", showWarnings = FALSE)
```
## CLI flags
```bash
corteza [options]
```
| Flag | Description | Default |
|------|-------------|---------|
| `--provider
` | LLM provider: `anthropic`, `openai`, `moonshot`, `openai_codex`, `ollama`, `openai_compatible` | `anthropic` |
| `--model ` | Model name | provider default |
| `--base-url ` | Endpoint for `--provider openai_compatible` | config / env |
| `--tools ` | Tool filter: `core`, `file`, `code`, `git`, `r`, `data`, `web`, `chat` | all |
| `--session ` | 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 ` 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: `.
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 `
or `/deny ` 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`:
```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:
- `drop`: remove the selected entries, keeping each tool call and result together.
- `replace`: summarize the selected entries in the first selected entry and
remove the others. A retained tool result keeps its original call paired.
- `note`: replace a working note containing task facts, decisions, and next
actions, placed before the latest human prompt so it cannot merge into the
current assistant message or split a tool call from its result. On the first
turn, a short context-workspace header keeps a user message at the start.
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:
```json
"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:
```json
"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
`.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:
- **provider** — `openai_compatible` (the wire format, not an official endpoint)
- **endpoint** — `base_url`, from config or the `OPENAI_COMPATIBLE_BASE_URL` environment variable
- **model** — passed through untouched; required, since there is no default
`base_url` is used as given with `/chat/completions` appended, so include
whatever `/v1` prefix the gateway expects.
```json
{
"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:
```bash
corteza --provider openai_compatible \
--base-url https://openrouter.ai/api/v1 \
--model meta-llama/llama-3-70b-instruct
```
```r
# 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 (`.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 `/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 2.
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")//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 ` picks a fixed tool list at spawn time:
- `investigate` (default): `read_file`, `grep_files`, `r_help`, `web_search`, `fetch_url` (read/search/help, plus network reads).
- `minimal`: `read_file`, `grep_files`.
- `work`: investigate + `bash`, `write_file`, `replace_in_file`, `list_files`, `git_status`, `git_diff`, `git_log`, `run_r`.
`/spawn --tools ` 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.
```r
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 [provider]` or `/model ` | 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 ` | Switch provider |
| `/dryrun` | Toggle dry-run mode |
| `/trace [N]` | Last N tool executions |
| `/skill list` | List installed skills |
| `/skill install ` | Install a skill |
| `/skill remove ` | Remove a skill |
| `/skill test ` | Run skill tests |
| `/spawn ` | Spawn a subagent |
| `/agents` | List active subagents |
| `/ask ` | Query a subagent |
| `/kill ` | Terminate a subagent |
| `/remember #tags` | Remember with tags |
| `/remember --global ` | Remember globally |
| `/recall ` | 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:
```json
{
"instruction_roots": {
"shared": "../../skills",
"team": "/srv/team/instructions"
},
"instruction_disabled": [
"shared:deploy/production"
]
}
```
An entry at `/analysis/logs/SKILL.md` has 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: `/saber ->
~/saber/inst/skills/saber` yields 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:
```r
# 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`):
```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.
```r
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 ` 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:
```json
"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:
```json
"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`:
```ini
[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:
```bash
systemctl --user daemon-reload
systemctl --user enable corteza-matrix.service
systemctl --user start corteza-matrix.service
```
View logs:
```bash
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*