CRAN release, batching the 0.7.1.1 to 0.7.1.94 development cycles below.
bot_configure(), bot_send(), bot_poll(), bot_run(),
bot_run_init(), bot_run_step(), bot_request_flush() and
bot_archive_all(); the matrix_* names still work and are deprecated.tool_delegate(), tool_job_status(), tool_job_cancel()
and tool_job_resume(). Unattended tool calls pass through rules in code,
an optional monitor, and a person./rc gives each CLI session a private room from which it
can be driven.audit_read() and the audit_payload_*()
functions.voice_serve() runs the AgentVoice gRPC service for one-to-one voice./auto runs, a continual harness of approved lessons, per-tool output
caps, and an opt-in supervised run_r worker./model lists models and sets reasoning effort; images in a room are
passed to the model as images.run_r returns what the code prints along with its value./call and /hangup are gone, and the voice.stt,
voice.tts, and voice.call settings are no longer read. The code is kept
on the livekitr-calls branch. AgentVoice (voice_serve()) is unchanged.Additional_repositories is removed.openai_codex, and
openai models that llm.api routes there) now closes its unfinished tool
calls with function_call_output entries. They were given Chat Completions
role = "tool" messages, which a Responses history does not accept./rc now creates a private Matrix room per CLI session by default and
reuses it when reconnecting or resuming. Explicit room overrides remain
available. The courier enables encryption before inviting operators when
configured for E2EE, and recovers rooms after interrupted creation/setup./clear, /reset, and /new in remote control start a fresh conversation
in a new room, leaving a link in the old room. Disconnected session rooms
ask for a CLI reconnect instead of starting an independent bot conversation.turn()$usage total when request records are partial;
terminal records retain both totals and identify the canonical choice.
Complete request coverage returns normalized input/output/total token,
cache and cost fields. Transcript usage is explicitly tagged llm.api;
its pi-style envelope does not imply pi usage field names.auto_log_read() now counts blank lines as malformed and reports their
positions rather than silently ignoring them./job resume <id> and talker job_resume for incomplete local
doer jobs. Save immutable per-attempt history and workspace snapshots, validate
identity and file integrity, and restore them in a fresh worker under the
normal checkout lock. Preserve newer checkout files and identify unavailable
workspace objects. Resume requests require the original requester or operator.cat("token:", value) through remote
approval while retaining credential-value and known-secret checks. Explain
credential and oversized-call refusals as declines, with no pending local
approval, and suggest credential references or smaller reviewable calls.send_room_file(path) for bot operators to send a requested attachment
to the current conversation through the host's chat client. Models and workers
do not receive chat credentials. Preserve tool policy and supervision, refuse
credential/configuration files, and record confirmed attachment event IDs.ask_room(room, question, thread) for one bot to consult its own
conversation in another room. Answer asynchronously with the destination's
conversational model and a snapshot of its current text context, with no
tools, recursive requests, or rewrites of its live window. Record the
question and answer in both conversations; retain cancellation and recovery.context_edit.retain_turns for conversation owners: protect the recent
human-led turns, including assistant replies and tools, from CLM edits.
Doers do not inherit this retention floor. The package default remains 0.chat(), rather than combining the saved provider with a configured model.
Honor explicit overrides; a provider change uses its matching configured
model or provider default.\u escapes
now, which R turns into the same characters at parse time. This clears
the one WARNING R CMD check had been reporting, and CI now fails on a
check WARNING (WARNINGS_ARE_ERRORS).bot_run_init() takes hooks, a named list of callbacks for policy the
loop asks the host about: on_clear(chat, cfg, room_id, title, vault_ref)
decides whether a cleared conversation becomes a room of its own, and
thread_context(chat, room_id, thread_root) names the archive a thread
continues. bot_segment_from_clear() and bot_thread_archive(), which
made that decision and read that index here, are gone: a bare bot
(bot_run(), or no hooks) archives and resets at /clear and never
rehydrates. The segment_rooms config gate moves with the segment code
into the host (cerebro)./sidebar [N] and "Let's take this offline" for copying recent
conversation into an operator's private room. Route active job progress,
approvals and results there, with private-destination checks and worker
retirement to keep subsequent shared-room work separate.
Import the excerpt as one user-role quotation with source attribution./approve-all [job-id] command
to authorize remaining requests for one active job. Add status and revoke
commands; preserve policy denials and per-call credential approvals, and
verify operator membership on use. Ordinary words and /auto do not grant it.activity_detail: true enables previews./model now lists current and configured models plus the active provider's
catalog, with numeric selection tied to the last displayed list. The CLI,
chat(), and Matrix rooms share the menu. Live discovery uses llm.api's
optional list_models() API; discovery failures keep configured choices./effort to show or change a conversation's reasoning effort.
/effort default restores its configuration/provider default. Split
talkers change independently of their doers.openai_compatible during model
discovery. Check Responses routing before offering context editing;
reject unsupported pure-CLM requests before generation and skip incompatible
fallback models./remote-control (alias /rc) to continue the live CLI or chat()
session from a Matrix room through a running local bot. Keep the same model, history,
workspace, and tool policy, with operator-only input and request-specific
approvals. Relay bounded progress and replies without a second sync client./auto, /max-tokens, /stop, and disconnect, with queued
output delivery and callback restoration when control returns locally./auto continues the current conversation. Add auto status and resume,
expose the execution setting, and hide tools unavailable to the auto monitor.read_handle as a read operation for approval policy on every
channel. Auto runs omit tools whose read/write/exec operation is unknown,
including unclassified package and MCP tools./max-tokens and report an unrecovered cutoff as a stopped auto run.context_edit.strategy = "clm" disables automatic summary compaction and
holder archival. Doers inherit the editing policy; reviewers and monitors
keep their existing configuration.context_vault before
removal. Original entries are preserved as Markdown and provider-native
JSON. Older human turns may be filed and dropped, never rewritten; the
latest human turn remains protected. Workers never inherit vault filing./clear.edit_context schema's required fields as a JSON array.
The single required field previously became a string, causing providers
to reject requests with context editing enabled before generating a reply.edit_context tool drops or summarizes completed entries and maintains a
task note while protecting instructions, human messages, and the current
tool batch. Edits commit through the session journal at a checkpoint;
compact edit segments share immutable entries, and failed writes prevent
the live rewrite. context_edit.enabled / cadence, session overrides,
/context-edit on|off|status, and --context-edit control the feature.
Anthropic and OpenAI chat-completions shapes are supported; threshold
compaction remains the fallback. Subagents inherit the effective setting,
and CLI/room reports and structured events show each applied edit.<id>.journal/ beside the transcript now holds live.json, the live
history as last mirrored after every assistant message and tool result,
and segment-NNNN.json, the exact history each compaction replaced,
written before the rewrite so a write failure blocks the compaction
rather than losing the prefix. The record is the segments past their
inherited heads plus the live tail: every entry once, no synthetic
summaries. The same record covers archival, subagents (whose per-turn
transcript slice now comes from the journal and is correct across a
compaction inside the turn), Matrix rooms and threads, and interrupted
turns. journal_enabled: false turns it off. This is the pattern a
downstream host already ran for long autonomous sessions, now built in
for every surface.fetch_url). The talker's
toolset gains fetch_url, so a question about a page the user names
(a URL, a GitHub issue and its comments, a raw file) is answered by
reading it, not by handing off a whole job. Without it, a talker asked
to read an upstream issue had no way to fetch it and escalated to
cloning the repository through the doer, which then tripped the
write-outside-project gate. The guidance tells the talker to fetch and
answer, and never to clone a repository just to read something.delegate gains attach_context:
the message the turn is answering is written beside the job as
context.md and the task points the worker at it, so the content never
passes through the talker's tokens.Matrix tool calls are supervised; auto_approve_asks no longer
approves everything. The setting used to answer "yes" to every call
policy asked about, in a room's turns and in its jobs, the
credential-path rule's "ask" included. Now every call in a room goes
through three steps. Rules in code read it first, shell commands and R
code included, and put to a person: credentials, writes outside the
project, elevated privileges, other machines, publishing, and git
commands that throw work away. With auto_approve_asks: true, what the
rules leave open goes to a monitor, a model with read-only tools and
the project's instructions in a process of its own, which approves,
refuses with a reason the working agent reads, or passes the call to a
person. A person answers by reaction in the room, as before; without
the setting a person is asked in place of the monitor. A job's result
says how its calls were answered. The monitor's model and any extra
writable directories are set under supervisor in the user's own
config (a project config cannot set them); see
vignette("configuration"). The monitor is started on first need,
retired when idle, and counted against jobs.max_workers like a doer
or reviewer. The rules read text and are not a sandbox.
A tool result that was cut for length can be searched.
read_handle gains op = "grep" (the lines matching pattern, with
line numbers and a count of how many lines were searched) and
op = "lines" (start to end, 40 at a time). The talker, the
reviewer, and the monitor now have read_handle: the talker was
shown the first 40 lines of a long result and told to read the rest
with a tool it did not have, and the other two had no way at all. The
notice on a cut result now names only a tool the session has, and
says so when it has none. In a Matrix bot each room keeps its own cut
results (named .o_001, ...), so one room cannot open what another
room's tools returned; they shared the process's store before.
Matrix approvals only accept an operator or the room's one human.
Any reaction other than the bot's own used to answer an approval prompt,
so a second bot in the room could approve this bot's tool calls.
Approvers are now the configured operators, or, with none configured,
the room's one human when it has exactly one. Other reactions are
ignored. A room with no possible approver gets a notice and the request
is declined. Bots not listed under bots count as humans.
Talker mode: delegate work and keep talking. With
"talker": {"enabled": true} in a bot config or the corteza config, a
session runs on a fast model (Haiku 4.5 for anthropic, gpt-6-luna for
openai, or talker$model) with read-only tools plus delegate,
job_status, and job_cancel. Delegated work runs on a persistent
per-session worker using the session's configured model. In Matrix the
bot keeps answering while jobs run, posts each result to the room or
thread it came from, and takes approvals as reactions on later polls.
In the CLI and chat(), results and approvals appear before the next
prompt; /jobs and /cancel manage them. Off by default.
The doer starts with the project's context. A delegated job's worker gets, after its role, what a session opened in its directory gets: the shared instructions file, the project's AGENTS.md or CLAUDE.md, the briefing, and configured context files. The task text is all a talker hands over, so without this a project's rules reached the doer only when the talker restated them. A reviewer gets the shared and project instructions after its own prompt, so it can say when the work broke one; its tools stay read-only and confined to the checkout.
A Matrix bot's rooms can hand work to each other. delegate
takes room (a room's name, id, or project directory). The job is
then that room's: it runs on that room's doer, in its directory, under
its checkout lock, with approvals asked there. That room is told when
the job is accepted, in the room and in its talker's history, and the
result is posted in both rooms. Either room can ask about the job or
cancel it. Only a configured operator's request is handed over. A room
whose topic names another directory than its running session works in
is not handed work until the two agree (/clear there starts a
session in the new directory). When no room matches, the talker is
told that every room was checked, shown the nearest names, and told
when the project directory exists with no room working in it; the
reply no longer lists every room, which a bot in many rooms had cut
short.
A job's result is retried until it is posted. A result, or the notice of a hand-off, that could not be sent (homeserver down, token mid-rotation) was dropped and reported as delivered. It is now kept in the state directory and retried by the poll loop with a growing delay, and set aside after a day. The job itself is never resubmitted.
A job runs in the directory its record names. A job queued for one
directory fails without running if its session has since moved to
another, and a live worker is reused only while it is in the session's
directory. /clear and /model no longer orphan a running job: the
job and its worker move to the replacement session.
A Matrix bot retires idle job workers. Each room and thread kept a
doer process, and a reviewer once it had one, for as long as the bot
ran. A worker idle for jobs$worker_idle_minutes (default 30; 0 to
disable) is now closed, and no more than jobs$max_workers (default
8) are kept, longest idle closed first. Both are read from the corteza
config for the bot's own directory. A worker running a job, or waiting
on an approval, is never closed, so the count can exceed the limit
while that many jobs run. The next job in that room starts a new
process that restores the worker's saved workspace and conversation.
A thinking setting names the Anthropic thinking type. thinking
in the corteza config, in a talker block, or under jobs and
jobs$reviewer, is sent as the request's thinking.type. Claude
Sonnet 5.5 thinks unless told otherwise and refuses "disabled";
"between_tools" is its setting for answering without thinking first,
as in "talker": {"enabled": true, "model": "claude-sonnet-5-5", "thinking": "between_tools"}. The value belongs to one model, so a
fallback to another model does not carry it and /model clears a
talker's. It cannot be combined with thinking_budget_tokens.
Requires llm.api 0.1.9.13, whose agent() takes thinking.
Jobs are durable and never re-run blindly. Each job's intent is
written before a worker sees it and its outcome beside it. After a
restart, a job that never started is still queued; one that started
with no outcome is reported indeterminate and not re-run. The worker
checkpoints its R workspace and history at each job boundary and a
replacement restores them. Jobs carry an owner, so bots sharing a
machine or a room never settle or start each other's jobs.
One writing job per checkout. A job that edits takes its checkout's lock (the git toplevel) and later writers queue until it ends, across every bot on the machine. Edits made outside jobs are not covered.
Optional review of delegated work. delegate(task, review = TRUE),
or "jobs": {"review": true} as the default, follows a finished job
with a review by a second worker that has read-only tools, no network,
no access outside the checkout, and no way to run code. It reads the
job's diff and report and answers with a verdict and findings. The
diff is taken between snapshots of the checkout as the job started and
ended, so it leaves out changes that were already there, and it covers
untracked files up to "jobs": {"review_snapshot_max_mb": 20} in total
(ignored files are never covered). Past that limit the two snapshots
still cover the same files: files that were untracked at the start are
left out of both, or the job's new files are left out of the end, and
the reviewer is told which. The checkout stays locked from the
start of the job to the end of the review. If the bot restarts in
between and another job takes the checkout first, the review is
cancelled instead of run on a changed tree.
"jobs": {"reviewer": {"provider": ..., "model": ...}} runs the
reviewer on a different model. The reviewer cannot run tests, and a
review does not start a revision by itself.
git_status, git_diff, and git_log no longer run commands or
read outside allowed_paths. A ref or path could carry shell
commands, a ref could be an option, and path could name any
repository on the machine. Git now runs without a shell, and a
session confined with allowed_paths is held to it, including inside
a subdirectory of a repository. Three things behave differently in
every session:
git_diff is a literal path. Globs and pathspec
magic such as :(top) are not interpreted.<rev>:<path>, the object id of a
file or a tree, a tag on one, and anything starting with - are
refused.post-index-change hook,
external diff, textconv, or gpg, and no fetch of objects a partial
clone lacks. A file under a clean filter (git-lfs) is compared as
its raw content, and a submodule by its commit only.Matrix /model now changes the model replies use. It set a field
turn() never read, so it only renamed the badge. The badge also
showed the provider's default model for sessions created from
cfg$model; it now shows the model in use.
voice_turn(), voice_turn_report()) now has a
second media backend: a call over LiveKit, where the bot is a
participant and hears one audio stream per person. The call runs in a
worker process beside the bot (R/call-loop.R, R/call-worker.R):
it cuts each person's audio into utterances by signal energy, posts
each to an OpenAI-shaped transcription route, answers through the
room's session, synthesizes the reply a sentence at a time through an
OpenAI-shaped synthesis route, and publishes it in 200 ms chunks.
Someone talking over the bot stops it within a chunk, cancels the
generation, and trims the reply posted to the room to what was heard.
With several people the bot answers when addressed by one of its
names or as a follow-up within 20 s; with one, always. The bot
process posts and edits on the worker's behalf and tells it the
call's media keys; the worker never holds a Matrix credential.
Configured under voice.stt, voice.tts and voice.call in the bot
config (see vignette("configuration")); needs livekitr (Suggests).
Each route speaks one of two wires: the OpenAI audio API, or
wire: "gpu-host", a gpu.ctl host's POST /infer spoken directly
(whisper and chatterbox entries; the bearer token from key_file,
the voice cloned from the reference WAV named by voice.tts.voice).
The bot joins a room's MatrixRTC call when someone else is in it (a
call button posts a membership event and nothing else; chat.api
0.1.0.1 reports those from the poll), in the rooms voice.call.rooms
allows, and hangs up when everyone else has left; /call joins by
hand and /hangup leaves. Joining goes through chat.api's call
surface (chat_call_join(), an encrypted client). The bot process
forwards the media keys that arrive on each poll to the worker and
shortens its long poll to half a second while a call is on, so the
worker's replies are posted promptly. voice.call.greeting is said
once on joining; voice.call.connect_delay_s holds the worker
between the membership and the media connection for clients that
only notice participants who arrive after them. Tried live against
FluffyChat 2.10 on a LiveKit SFU: the bot joins, is heard, and hears
the room once the peers' media keys arrive; the log names each key's
sender and recipients, never the keys.run_r releases its output sink when a worker call is interrupted.
The supervised worker aborts an over-deadline run_r with an interrupt,
which unwound past the stream cleanup and left a sink, its file connection,
and the temp file open in the persistent worker. With no ceiling, repeated
timeouts accumulate sinks, connections, and files over a long run. Cleanup
now runs on interrupts as well as on errors and normal returns./auto runs continuously when no --loops is given. Previously a
bare /auto <goal> stopped after the configured iteration cap (default
10). It now runs continuously, governed by the monitor subagent and the
resource caps -- tool calls, tokens, time, cost, and the stall guard --
rather than an iteration count, matching the "work until done, stop at
the boundary" model of an Auto approval mode. --loops N still sets an
explicit iteration cap. The run is never truly unbounded: max_tool_calls
and stall_loops stay finite by validation, so a continuous run always
stops on some cap or a monitor verdict.corteza.narration_streak tool-call turns, default 3) is reworded to
match.chat(), once
per model response and independent of the approval prompt -- previously it
surfaced only inside an approval prompt, so an auto-approved call (every
call in an unattended run) showed nothing. See cli_commentary_lines().task_create is accepted automatically instead of prompting a human (which
would hang an interactive run or default-deny a non-interactive one), and
the task guidance drops its "ask clarifying questions first / wait for plan
approval" steps in favour of the unattended flow. Attended sessions still
prompt for plan approval exactly as before.AUTO_STATUS: blocked is a recognised status: the loop treats it as an escalation and
stops immediately, rather than routing the request through the monitor as
ordinary progress where a continue verdict would run another turn on an
unanswered question. The autonomy prompt tells a blocked worker to report
it rather than emit continue with prose.run_r starts with R's default packages. A supervised worker
(run_r_mode = "worker") is a bare callr::r_session, which loads only
base plus callr's own tools, so common helpers from utils/stats/
methods (str, tail, head, setNames, capture.output, ...) were
missing, unlike in-process run_r which inherits the front end's default
packages. The worker now sets R_DEFAULT_PACKAGES to the normal default
set so both modes see the same base environment. A host that sets
R_DEFAULT_PACKAGES in run_r_worker_options$env still wins.run_r returns what the code prints. Text written with cat(),
print(), message(), or a warning comes back ahead of the final value's
print, like a console, in both run_r_modes. Streams interleave in
emission order, so a message() before a cat() prints before it.
Warnings are muffled only under the default warn < 2; with
options(warn = 2) a warning still becomes an error and halts. Output
written before an error is kept ahead of the Error: line. tool_run_r()
also returns an r_error flag so a caller can tell a failed evaluation
from a successful one, without changing the model-facing text or
isError. Previously only the final value's print was returned and
everything else was lost.config$tool_output_caps raises the 50-line /
5000-character tool-result cap for named tools whose results the model must
see whole (a host-supplied game board, a table), keyed by tool name with
max_chars and/or max_lines. Other tools keep the universal cap, and a
result past the raised cap still stashes to a handle.config$run_r_worker_options passes named
arguments to callr::r_session_options() for the supervised run_r worker
(env, libpath, cmdargs, arch), so a strict host can run only the
model's R under a sandbox wrapper at R.home("bin")/<arch>/R.run_r_mode = "worker" gives a session one private persistent
callr::r_session, applies skill_timeout, bounds model requests with
skill_timeout_max, retains partial state after a clean interrupt, and routes
handle inspection back to the owning worker. The historical in-process
tool_run_r(code, envir=) contract remains the default. Timeout results tell
the agent to inspect retained state and optimize, split, parallelize, or
approximate before retrying. The session's skill_timeout now also supplies
the default argument to self-bounded bash, cmd, and run_r_script calls;
explicit caller values continue to win.report = TRUE request asks for status, notes, findings, concerns,
deviations, open questions, and feedback. The parent still receives a
character reply for compatibility; the report is rendered for the model and
retained in a corteza_report attribute for R callers.query_subagent tool now defaults to wait = FALSE: the prompt
starts at once and collect_subagent fetches the reply, so several
subagents can work in parallel while the parent continues. With
wait = TRUE, subagent_query() fires the same one-shot call and
collects with a real deadline (timeout, default 60 s; Inf opts out
of the bound). A child that has not replied by then leaves the query
pending for subagent_collect() or subagent_kill() and the call
returns NULL, where it previously blocked for as long as the child
took. A timeout that is not a single non-negative number is refused
before the prompt is fired. /ask in the REPL says so and points at
/collect; the hall monitor reads a timed-out query as no verdict and
escalates, as its contract already said it should.temperature, allowing compaction to work with
subscription endpoints and models such as Claude Opus 5 that reject it.bot_default_cwd() capitalised the Matrix localpart, so a bot logged in
as @cornelius:cornball.ai fell back to an empty ~/Cornelius beside the
~/cornelius instance directory cerebro runs it from, and created it.
The fallback is now ~/cornelius. An instance directory with other
capitalisation, such as ~/LilCasey, was mismatched before and still is.instruction_roots entry may point at a skill directory
outside the root, such as a package's inst/skills/<skill>. corteza walks
the target as a bounded alias and derives the id from the link name, so one
flat hub of package links can serve corteza, Claude Code, and Codex alike.
Deeper symlinks must still stay inside their root, skill resources still
cannot reach outside their bundle, and a skill reached through two roots
stays a single entry owned by the more specific root.Context compaction now belongs to the shared agent lifecycle. turn()
checks the live context before a run and after each complete assistant/tool
result batch, so direct callers cannot bypass it. A single long autonomous
prompt can be split at a safe call/result boundary while retaining a recent
token tail; Anthropic, OpenAI Chat, and Responses/Codex native histories are
all pairing-aware. A real context-window overflow gets one compact-and-
continue retry without inventing a user message, and usage from both attempts
is retained. CLI and chat() manual/automatic compaction now use this same
path and durably record summaries through a pre-rewrite lifecycle hook.
Codex compaction omits unsupported sampling controls, and caught summarizer
failures are exposed through a lifecycle hook instead of remaining silent.
Compaction briefs can be domain-specific. new_session(compaction_prompt = )
lets a long-running host state what evidence and working state its summary must
preserve, while NULL retains corteza's existing coding-agent brief.
Context accounting understands Codex and provider products. Responses output, function-call results, and opaque reasoning state are no longer reported as near-empty history. Context-window lookup can distinguish the public API's model limit from the smaller ChatGPT Codex working window.
Instruction skills are now compact, lazy, and session-scoped. Startup
discovers exact SKILL.md files recursively, puts only their names,
one-line descriptions, and stable ids in the prompt, and exposes the
read-only skill_instructions tool for exact body or relative-resource
retrieval. Each CLI, console, Matrix room, and subagent owns an immutable
hash snapshot; changed, foreign, traversal, and symlink-escaping resources
are refused. Explicit instruction_roots merge by stable root id and
instruction_disabled ids union across global and project config.
Instruction documents remain separate from executable tools, and the
legacy full-body format_skill_docs() contract remains available.
System context is now a provenance-carrying saber manifest. Corteza
requires saber 0.7.2.3 at its published commit, renders compact shared and
project instructions through its manifest API, and retains the source
metadata on CLI, chat(), and Matrix sessions. /context reports included
and skipped sources, their token estimates, reasons, and paths/origins
without exposing their bodies. The migration removes corteza's obsolete
inlined saber::agent_context() fallback and the eager package-help layer
that duplicated tool schemas at substantial token cost; package help remains
available on demand. Claude-specific global instructions and basename-
matched memory are no longer guessed implicitly—operators can add any such
files explicitly through context_files.
run_r can now be capability-scoped without changing its default.
tool_run_r(code, envir = ...) accepts a caller-owned persistent R
environment whose assignments and large-result handles remain private to
that scope. Existing calls still evaluate in globalenv() and retain
workspace auto-capture; the model-facing tool schema still exposes only
code, so hosts—not models—select the execution boundary.
Limit failover now finishes interrupted tool runs without replaying
completed tools. Provider-native histories are bridged to portable text
when the next candidate uses a different wire. Matrix configs also honor
reasoning_effort and can set fallback_primary_retry_at (for example,
"Mon 03:00") so a limited primary is retried at its weekly reset while
the fallback serves requests in the meantime. Every fallback reply now names
the actual model/provider; crossing from subscription providers onto an
API-key provider produces an intentionally loud billable-usage banner.
new_session(cache = "5m") (or "1h", or "none") reaches llm.api::agent(cache = ),
with config$cache as the fallback. Anthropic-only, and stripped for
other providers -- per candidate during a fallback too -- so a codex
session never carries it and llm.api's off-Anthropic warning stays
meaningful. Validated as a closed set, because llm.api's match.arg
would otherwise partial-match "5" to "5m" silently. Billing-only:
the model sees byte-identical input, so this cannot change a result.
With llm.api 0.1.9.6, now the Imports floor, the marker covers the
message history as well as the system prompt, so each request in a
tool loop reads the previous request's context from cache and pays
fresh input only for what was appended since. The floor is raised
because an older llm.api marks the system prompt alone and would
silently deliver a fraction of that.reasoning_effort now works on Anthropic too. Claude's depth
control is output_config.effort, not the thinking budget, and its
scale is "low", "medium", "high", "xhigh", "max" -- the API
names it in the 400 when refused. Rather than add a second setting,
reasoning_effort is routed to whichever field the provider's wire
uses, so a session sets it once and it means the same thing on either
family. Routing is symmetric, because a limit fallback re-routes per
candidate and a one-hop-only value would silently vanish on the
provider that answered. thinking_budget_tokens is unchanged and
still Anthropic-only; the two are independent.API error (400): Invalid value: 'thinking' -- and since a
400 is not a limit error, that ended the turn instead of continuing
down the chain. A Matrix bot on anthropic_claude hit this the
moment an Anthropic usage limit sent it to its openai_codex
fallback mid-conversation. Candidates whose wire cannot replay the
history's shape are now skipped (with a message), so the walk reaches
one that can.new_session() takes
reasoning_effort ("low"/"medium"/"high", forwarded on the
openai and openai_codex wires) and thinking_budget_tokens
(Anthropic extended thinking). Both resolve session-field-then-config
like max_tokens, so reasoning_effort or thinking_budget_tokens
in .corteza/config.json reaches the CLI, chat(), and Matrix
surfaces without further wiring. Each is dropped for providers whose
wire cannot carry it -- including after a limit fallback rewrites the
provider, where an effort setting meant for codex would otherwise
reach the Anthropic Messages body as an unknown field and 400..corteza/harness.json travels with a cloned repo, so it
is third-party data: entries are validated as they load, and
project lessons render in a separate "Untrusted project notes"
block that tells the model to treat them as reference material and
never as instructions, unless the project is trusted via
harness_trust_project in the global config. Rollback is now a
compare-and-swap that refuses when an entry changed after the
refinement being reverted, instead of silently destroying the later
edit. Store writes use a unique temp file, check file.rename(),
and hold a lock across the whole read-modify-write so concurrent
sessions can't discard each other's entries. /refine shows
evidence, kind, and path with each proposed edit and enforces the
8-edit maximum in code.harness_auto: config flag for recording lessons without
prompting (implemented as the default for the harness_note
permission, so there is one enforcement path; explicit permissions
still win). Auto-recorded notes remain in the ledger and reversible.cwd, so
harness_note wrote to the process working directory instead of
the session's project. Found by the new end-to-end approval tests.harness_note(title, fact, evidence) is a tool
the model calls the moment it verifies a durable fact (approval-
gated by default); /refine reviews the conversation into proposed
edits shown one at a time for approval, with /refine list,
/refine rollback <id>, and /refine promote <id>. Entries carry
ids, versions, provenance, and evidence; every applied edit records
before/after snapshots so rollback is a generated inverse; the base
system prompt is never in the store. Project scope lives in
.corteza/harness.json, global in the user data dir. Injection is
the whole store while small (empty store adds nothing to context).new_session() gains max_tokens: a per-response output-token
budget forwarded to llm.api::agent (NULL defers to
config$max_tokens, then llm.api's provider default — 4096 on
Anthropic). Agent turns that write long tool calls, big run_r
bodies especially, need more than that default: a response the
budget cuts off ends the turn with [Output truncated: max_tokens]
(llm.api#39) instead of the model's answer. Resolution follows the
web_search/base_url idiom via .session_max_tokens(); both
boundaries validate (single positive whole number — as.integer()
alone would truncate fractions and pass NA through). Requires
llm.api >= 0.1.9.5: forwarding a budget onto an older llm.api walks
straight into the silent-truncation bug #39 fixes.agents/<agent>/auto/<run_id>.jsonl. run_start is
written before validation (a refused start still appears), each gate
consultation gets a record naming the deciding authority (budget /
envelope / monitor / accounting) — envelope-refused calls previously
left no trace anywhere — each monitor progress verdict is recorded,
and run_end carries a mechanical stop category, spend, executed
calls, and the worktree delta. A file with no run_end is a run
whose process died. The auto_run_id also stamps main-session trace
entries written during the run and the monitor's transcript header,
so the three surfaces join; attended formats are unchanged.turn() falls back to other providers on limit errors. A new
fallback config key (Matrix config and .corteza/config.json; a
bot_configure() argument) lists "model provider" pairs to try when
the primary answers with HTTP 429/503/529 or a rate, usage, or quota
message. The provider that tripped is skipped process-wide for
fallback_cooldown_minutes (default 30), then the primary is tried
again. A limit hit after tool calls have run is not retried, so no
tool call executes twice. Other errors pass through unchanged./auto [--loops N] <goal> runs bounded unattended iterations,
supervised by a read-only "hall monitor" subagent. The monitor is the
point: turn() routes only "ask" verdicts to approval_cb, and the
default tensor resolves ordinary project writes to "allow" outside
~/projects, so a supervisor wired there would never be consulted for
the edits that matter. Every call surviving policy() now passes a
session$auto_gate instead, whatever the verdict..corteza/config.json travels with a cloned
repo. Shell and R execution is off unless enabled per run with
--exec or in your own global config, because those tools' effects
cannot be resolved from their arguments.subagent_turn_init() previously
built its session without a web_search argument, so every subagent
had server-side search on regardless of its tool preset./permissions switches approval mode for the session
(/permissions allow|ask|deny), instead of only printing settings.
Changing how much corteza asks before running a tool previously meant
editing config.json and restarting. Session-scoped like /dryrun;
the reply names the config file for anyone who wants it permanent,
and says plainly that allow does not waive the credential-path
guard, which short-circuits ahead of the config overlay.grpc_* function surface): Suggests, namespace qualifiers, the
runtime floor (rgrpc >= 0.1.0, the first release under the new
name), and the refusal messages.user_id), OpenID verification goes straight to the
configured server URL instead of through Matrix discovery — a
tailnet homeserver is deliberately not publicly discoverable. Every
other server name still discovers normally, and the bypass changes
only where the question is asked: the sub-domain binding and
membership check are untouched. Derived entirely from the bot
config; no hostname constant in code or config. Replaces the
hostname-rewriting shadow in the launch script.voice.speech_style in the config
replaces the instruction's text; typed room chat keeps markdown.gpu-voice-alloc/1
contract. The request body carries the protocol version, and every
mint presents the service credential from the new
voice.allocator_token config key (a configured allocator without
its token refuses with a named config error). The response envelope
(ok, v, allocation_id, room_id) is validated before the
grant is read, a refusal's error sentence is surfaced, a 401 is
reported as the config problem it is, and the allocation id is
logged so log lines and revocation can name the grant.llm.api::llm_cancel(): the provider stops
generating and the turn returns its partial reply immediately,
instead of running out a tail nobody is listening to. The voice
server is single-threaded, so that tail was exactly the delay before
the user's next turn could start -- and it billed for every token.
The room record still keeps everything generated up to the cut.voice_serve() runs the AgentVoice
gRPC service a fluffychat live-voice client talks to: AllocateVoice
verifies the caller's Matrix OpenID token against their own
homeserver, checks room membership, and relays a media grant from the
fleet allocator (voice.allocator, HTTP); Converse runs one turn
through the shared turn() machinery, streaming text deltas as the
model produces them; ReportTurn cuts the stored room message down
to the prefix the user actually heard, counted in code points. The
reply is posted when generation ends and edited down on report, so a
session that dies unreported leaves the full text standing -- the
documented fallback. Audio never crosses corteza: the client streams
PCM to the model hosts directly. Runs as its own R process, next to
the room poll loop, and needs the grpc and RProtoBuf Suggests.
The contract is inst/proto/cornball/agent/v1/agent_voice.proto,
vendored from cornball-ai/fluffychat.cfg$transport picks the client
constructor behind bot_chat_client(): absent or "matrix" is the
Matrix adapter as before; a {"constructor": "pkg::fn", "args": {...}}
list builds any other chat.api client, and the whole room loop runs
on it. The adapter must hold its own poll cursor and report
first_run; the existing guard now says so.A segment room is not named after the command that ended it.
The fork auto-prepends a bare /clear as its own message, so a
conversation opening with one produced a permanent room titled
/clear (2026-08-20). Commands are skipped when picking the title.
A conversation the user said nothing in gets no room. A /clear
straight after a /clear archives fine and used to become a sidebar
entry that will never be worth reopening. The bar is "said anything at
all", not a turn count -- a threshold would be a number nobody chose.
openai_codex get 20 MB, and a provider that is not listed takes
the conservative default rather than a guess. image_max_bytes still
overrides all of it. The "skipping" message now names the limit as
well as the size, since a line saying a picture was refused without
saying what would have passed is the one thing a reader has to go
find out.A picture posted in a room reaches the model as a picture. An
image arrives as its own message carrying an attachment that names
where the bytes live; corteza fetches it through the transport
(chat.api::chat_download()), encodes it, and sends it alongside the
text as llm.api::llm_content(). Before this the bot saw only the
filename, and answered that no image was attached.
On by default for the cloud providers, off for ollama (a local
build is as likely to be a text-only 9b as a vision model). Both are
overridable with images: true/false in the Matrix config, and
image_max_bytes sets the per-image ceiling (5 MB by default). One
unfetchable picture -- an encrypted attachment, most likely -- costs
the message its image and nothing else. Requires chat.api >= 0.0.1.24
and an llm.api with llm_content(); without either, the text goes
through as it did before and a message on stderr says why.
ai.cornball.fold index the fold writes into the topic room. Replies
go back into the thread they answer, falling back to the room where
the transport cannot route one. Requires chat.api >= 0.0.1.23.segment_rooms, /clear
files the ended conversation as a "segment": a private room named by
the conversation's opening line, carrying the archived-transcript
pointer as a notice and an ai.cornball.lifecycle state event that
clients group and demote by. Off unless configured. Requires
chat.api >= 0.0.1.22 (chat_set_state()); older transports skip
with a message.matrix_* is now bot_*, and R/matrix.R is R/rooms.R. corteza
reaches every transport through the chat.api contract and makes no
mx.api or mx.client call of its own, so the prefix named a coupling
that no longer exists. Of the 74 functions renamed, roughly 20 were ever
transport; the rest are agent behaviour that had been wearing a Matrix
prefix since the day it was written.
The eight exported names stay as wrappers that warn and forward:
matrix_configure, matrix_send, matrix_poll, matrix_run,
matrix_run_init, matrix_run_step, matrix_archive_all,
matrix_request_flush. Nothing about behaviour differs. They are
scheduled for removal at 1.0.0.
Message records use the contract's field names: room_id is
channel, event_id is id, matching chat.api::chat_message().
cfg$room_id is unchanged -- it is a key in a JSON file on disk, and
renaming it would be a config migration rather than a rename.
The last mx.* calls are gone. Credentials and current-state reads
go through chat.api: chat_matrix_config(), chat_config_save(),
chat_matrix_configure() and chat_set_identity() for the credential
lifecycle, chat_channels(), chat_history(), chat_pending() and
chat_mark_read() for the state the poll cursor cannot answer.
The startup invite catch-up was the last raw sync, and it was raw
precisely because an invitation is standing state that a cursor cannot
deliver. chat_pending() is the verb for it.
mx.api leaves Suggests, and matrix_require_mx() checks chat.api
alone: repeating a downstream package's own requirements is asserting
facts only that package can keep true. mx.client stays in Suggests
because the integration tests stub it to drive a real relogin through
the whole stack.
chat_poll() no longer returns $client. corteza used to take the
post-sync config out of it and rebuild a client per send, because a
/model rename could rotate the access token and the config file the
two shared was the only thing keeping them in step.
chat_set_identity() leaves the rotation on the client that performed
it, so there is one client for the poll and every send after it.
Invitations ride the transport contract, and res$raw goes unread.
The poll loop reads chat_poll()$invites and joins with chat_join().
That was the last thing it took off the raw sync response, so the
migration's stated finish line is reached: every message, decrypted
message, reaction and invitation now arrives as a record.
matrix_invite_inviters() is deleted. Reading who sent an invitation
meant walking the stripped invite_state, which put Matrix sync-shape
knowledge two packages away from the sync; mx.client owns it now and
the record carries it.
The operator gate is unchanged in behaviour and clearer in reporting: an invitation from someone not on the list and one whose sender the homeserver never named both refuse, and now say which is which.
One invite that cannot be joined no longer risks the others. chat_join()
raises so each failure is visible, and matrix_accept_invites() catches
per room so a revoked invitation cannot take the whole poll with it.
The startup catch-up still reads a raw sync. It needs a full no-since
sync to see invitations issued while the bot was down, and chat_poll()
only ever resumes from a cursor -- so it stays until the contract grows
a verb for that. It goes through the same gate, so the operator policy
is written once.
Requires chat.api >= 0.0.1.14 and mx.client >= 0.2.0.4.
Room metadata rides the transport contract. mx_room_name(),
mx_room_topic() and mx_room_members() are gone from the Matrix
loop, replaced by chat_channel_info() and chat_members(). Three of
the nine remaining direct mx.api calls go with them.
matrix_new_session() made three state reads and fetched the topic
twice: once inside matrix_room_cwd() for the working directory, and
again for the room description. It makes one now, and
matrix_room_cwd() takes the topic rather than fetching one.
matrix_archive_session(), matrix_archive_all() and
matrix_handle_flush_signal() take a chat client where they took an
mx_sess; matrix_room_members_cached() likewise. matrix_archive_all()
is exported, so that is a signature change.
A failed member lookup still keeps the previous cache rather than
emptying it. chat_members() raises instead of returning character()
precisely so the two can be told apart -- an empty room and an
unanswerable question are different, and only one of them means the
room emptied.
Also drops the unused mx_sess parameter from
matrix_default_system(), which no caller passed.
Requires chat.api >= 0.0.1.13, enforced at runtime.
Reaction approval rides the transport contract.
matrix_reaction_approval() no longer calls mx.api::mx_react() or
runs its own private mx_sync loop; it uses chat_react() and
chat_poll()$reactions. Two of the four direct mx.api sites are gone
with it. The approve/deny vocabulary stays in corteza, where the prompt
that teaches a user which emoji to tap also lives, and is now
overridable per config via approve_keys / deny_keys.
Three bugs fixed on the way:
cfg$room_id, so in every room but one no tap was ever seen.save_cursor = FALSE, so its
private cursor cannot be written over the bot's own position. This
was not reachable before -- the old loop drove mx_sync directly and
never touched the stored token -- but it becomes possible the moment
the loop goes through chat_poll(), and it is the failure that would
have silently eaten every message sent while a user was deciding.mx_extract_reaction_verdict()'s corteza wrapper is deleted: it
delegated the approve/deny vocabulary to the transport package.
Requires chat.api >= 0.0.1.12 and mx.client >= 0.2.0.3, both
enforced at runtime.
End-to-end encryption moves into the chat.api Matrix adapter.
R/matrix_crypto.R is gone and mx.crypto has left Suggests.
corteza no longer builds an Olm account, carries Megolm sessions,
tracks which rooms are encrypted, or keeps a second send path for
encrypted rooms. Setting e2ee in the Matrix config now enables
encryption in the adapter, which encrypts and decrypts on both sides
of chat_poll() / chat_send(); decrypted messages arrive in
chat_poll()$messages like any other traffic, so the poll loop stops
reading m.room.encrypted out of the raw sync. matrix_poll() drops
its crypto argument and matrix_run_init() no longer returns a
crypto context. Requires chat.api >= 0.0.1.9, enforced at runtime.
corteza names no crypto store. The adapter derives one per Matrix
device, (user_id, device_id), and records that identity in the store
so another device cannot open it. This replaces
dirname(config)/crypto, which tied the device identity to wherever
the config file sat, so a moved config silently minted a new device and
lost every Megolm session with it.
mx.client floor raised to 0.2.0.2. That release stops
mx_send_encrypted() reporting success for a message nobody can
read: it filters its own device by (user_id, device_id) rather than
device_id alone, refuses to post when every recipient device was
skipped, and treats a partial /keys/query or /keys/claim as
unanswered rather than as a user with no devices. corteza does not
call it directly, but every encrypted reply this loop sends goes
through it.
CI installs and version-checks the Matrix stack. It is all
Suggests, and _R_CHECK_FORCE_SUGGESTS_=false let a run pass without
any of it -- which it did, silently, because every Matrix test sits
behind a requireNamespace() guard that skips the whole integration
file. The workflow now installs mx.api, mx.client, and chat.api
from source and fails if either declared floor is unmet, so the
transport tests cannot skip themselves into a green tick.
Additional_repositories declares the cornball drat. chat.api
is a Suggests and is not on CRAN, so CRAN could not resolve it and
the check NOTE listed it with no availability line -- which blocked a
release. It is now published at
https://cornball-ai.github.io/drat/ and declared here, so the NOTE
reports chat.api yes against the declared repository, the form
Writing R Extensions describes for exactly this case./model acknowledgement was dropped
silently; matrix_update_displayname() now returns the refreshed
config. Startup and /clear advertised cfg$model rather than the
runtime override the next session is built with. The
mx.client >= 0.2.0 floor is now enforced at runtime, not only
declared, so a stale installed copy fails loudly instead of losing
the rename to a best-effort tryCatch.matrix_configure(model_badge =)
key: "never" (default, unchanged behavior), "non_default", or
"always". When a badge applies, deterministic post-turn code — not
the model — prepends a ⚡ <model> (<provider>) first line to each
reply and renames the bot to <name> ⚡ <model>, so a room session
that /model-switched to a paid provider is visible on every message
and in the sender line. In "non_default" mode silence means the
configured default: the badge appears only while a switch is live and
disappears on /clear or a process restart (both reset the profile
name too). The display name is account-global, so with sessions in
several rooms the most recent switch wins; the per-reply badge line
is always room-accurate. display_name optionally sets the base name
the rename builds on (default: the user-id localpart).
The profile update goes through the new mx.client
mx_set_displayname() (>= 0.2.0), which refreshes a rotated token
instead of silently failing the rename.chat_send(),
chat_poll(), and chat_typing() instead of calling mx.client and
mx.api directly. Encrypted rooms stay on the mx.crypto path, and
corteza still reads invites, reactions, and encrypted events off the
raw sync response itself. chat.api is a Suggests, gated by the same
check as the rest of the Matrix stack, so it is only needed on hosts
that enable the channel.openai_compatible provider for generic OpenAI-compatible
endpoints (#149). Point corteza at OpenRouter, DeepSeek, DeepInfra,
a local proxy, or a corporate gateway via a base_url config key or
the --base-url CLI flag (falling back to
OPENAI_COMPATIBLE_BASE_URL). The model id is passed through
untouched and is required; the API key comes from
OPENAI_COMPATIBLE_API_KEY or OPENAI_API_KEY, and a keyless
gateway works with neither. A missing endpoint or model is reported
at session setup rather than at the first request. turn() applies
the endpoint through llm.api::llm_base() scoped to each agent call,
so a mid-session /provider switch or a subagent on another provider
cannot inherit it. Requires llm.api >= 0.1.9. See
vignette("configuration").
Provider-native web search. The agent loop enables
llm.api::agent()'s server-side web search for the hosted providers
that support it (anthropic, anthropic_claude, openai,
openai_codex, moonshot). On by default, no Tavily key required;
the model searches inside its own turn. Toggle with --web-search /
--no-web-search, chat(web_search = ), or a web_search key in
.corteza/config.json. The Tavily web_search tool remains the
fallback for local models.
Matrix /model lists available models and switches by number.
Bare /model (or //model where the client eats single slashes)
replies with a numbered menu instead of echoing the current settings:
the configured default, the live local Ollama inventory, and any
hosted models declared via the new matrix_configure(models =) key
("model provider" strings, since hosted providers cannot be
enumerated remotely). /model 3 switches the room session to entry
3, setting model and provider together, so switching from a phone
client never needs a typed model name. The typed form
/model <name> [provider] is unchanged, and an unreachable Ollama
degrades the menu to the configured entries.
Private conversations and invites are restricted to configured
operators. The new operators key lists the Matrix ids allowed a
one-on-one conversation with the bot. A room with one human is
answered only when that human is an operator; everyone else gets
silence rather than a mention-gated session. Invites are gated on the
same list, read from the inviter in the stripped invite_state, and
an invite whose inviter cannot be determined is refused. Group rooms
are unaffected. Leaving operators unset preserves the previous
behavior.
Reply gating counts humans, not members. Rooms whose only non-bot
member is one human are answered without a mention; rooms with two or
more humans require a mention (replies count, since clients put the
replied-to user in m.mentions) or an open engagement window: after
the bot answers someone, that person's plain messages keep getting
replies for 5 minutes, ending early when they address someone else.
Other bot accounts are declared via the new matrix_configure(bots =)
parameter; their messages always require a mention. Room membership is
cached per session and refreshed on unknown senders or every 10
minutes, replacing the boot-time DM snapshot that left rooms stuck on
stale gating after membership changed. With bots unset, behavior
matches the old member-count rule minus the staleness; during a
members-API outage the gate fails open for human senders instead of
going silent.
Non-triggering messages are ingested instead of dropped. In a room
with more than one human, a message that did not warrant a reply was
marked seen, given a read receipt, then dropped with a bare next:
never appended to the session's history and, because it was already
marked seen, never reconsidered. The agent genuinely never saw other
people's non-mention messages in a busy room, even though it had
"read" them. Gated messages are now appended to history as context;
the reply gate and its bot-loop protection are unchanged, only
ingestion is. Turns are attributed with a [sender] prefix in rooms
with more than one human (both the live poll and the startup
backfill), and known bot senders are labeled even in one-human rooms,
so the model can tell speakers apart. A lone-human DM is unchanged.
Rooms load project context into the room system prompt.
matrix_new_session() passed a non-NULL system to session_setup()
with load_project_context = FALSE, so Matrix bots ran without the
project briefing, agent context files, .corteza context_files, and
package tool docs that other channels receive. The new
matrix_room_system() composes the Matrix identity header with
load_context() for the room cwd.
Archives are built from a Matrix-visible event ledger, not from
provider history. History holds tool calls and tool results that
were never Matrix events, so a restarted process (which rebuilds
history from the server's visible messages) could not align with what
it had archived, and tool-using rooms re-archived their whole backfill
on every restart. Sessions keep a transcript of
{event_id, role, content} and a queue of pending events, written
where events are seen or sent and drained once archived. Archival keys
on Matrix event ids, recognized against a bounded per-room tail of
already-archived ids. Transcripts no longer contain tool turns.
Archive progress is persisted per room instead of tracked in
memory. matrix_archive_session() used ingested_through, an index
into the in-memory session history, which reset on every restart while
startup backfill refilled history from the server. Each restart
therefore re-archived the backfilled tail. Progress is now an ordered
key tail persisted per room under CORTEZA_STATE_DIR, advanced only
after a successful ingest, so a failed archive retries rather than
loses turns.
Archives are keyed by room id, not display name. Room display
names and avatars are mutable Matrix state, so archived transcripts
use the global Matrix room id for title and source. The room name
is retained only as archive-time metadata, which keeps renames from
splitting one chat into multiple raw archive identities.
CRAN release consolidating the 0.6.9.1-0.6.9.8 development cycle plus a runtime-hardening batch. Last CRAN release: 0.6.9. Per-cycle detail follows.
matrix_run_init() / matrix_run_step(), with
the channel delegating session and transport plumbing to mx.client.anthropic_claude provider (Claude on a subscription via OAuth),
requiring llm.api (>= 0.1.8).fetch_url/web_search, now bounded by curl's own timeout) are no
longer wrapped in an R-level setTimeLimit; the residual flush moved to
skill_run entry so a validation/dry-run early return can't leak a queued
interrupt (#142, #139)./compact survives provider-native history (Anthropic, OpenAI chat, and
role-less OpenAI Codex Responses entries), including tool calls and outputs./compact now renders Anthropic, OpenAI chat, and role-less OpenAI Codex
Responses history safely, including tool calls and tool outputs. This prevents
provider-native history entries from wedging compaction.
Matrix documentation now matches the anthropic_claude provider default and
documents the optional crypto polling context.
anthropic_claude provider, which drives Claude on a Claude
subscription (OAuth) via llm.api instead of an API key. Added to
matrix_configure()'s provider whitelist and routed through the Anthropic
message shape for interrupt-time tool-result repair. Requires llm.api
(>= 0.1.4.3).matrix_run_init() (build the session registry, catch up on invites,
backfill history, init E2EE) and matrix_run_step(state, timeout) (one
poll-and-reply iteration). matrix_run() is now a thin wrapper over
them. Lets an external scheduler own the main process and interleave
the Matrix poll with its own work. No behavior change for
matrix_run().mx.client package instead of hand-rolled plumbing
over mx.api. Behavior is unchanged; ~37 fewer lines in the adapter.run_r/run_r_script semantics, and that
the bash tool makes corteza a general-purpose agent rather than R-only.PR #96 already redirected R_USER_CACHE_DIR for the test run (so
saber's transitive briefs writes landed in tempdir), but corteza
itself writes to all three tools::R_user_dir() roots — cache
(saber briefs, /last response), data (session transcripts via
R/paths.R, matrix state), and config (matrix.json). Session tests
were still leaking files under
~/.local/share/R/corteza/agents/…/sessions/*.jsonl, which would
have tripped CRAN's "checking for new files in some other
directories" NOTE on BDR's donttest box even when local
tinypkgr::check() reports Status: OK. The suite-level redirect in
tests/tinytest.R now sets all three (R_USER_CACHE_DIR,
R_USER_DATA_DIR, R_USER_CONFIG_DIR) to tempfiles for the check
run; end-user behavior is unchanged.
Patch release batching the 0.6.7.1 through 0.6.7.12 dev cycles. Per-PR detail is preserved in the dev-marker sections below.
chat(provider = "ollama") followed by an LLM-triggered
spawn_subagent was silently producing children on Anthropic: the
default tool_executor in .make_tool_handler() called call_skill()
with no ctx, so tool_spawn_subagent() saw ctx$session = NULL and
subagent_spawn() fell through to getOption("corteza.provider"). The
executor now passes ctx = list(session = session), so provider, model,
cwd, plan_mode, and archival_depth all flow through to the child.
A single unbounded tool result (e.g. a bash command that printed 57k
lines) was flattened straight to the model and mirrored into history,
blowing the next call past the provider token limit; /compact then
couldn't recover the session. Tool results now pass through a universal
guard (admit_tool_result()) that caps oversized output to a marker plus
a recoverable handle, and /compact elides giant message bodies so it can
rescue an already-wedged session.
A chat() started without an explicit model no longer crashes after
the first turn. The per-turn context meter called
context_limit_for_model() with a NULL model and errored on the table
subscript; it now falls back to the default limit (and resolves a
model-less session to the provider default for display).
Packaging-only pass ahead of the 0.6.8 release: applied a deferred
rformat reflow, generated the user_interrupt_marker man page,
refreshed cran-comments.md, and turned the skills vignette's clone
example into a <your-skills-repo-url> placeholder so it carries no
dead link. No code behavior changes.
A turn interrupted with Ctrl+C / Esc now leaves the same "stop and ask the user what to do instead" instruction in history as an explicit deny, so the LLM checks in on the next turn rather than quietly continuing. Previously the interrupt marker was terser than the deny marker even though the approval prompt's Esc/Ctrl+C hint routes through the interrupt path.
default_provider_model() now delegates to
llm.api::provider_default_model() instead of corteza's own
provider-to-model table, which had drifted. corteza tracks llm.api's
picks going forward (e.g. openai and moonshot defaults shift to
llm.api's current choices). No change when you set a model explicitly.
serve() no longer hands the subagent tools (spawn_subagent,
query_subagent, collect_subagent, list_subagents, kill_subagent)
to MCP clients by default. A spawned subagent runs its own agent loop
and spends autonomously on the host's LLM credentials, so an unattended
MCP client could otherwise trigger unbounded cost it never sees.
Enable with subagents.expose_over_mcp (config, default FALSE) or
serve(expose_subagents = TRUE). When enabled, cumulative subagent
spend over MCP is capped: spawn_subagent/query_subagent are refused
once spend crosses subagents.mcp_spend_cap_usd (default $5.00; <= 0
disables) or an optional subagents.mcp_spend_cap_tokens for providers
that don't report cost. The cap reads the same meter as /spent; the
in-process chat()/CLI loop is unaffected.
/spent (alias /cost) reports the approximate USD spent this process
run, in both chat() and the CLI. Each turn accumulates the cost
scalar that llm.api 0.1.4 returns, plus token counts. When a model is
absent from llm.api's price snapshot its cost is unknown, so totals are
shown as a floor rather than a precise figure.
Spend is process-lifetime: /clear no longer zeroes the tally, it
closes the current conversation and opens a new one, so /spent
itemizes each conversation between clears with a grand total. /clear
now also kills any live subagents (a fresh conversation leaves none
behind); their spend is retired into the run total, shown as a separate
process-level subagents line that keeps counting an agent after it is
killed. Resumed prior-run spend is not loaded from disk.
The CLI no longer runs tools in a separate callr worker. chat() and
the CLI now drive one shared loop (run_repl_loop) in a single R
process, executing tools in-process via corteza::turn().
.subagent_registry per session: a model-spawned subagent shows up in
/agents, and a /spawn-ed one is reachable by query_subagent./flush
moved into the shared loop. Per-surface differences (rendering, help
text, input reader) ride through injected hooks, not forked logic.chat() gains the per-turn context meter and auto-compaction the CLI
already had./r, !) and the foreground bash tool moved off
blocking system2() onto interruptible processx::run(), so an
interrupt returns to the prompt instead of killing the session.worker_dispatch, worker_tool_list, cli_worker_spawn,
cli_worker_drain_events). worker_init() stays exported: subagents
call it across the callr::r_session boundary to set cwd and register
skills.Hitting Ctrl+C (CLI) or Esc (RStudio chat) after a multi-tool turn no longer loses the tool calls that completed before the interrupt. Previously the next turn / next CLI invocation saw the original user prompt, an interrupt marker, and nothing else.
Imports: llm.api (>= 0.1.4) so agent() exposes the
history_callback parameter. R/turn.R uses it to mirror the
in-progress provider-native history into the live session env as
each assistant message and each tool result lands. With the
callback wired, turn_session$history reflects the latest snapshot
even when an interrupt unwinds the turn mid-flight.repair_interrupted_tool_history() (with the
surrounding apply_exit_marker()) synthesizes provider-correct
tool_result blocks for any tool_use issued during the current
turn that never got matching results. Wired into chat()'s
interrupt and corteza_user_deny condition handlers. Anthropic's
"every tool_use needs a tool_result" requirement no longer 400s
the next chat() API call after an interrupted multi-tool turn.dump_completed_tools_summary() walks the per-turn
history slice and appends a text summary of completed tool calls
to the persistent flat-text session$messages before the
interrupt / denial marker. Wired into both CLI exit handlers
(inst/bin/corteza). The next CLI turn's api_history rebuild
now carries the work the LLM accomplished before the interrupt
("[ran tool_name(...) -> result]" entries instead of nothing).on.exit guard around the archival ownership transfer.
An inner guard inside archival_archive_turn() kills the holder
subagent if anything between spawn and the explicit
transferred <- TRUE unwinds; an outer guard in
maybe_archive_turn() covers the parent-history-collapse window.
Either failure cleans the orphan instead of leaving it sitting in
.subagent_registry with no parent reference.subagent_query() / the query_subagent tool gain a return_name
argument. When set to a name (or .h_NNN handle) the child left its
result bound under, the child resolves it after the turn and ships
the value back; the parent stashes it in the handle store and
appends a [stored as .h_NNN] block to the reply. A large
structured result is then referenced by name in a later run_r
instead of being inlined into the parent's context, mirroring how
run_r already returns large values. The async (wait = FALSE)
path captures the name at query time and applies it on collect.skill_install() clones GitHub repos via processx::run()
(already an import) instead of system2(), with a nonzero git
exit surfaced as a clear error. The clone is extracted into an
internal git_clone() helper so its status handling is
unit-testable against a local repository, no network required.find_break_point() (text chunking) replaces two backward
character-scan loops with a two-tier regex: last newline, else
last whitespace (tabs included). Behavior preserved; added
regression tests for newline-beats-later-space and tab handling.Patch release batching the 0.6.6.1 through 0.6.6.20 dev cycles plus the out-of-band "Deny aborts the whole turn" change. Per-PR detail is preserved in the dev-marker sections below.
corteza_execute_in_chat() / _retain() now expand the current
line to the full top-level R statement before sending, matching
RStudio's built-in Ctrl+Enter behavior. Before, hitting Ctrl+Enter
on the first line of lm(y ~ x,\n data = df) sent only the
unclosed first line -- annoying outside chat(), and broken
inside chat() where /r lm(y ~ x, failed to parse. Buffer is
parsed with keep.source = TRUE; falls back to single-line when
the buffer has a syntax error elsewhere or the cursor sits on a
blank / comment line outside any expression.chat() and the CLI /r handlers now read continuation lines
with a + prompt until the expression parses, mirroring R's
built-in REPL continuation. Capped at 100 continuation lines to
keep a stuck parse from blocking the REPL.handle_eval_env() used to skip
reassignment when the .h_NNN symbol already existed in
globalenv, so rebinding a handle id in the store left the old
globalenv copy in place (codex repro'd: tool_run_r('.h_001')
returned the previous snapshot after the store was rewritten).
Now handle_eval_env() assigns unconditionally and removes
globalenv bindings the package previously created that are no
longer in the store, via a new .handle_managed registry.
clear_handles() also sweeps the managed bindings out of
globalenv.session_id() ignored its caller's agent and only collision-
checked the default agent's transcript dir. With the
docker-style ~6000-name pool, a non-main agent could mint a
name that already existed in its own store and silently reuse
the transcript. session_id() now takes agent_id, threads
it to .session_name_exists(), and the collision check now
consults both the transcript file and the in-store metadata
for that agent.ctx$path, so
tools::file_ext() returned "" and the addin routed the
line as "other" -- inside chat() that meant the LLM saw raw
R code instead of a /r ... invocation. Untitled buffers are
now treated as R, matching RStudio's built-in assumption.corteza_execute_in_chat() (plus
corteza_execute_in_chat_retain() for Alt+Enter) reads the
current line / selection from RStudio's source editor and sends
it to the console, auto-prefixing /r for .R files and
! for .sh / .bash files when corteza::chat() is the
active REPL. When chat() is not running, no prefix is added --
the addin is a superset of RStudio's default execute-line
behavior, so binding it to Ctrl+Enter doesn't break normal R
scripting outside chat.chat() now sets options(corteza.chat_active = TRUE) on entry
and clears it on exit; the addin reads this flag to decide
whether to prefix.rstudioapi added to Suggests. Addin file lives at
inst/rstudio/addins.dcf so RStudio picks it up after install.Setup: in RStudio, open Tools -> Modify Keyboard Shortcuts, pick "Addins" in the dropdown, and bind Ctrl+Enter (and optionally Alt+Enter) to the two corteza addins. Override the built-in "Run current line/selection" mappings.
tool_run_r() was advertised as "Execute R code in the session's
global environment", but PR #36 (Phase 5 of CLI/worker split,
2026-04-21) silently changed it to evaluate in a fresh child env
of globalenv. Ordinary <- and = assignments landed in the
child env and disappeared when the call returned; only <<-
walked up the scope chain and persisted. Reported 2026-05-20.handle_eval_env() now copies handles INTO globalenv (under
their hidden .h_NNN names that ls() filters out by default)
and returns globalenv itself. tool_run_r() evals in globalenv,
so <- writes the the right place and survives between calls.test_tool_impl.R and test_handles.R
exercise multi-call persistence with both <- and =.U+1F7E8)
laid out in a brick pattern: adjacent rows offset by an odd
number of cells so the kernels stagger like masonry instead of
stacking in straight columns. No ANSI escapes -- the emoji is
its own colour, supported across every modern terminal.kimi-k2.6 for gpt-4o shifted the right edge and broke the
brick offset.0.6.6.17 -> v0.6.6.adjective_surname
(e.g. boring_wozniak) instead of UUIDs. Wordlists are the
Docker moby/moby adjectives plus ~60 scientist surnames.
Collisions retry up to 10 times before falling back to a hex
suffix.corteza::chat() now prints a session adjective_surname
line below the banner, matching the CLI's layout.chat() and the ~/bin/corteza CLI now open with a gold
brain-corn silhouette rendered in 256-color ANSI, with the
corteza version, active model, provider, tool count, and
/help / /quit hints embedded directly inside the kernels.
Replaces the prior single-line corteza chat | model @ provider
header. Uses 256-color index 220 (gold) for compatibility with
terminals that don't advertise true-color via COLORTERM.corteza_startup_banner() in R/banner.R --
template-driven, so future tweaks to the silhouette only touch
one constant.! <cmd> prompt prefix runs a shell command locally (bash on
unix, cmd.exe on Windows), prints output, and stages it for the
next LLM message. Matches the Claude Code / codex ! convention.
The space after ! is required so prompts that legitimately
start with ! aren't captured. Output is capped at 4000
characters in the staged version (with a truncation note); the
on-screen output is full./r <expr> is now available in the CLI too -- previously chat()
only. Same staging behavior: visible result prints inline, gets
queued for the next LLM message, with an oversized printed
result swapped for str() of the value.run_bang_shell() and run_r_eval() live in
R/chat-slash.R so both surfaces use one source of truth for
the local-eval + cap logic.task_create(tasks) / task_update(index, status)
LLM tools (Claude-Code-style TaskCreate pattern). The list lives
on the session, is injected into each turn's system prompt as
numbered [ ] / [>] / [x] / [-] lines, and is shown to the user
as a compact summary whenever it changes during a turn. Status
is pending, in_progress, completed, or cancelled.
Promoting a task to in_progress auto-demotes any other
in_progress task to pending so the "one active at a time"
invariant holds without rejecting valid edits..make_tool_handler() (R/turn.R) rather than the normal skill
executor, so the CLI's callr-worker dispatch can't strand
mutations in the wrong process. The intercept fires before
dry-run, policy, and approval -- task-list updates never prompt
the user./tasks (and /tasks clear) slash commands in both chat()
and the terminal CLI. /clear wipes the task list along with
the conversation, matching the "fresh start" intent.session_new, session_save, session_load) so it survives
CLI restarts and chat(session = ...) resumes./plan mode, which is a one-shot
research-and-propose flow; the task list tracks ongoing
progress for multi-step work without changing the policy gate.model is NULL, corteza::chat()'s startup banner and
per-turn "Thinking with ..." line route through
resolve_provider_model(provider, model) instead of falling
straight to the literal "(provider default)" placeholder. With
config$provider = "moonshot" and no explicit model, the
banner now reads kimi-k2.6 @ moonshot, matching what the
terminal CLI already showed. Unknown providers still degrade to
the "(provider default)" string.cli_approval_lines() takes a new deny_label argument (default
"Deny"). corteza::chat() passes "Deny (Esc)"; the terminal
CLI passes "Deny (Ctrl+C)". The key doesn't literally cancel
the readline prompt itself, but it cancels the in-flight turn,
which is the user-facing escape hatch users want to know about.
Deny still raises the corteza_user_deny marker; Esc/Ctrl+C
during a turn raises an interrupt marker. The LLM treats both as
"stop and check with the user" -- the wording of the interrupt
marker is less directive than the deny marker, worth tightening
in a follow-up.R/render-md-ansi.R adds render_md_ansi(text) (internal):
strip markdown syntax markers and apply ANSI styling for terminal
display, leaving the raw markdown source intact when the output
isn't a TTY (NO_COLOR, piped to file) or when the user opts out
via options(corteza.markdown = FALSE). Both corteza::chat()
and inst/bin/corteza now route assistant responses through it,
so the two surfaces stay in sync. Replaces the smaller bespoke
renderer that previously lived in inst/bin/corteza. Handles
H1/H2/H3 headings, **bold**, *italic* / _italic_ (modern
terminals interpret as italic), `inline code`, fenced code
blocks (dim, 2-space indent), - / * bullets, > blockquote
lines, and [text](url) links.[user declined: ...] tool result that
the LLM saw and treated as feedback, planning the next call --
which forced users to mash "3" through cascades of dependent tool
calls. The next turn now starts with a history marker that names
the denied tool and tells the LLM to stop and ask the user what to
do instead, rather than retrying or planning a workaround./copy slash command copies the most recent assistant
response to the system clipboard via the optional clipr
package. Silent no-op until the first assistant reply lands;
prints a hint if clipr is not installed or the clipboard
isn't available (e.g., headless Linux without xclip /
wl-clipboard).tool_run_r_script() switches its callr::rscript() call from
stdout = "|" to stdout = NULL, with stderr = "2>&1"
unchanged. The old combination hangs indefinitely on Windows with
CRAN callr 3.7.6 when the child script errors via stop() — the
internal timeout never fires (r-lib/callr#313). res$stdout is
still populated by the 2>&1 redirect, so the return value and
LLM-facing text are identical. Can be reverted once we depend on a
fixed callr (>= the post-e93efd1 release).test_subagent_callr.R gate rationale corrected: that gate is
about per-test budget, not the callr bug; r_session uses a
separate code path and was empirically verified not affected.skills_as_api_tools() (the chat() path) now applies the same
available() predicate filter that schema_from_registry() (the
CLI path) has always used. Conditional tools — git_* when not
inside a repo, platform-specific shell tools — no longer show up
in chat() while being hidden in the CLI. Counts agree.turn_footer_line() defaults to the detected terminal width
(COLUMNS env, options("width")) instead of a fixed 60-char
line, so the ─ Worked for 3m 18s ──── separator reaches the
right edge./status is now an alias of /context. Both render the same
block: a Codex-style header (corteza version, model+provider,
cwd, session id) followed by the context meter.system in bright blue,
tools in bright magenta, history in cyan — so the bar maps
visually to the breakdown rows below.New compact horizontal /context display answers the two questions
a user actually has: how full is context, and what's using it. Same
in both corteza::chat() and the CLI:
Context 24.7K / 128.0K 19% compact 90%
[██████████..................................│.....]
system 22.0K 89%
tools 2.7K 11%
history 56
│ tick at its
cell position in the empty part of the bar.Dropped the "Project context still comes from saber::briefing() and saber::agent_context()..." paragraph — the bar already shows where the system budget is going, and the prose was longer than the data.
corteza::chat() or ~/bin/corteza turn, a dim
footer line shows wall-clock duration: ─ Worked for 3m 18s ────.inst/bin/corteza only, so
corteza::chat() couldn't render the same /status, /doctor,
/config, /diff, or /review output. They now live in
R/cli-helpers.R as internal package functions; both surfaces call
them. chat() gets all five commands plus /last and /outputs./compact in chat() now uses the same do_compact() the CLI does,
instead of a separate inline implementation. Both routes share one
prompt and one chat-call shape./last, /outputs) is now session-scoped via
session$sessionId rather than a CLI-process global. Subagents and
parents have isolated buffers; /clear drops the outgoing
session's buffer./remember and /recall in the CLI are gone. They called
memory_store / memory_search / strip_tags / parse_tags —
none of which exist in the package — and would error on use.
/flush is alive (rebuilt earlier this cycle as a real memory
flush via .run_memory_flush()) and stays.Backspace past the start of your typing no longer eats the >
prompt character. The bash hack used for line editing now passes
the prompt to read -e -p so readline owns the cursor instead of
the prompt being a cat() that readline can't see. ANSI color
escapes in the prompt are wrapped in \001 \002 so readline's
column math stays correct.
The approval prompt's Choice [1]: dropped the [1] shell
convention; the (Enter) hint already lives on choice 1 itself,
so the bracket was redundant.
Multi-line input in both corteza::chat() and the CLI. Two
entries with two contracts:
/paste [optional text] — explicit "paste anything" mode.
Collects every line verbatim (logs, code, paths with literal
trailing \, etc.) until /end on its own line or Ctrl+D.\ — drops into
bash-heredoc-with-continuation mode mid-line, seeded with what
you already typed. Keep ending lines with \ to continue;
the first line without a trailing \ is final and gets
included. \\ at end of a line stays literal. /end and EOF
also terminate.Paste content that happens to start with / is not reinterpreted
as a corteza command. No "Paste mode..." banner — IYKYK.
Reason section
(gate text + Policy: + Model route:) is gone; Access collapses
to a single line that names the path or command (e.g. Write to CLAUDE.md, Run command in /home/troy/corteza); the redundant
Path: detail line above Access is suppressed when it would just
repeat the same path. Choices 1 and 3 carry key hints: (Enter) and
(Esc).● User replied: summary paraphrasing the chosen action (e.g.
"Allow writing to CLAUDE.md once").User replied: summary via ANSI cursor-up + clear-down. In
corteza::chat() running under RStudio (whose console doesn't
honor cursor-position escapes) the block stays in scrollback with
the summary appended below.replace_in_file and write_file now attach a unified-diff payload
to their MCP result. The CLI and corteza::chat() render it inline
in the tool-call output as ⎿ Added N, removed M followed by one
row per kept line (NNNN +|-| content with red/green color) instead
of the prior N lines in Xms summary. The LLM-facing result text is
unchanged — the diff is for the human reading the terminal.replace_in_file → "Update",
write_file → "Write" (matches the inline-diff phrasing).diff -u. If diff isn't
on PATH the tool degrades to a one-line size summary rather than
failing. Diff payload is capped at 200 lines / 20000 chars with a
[diff truncated: N more lines] marker so big writes don't dump
thousands of lines into chat scrollback./diff slash command's output is also ANSI-colored.ansi_supported() / ansi_colors() in the package are now the
single source of truth for both corteza::chat() and the
~/bin/corteza CLI. RStudio's R console (which is not a tty) is now
correctly detected as ANSI-capable, and NO_COLOR / FORCE_COLOR
overrides work in both surfaces.corteza::chat() and the
~/bin/corteza CLI catch the R-level interrupt.callr worker subprocess, the worker is sent SIGINT so
the in-flight tool (e.g. a long bash or run_r call) actually
stops. The worker is recycled only if it doesn't return to idle.[Interrupted by user before completing.] marker so the next turn's
model sees that the prior turn ended early.corteza::chat() is interrupted by Esc (RStudio's console
intercepts Ctrl+C for copy). In the terminal ~/bin/corteza CLI it's
Ctrl+C — terminals send raw ^[ for Esc, which is not a signal.load_saber_briefing() now wraps saber::briefing() in
suppressMessages() so the briefing text no longer leaks to the
user's terminal every time a subagent calls session_setup().subagent_query(id, prompt, wait = FALSE) fires a prompt and
returns the canonical id immediately; the parent collects the
reply later with subagent_collect(id). A subagent can carry
only one in-flight async query at a time — both wait paths
refuse to stack on top of a pending call./queue <id> <prompt> (fire) and /collect <id>
(drain). /agents distinguishes idle vs busy.collect_subagent mirrors the CLI surface.agents/subagent-<id>/sessions/<id>.jsonl, matching the
shape archival holders already use. Disk space is cheap; context
is expensive. Compaction (below) can rewrite the in-memory
history without losing anything on disk.inst/bin/corteza into
package code so chat, the CLI loop, and subagents share the
same budget math: context_limit_for_model(), format_tokens(),
estimate_text_tokens(), estimate_history_tokens(),
estimate_tool_tokens(), estimate_live_context_tokens(),
context_usage_pct(). Also default_provider_model() for
resolving the model identity a subagent will actually run with.subagents.context_compaction config block. Defaults to
mode: inherit_strict with compact_pct: 75. Working subagents
compact their own in-memory history after each turn when usage
passes the threshold; the on-disk transcript stays intact.
Archive holders are skipped via a kind marker stamped by
subagent_seed_history()./agents now shows model, age, live context (tokens / limit),
cumulative input/output tokens, and cumulative cost per
subagent. Live tokens are computed via a child-side
r_session$run() call per /agents invocation; busy children
show ctx ?. Cost is captured when the provider returns it;
shown as ? otherwise (most non-Anthropic providers).plan_mode flag. When on, the LLM is told to
research and propose rather than act: the policy engine denies
write/exec tool calls (write_file, replace_in_file, bash,
run_r, run_r_script), and an exit_plan_mode tool is injected
into the tool list. A successful exit_plan_mode call flips the
flag back off so the LLM proceeds with the work./plan slash command in chat() and the corteza CLI: bare
toggles, /plan <task> enables and submits the task as the next
prompt.plan_mode from parent_session so spawning a
child can't launder a write through plan mode.archival config block. Default off — CRAN users see no behavior
change. When enabled, finished turns collapse into a fresh subagent
that holds the full transcript, while the parent's history keeps a
compact {summary, subagent_id} block. The LLM sees live subagents
in its system prompt and picks query_subagent vs spawn_subagent
as a normal tool decision.[Max turns reached] is no longer a dead-end string: with archival
on, the full transcript persists in a subagent for follow-up via
query_subagent.archival.trigger.depth_cap).transcript_append infra under
agents/subagent-<id>/sessions/<id>.jsonl.subagent_seed_history, subagent_turn_set_id.archival.enabled requires subagents.enabled.
No silent overrides.vignette("retroactive-extraction") for the full opt-in
surface, design notes, and known limitations./spawn now parses --model, --preset, and --tools in any
order. Matches the MCP tool_spawn_subagent surface.investigate, work, minimal).
Default is investigate (read/search only).subagent_spawn(tools = character(0)) is now a documented
configuration: spawns a holder with no active tools. Used by the
archival runtime to create transcript-only subagents.resolve_subagent_tools() honors config$subagents$default_tools
when neither preset nor tools is supplied (was silently bypassed
before).file("stdin") rather than stdin() (which reads from the script
source under Rscript -e), echo the client's protocolVersion in
the initialize response instead of hardcoding it, and serialize
empty capabilities.tools as a JSON object ({}) rather than an
array ([]). Thanks to Grant McDermott (@grantmcdermott, #62).configuration vignette covering config files and precedence,
CLI flags, the full JSON config-key surface (core, context, safety,
skills, subagents, channels, etc.), slash commands, MCP server setup
(stdio and socket transports), session tuning, systemd service, and
environment variables. Thanks to Bob Rudis (@hrbrmstr, #54).Live context indicator now reflects the actual size of the next
prompt (system + tools + message history) rather than cumulative
billed API tokens. Old behavior counted up forever — /clear and
/compact had no visible effect on the indicator. Status line label
updated from Usage to Live context./context now prints live usage and the auto-compact threshold
alongside the loaded context files.resolve_provider_model().
Legacy kimi-k2 now resolves to kimi-k2.6 for moonshot.
Moonshot's chat temperature is forced to 1 (their API rejects other
values on kimi).session$compactions ->
session$compactionCount, matching memoryFlushCompactionCount.
Existing on-disk sessions show 0 compactions until the next compact;
display-only._read_prompt_via_bash now prints the prompt from R and captures
input through a tempfile. Previously relied on bash -p plus
system2(stdout = TRUE), which was fragile on terminals that mixed
the prompt into stdout.First CRAN submission.
Eight-phase refactor of the command-line interface so its subprocess no longer speaks MCP internally.
callr::r_session worker. Tool
dispatch goes through corteza::worker_dispatch() directly; no
JSON-RPC, no tools/list handshake, no per-call envelope on the
CLI-to-worker path.serve() remains a spec-compliant MCP server for external clients
(Claude Desktop, VS Code, mcptools). Public MCP behavior is
unchanged.R/registry.R. chat(), serve(),
and the CLI all read from .skill_registry. No state duplication.cli_worker_spawn(), worker_init(), worker_dispatch(),
worker_tool_list(), cli_worker_drain_events() exposed with
@keywords internal so the callr session can reach them as
corteza::*.corteza_tool_error condition class
carries tool name, args, original class, and message across the
worker pipe.R/subagent.R) also use callr::r_session instead of
spawning corteza::serve() children. Same architecture: one
persistent worker per subagent, direct tool dispatch, no MCP.R/schema.R with schema_from_fn() and register_skill_from_fn().
Tool definitions are derived at runtime from formals() and the
package's .Rd files via tools::Rd_db(). Replaces 20+
hand-written skill_spec(params = list(...)) blocks with one-line
registrations.(type) parenthetical in @param
docs ((character), (integer), (logical), (character vector),
(character; one of: a, b, c) for enums).schema_from_registry() produces the Anthropic-API-shaped tools
payload the CLI sends to the model — in the CLI's own process, not
round-tripped through the worker.inst/tinytest/test_tool_schemas.R asserts every formal maps to a
@param entry and vice versa. Drift between doc and signature
fails the test suite.register_skill_from_fn() accepts an available predicate;
schema_from_registry() filters tools whose predicate returns
FALSE. Git tools gate on .git; web search gates on
TAVILY_API_KEY. 18-20% fewer tokens in the system prompt for a
bare environment.tool_run_r() wraps non-scalar or over-threshold values with
with_handle(). The LLM gets a str() summary plus an opaque
.h_NNN handle instead of a flood of printed output.tool_read_handle(handle, op) for subsequent inspection
(str, head, summary, print). Handles are addressable by
name in later run_r calls.tool_call, tool_result,
timings) to stderr. CLI drains them between calls.--trace flag (and options(corteza.trace = TRUE))
pretty-prints the events inline via printify::print_step() /
printify::print_message().NO_COLOR honored, FORCE_COLOR overrides,
classic Windows consoles fall back to plain text.bash to an absolute path (Rtools first,
Git Bash fallback) so C:\Windows\System32\bash.exe (the WSL
launcher stub) cannot intercept commands.cmd shell tool when no real bash is found.normalizePath(winslash = "/") consistently.callr (for the worker transport), printify (for --trace
rendering).codetools, curl, jsonlite, llm.api, processx,
saber.mx.api, tinytest.