Repository navigation
v2.23.0
[2.23.0] — 2026-10-04
Added
-
Session cap with rotation digests. With
run_config.context.session_capset to{tasks, tokens}, a role's SDK session ends between turns once it has been given that many distinct tasks or has used that many tokens (a native child's responses are not counted; a response split across messages counts once). The turn that crosses the cap finishes; the next message starts a fresh generation, and the loop checks on every path: a tagged task session, the role session, an untagged message, and at session start (a resumed session at the cap starts fresh, its resume dropped). The new generation's first message opens with a rotation digest: what the last generation did, the role's open tasks, the budget used, and a warning after two rotations in a row without a finished task; the digest is within 4,000 characters, and what is dropped is named, never cut. Counters persist in the run'ssession-counters.json, asession-rotatedaudit event records each rotation with its overshoot, andsession-cap-usage-missingflags a runner that reports no usage.packets.jsonlgeneration records carry the rotation generation and the digest hash. An org with nosession_capis unchanged and writes no counters. -
Role notes:
org_note_appendand injection into a fresh session. Withrun_config.context.notes: true, a role getsorg_note_append, which appends an entry to its own<org dir>/notes/<role>.md. The file is append-only: to curate, a role appends an entry withcurrent_state: truethat restates what still matters. Each fresh SDK session of that role starts with its last current state and the entries after it, ahead of the task, within 4,000 characters and without pushing the first message past 12,000; an entry is included whole or not at all, and an entry over 4,000 characters is rejected, never cut. A resumed session is left alone. The run'spackets.jsonlrecords how many entries were included and left out and the hash of the injected block, and its first-message hash covers the notes. A barerequire_brieforsession_capadds no notes tool, and an org withoutnotes: trueis unchanged. Notes are not yet protected from other roles' direct file access; that protection belongs to the sections design. -
Summary-only returns. In an org on the
run_config.contextsurface,org_task_done'sresultis a summary of at most 1,000 characters plus the ids and file paths of what the role produced. A longer result is rejected with a remedy and the task stays open; it is never truncated. The tool's description says so only for such an org. An org with nocontextkey accepts any result length, as before. -
Context packets with a record of what was injected. In an org that adopted the
run_config.contextsurface,org_taskandorg_plan_graphtasks takereferences(files,memory_keys,task_ids), listed after the brief in the task's dispatch. A packet over 256 distinct references, or whose title, brief and references together pass 12,000 characters, is rejected with a remedy; nothing is truncated. The runtime records what it injected in the run'spackets.jsonl: each dispatched packet with the hash of every part, and the first message of each fresh SDK session (a generation) with its hash and the packet it carries. A resumed session adds no record; a new generation appends one. References are not snapshots: a path or key is listed, not captured. -
An opt-in
run_config.contextsurface, starting with typed brief fields. An org that sets acontextkey (require_brief,notes: trueorsession_cap) now getsobjective,output,tools,boundariesandacceptancearguments onorg_taskand on eachorg_plan_graphtask. They are rendered with the existing free-textbriefinto the one brief the assignee receives, under the same 4,000-character limit; a longer brief is rejected, not truncated. Withrequire_brief: truea task missingobjectiveoracceptanceis rejected (fororg_plan_graph, the whole graph, so no half-built plan is left), and other missing fields come back aswarnings. Without it, a missingobjectiveoracceptanceis only a warning. An org with nocontextkey keeps its tool list and prompt byte for byte.org validateno longer reportsrun_config.contextas unsupported. -
Per-call context logging. Every model call a role makes now appends one record to the run's
context.jsonl: the size of its context (uncached input plus cache reads plus cache writes), how old its session is, its cache read and write tokens, and whether it was the first call of a session, a resumed session or a subagent's call. A response split across several messages is one record. The first call of a session shows the prefix cache read vs write at session start.monomind org report <org> --contextprints, per role, the calls, sessions, mean and max context, cache hit ratio and the session-start cache split (--jsongives the same figures). A run's other files are unchanged. -
A shared checklist of org caveats, run wherever an org is saved or started.
org validate,org run/org servestart,org reload,org create, and the dashboard's config patch, import and create now run the same checks (orgrt/validate-checklist.ts). Errors stop the save or start; advice is a warning that never blocks, and a start records its warnings on the run's bus aschecklist-warning.- Errors: a deferred feature in the definition (
sections,documents,loops,requires, andrun_config.context,budget_usd,budget_mode,experimental) used to be ignored silently and now fails with "not yet supported". - Warnings: an unknown top-level or
run_configkey, which the runtime would ignore; file scopes that do not cover the paths a role's duties name; shell duties with Bash denied;max_concurrent_agentsbelow the role count (a finished worker keeps its slot, so roles past the cap wait); nobudget_usdon any priced role;budget_usdon a codex or antigravity role, which reports no USD and is never closed by it; tools (Bash, WebFetch, WebSearch) that wait for a human approval nobody answers, and the same in a scheduled org; role text that tells a role to useSendMessage; a dated detail in a role prompt, which breaks the cached prefix; a boss prompt that does not require self-contained briefs; a role with no explicit model.
- Errors: a deferred feature in the definition (
-
Org sections: an eval-only opt-in surface for sub-team structure within an org (
sections). A definition can now partition roles into sections with their own budgets, a single-writer policy per document, lead rights, loop/rework rounds, and spend-based soft closure with 80% budget notices to lead and root. This ships eval-only: a sections org must setrun_config.experimental: "eval", andorg run/org serverefuse to start ANY org that usessectionsfrom the CLI regardless of that flag — only the internal eval harness (which passesevalGate: truetostartOrg) can start one (packages/@monomind/cli/src/orgrt/documents/eval-gate.ts,definition.ts). It is not yet available for general org use. -
policy.sandbox.denyExecmasks programs from a role's whole process tree. Opt-in; a role's Bash tool and everything its process tree starts (a nested SDK sandbox included) run inside a bubblewrap layer where every matching binary is replaced and fails closed. A role withoutdenyExecset is unchanged. -
policy.sandbox.denyReadhides files and directories from a role's shell, in the same sandbox layer asdenyWrite. Opt-in; a role without it set is unchanged. -
policy.sandbox.homeWriteAllowkeeps a role's process tree from changing the real$HOME. Opt-in: bubblewrap mounts a tmpfs overlay over the home (reads fall through, writes land in a throwaway upper layer) and binds only the allowlisted subpaths back read-write. -
run_config.verify_writes(default on) refuses a completion over a demonstrably failed write. AWriteLedgerwatches the bus forWrite/Edit/MultiEdit/NotebookEditcalls, policy denials andtool_result ok:false.org_task_doneis refused, naming the path, when that role has a failed write with no later success and the file is still missing or empty on disk; anachievedorg_completeis refused for any role's unwritten deliverable. A report that names the path and says it is blocked is accepted; each(scope, path)is refused at most twice. -
run_config.lead_watch(default on) nudges a lead when a role stalls. A role with open work — an openorg_task, or an actionable message from itsreports_to/the boss — that has no session afternot_started_s(default 90s) or no bus event forsilent_s(default 180s) gets one[watch]message sent to its lead's mailbox, with doubling backoff, at most 3 per episode. Never fires for a role with no open work, a role that is progressing, or during a pending human wait; cancelling, re-creating for another role, or messaging the role settles it.
Fixed
-
The run's task text reaches every role, not only the coordinator.
org run --task(andstartOrg's task argument) was sent to the coordinator as a message; every other role's system prompt carried the org goal alone. With a short goal and the details in the task, workers asked the lead for what the task said, or read the run's bus to recover it. Every role's prompt now has aTask for this run:block with the text, whenever it differs from the goal. A run with no task, or a task equal to the goal, keeps its prompts byte for byte. A resumed run restarted without--taskstill has the goal alone. -
A tool-call fence that will not parse is returned to the role as an error. Roles on the fence-protocol runners (codex, antigravity, opencode, kimi and others) call org tools by writing a ```tool_call block. A block with invalid JSON, such as an
org_sendwhose message held raw newlines, or one cut off before its closing fence, was dropped with a note on the bus and nothing was sent back: the role believed the call had run and ended its turn, and the org could sit silent until the idle watchdog stopped it. The role now receives a `tool_result` saying its call could not be parsed, that nothing ran, and how to send it again; calls in the same reply that did parse still run. The bus note is unchanged. -
A role stopped mid-query now reports its cost. When an org completed or was stopped, every Claude role still working had its query killed outright; the query never sent a result, so its cost was never recorded. A short trial's meter showed $1.46 where its session transcripts put the spend near $5.65, and
budget_usdclosures never saw that spend. The Claude runner now interrupts the query first. The SDK answers with a result carrying the cost so far, which the runtime records like any other turn, as aninterruptedstop that is not a failed turn and does not count toward the circuit breaker. A query that does not answer within 3 seconds is aborted as before. -
A role's
budget_usdis a hard stop on Claude roles. USD cost arrives only when a turn ends, so one long turn could run far past the cap: in a 24-minute trial of an 11-role org capped at $10, a $0.80 role spent $6.50 and the org spent $27. Each Claude query now carries the SDK'smaxBudgetUsd, set to what the role has left of its cap, so the SDK stops the query once it is exceeded. Overshoot is now limited to the model call in flight. That stop closes the role for budget like any exhausted cap; it is not counted as a failed turn, does not trip the circuit breaker and is not reported as a crash. A role with no USD left starts no query. Codex and antigravity roles report no USD and are unchanged. -
The org token meter counts each model response once (#597). One API response can reach the SDK stream as several assistant messages that share a
message.idand repeat the same input and cache usage. The live meter added every one of them, so roles were metered at roughly twice their real tokens:usageevents were inflated andbudget_tokenscaps closed sessions early. The result-time settle only tops the meter up, so it never corrected this. The Claude runner now passes the response id through, and the meter counts only what a message adds over the largest usage already seen for that response. That also takes a response's final output count instead of the placeholder counts on its earlier messages. Subagent responses have their own ids and are still counted. USD costs were not affected. -
The resource governor's SDK-session count no longer doubles when a role runs sandboxed. It matched any process whose command line referenced the SDK package path; a sandboxed role's bubblewrap wrapper process matches too, so each role's session was counted twice and
MONOMIND_MAX_SDK_PROCSthrottled new role starts at roughly half its configured cap. It now reads the process table directly and counts only the actual SDK agent process per role.