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.
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)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.
/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.
/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.
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.
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:
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:
"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.
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:
openai_compatible (the wire
format, not an official endpoint)base_url, from config or
the OPENAI_COMPATIBLE_BASE_URL environment variablebase_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.
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.
| 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 |
| 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.
| 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 |
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:
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.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 |
| 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.
| Key | Type | Default | Description |
|---|---|---|---|
dry_run |
boolean | false |
Preview tools without executing |
trace |
boolean | false |
Emit structured trace events |
| Key | Type | Default | Description |
|---|---|---|---|
rate_limits |
object | {} |
Per-provider limits |
| 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:
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 <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.
| 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 |
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"
)| 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 |
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 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.
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()"]
}
}
}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.
| 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 |
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.
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.
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 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.
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.targetEnable and start:
systemctl --user daemon-reload
systemctl --user enable corteza-matrix.service
systemctl --user start corteza-matrix.serviceView logs:
journalctl --user -u corteza-matrix.service -f| 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-...
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.
| 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