Skip to content

v2.23.0

Choose a tag to compare

@nokhodian nokhodian released this 04 Oct 21:48
· 141 commits to main since this release

[2.23.0] — 2026-10-04

Added

  • Session cap with rotation digests. With run_config.context.session_cap set 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's session-counters.json, a session-rotated audit event records each rotation with its overshoot, and session-cap-usage-missing flags a runner that reports no usage. packets.jsonl generation records carry the rotation generation and the digest hash. An org with no session_cap is unchanged and writes no counters.

  • Role notes: org_note_append and injection into a fresh session. With run_config.context.notes: true, a role gets org_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 with current_state: true that 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's packets.jsonl records how many entries were included and left out and the hash of the injected block, and its first-message hash covers the notes. A bare require_brief or session_cap adds no notes tool, and an org without notes: true is 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.context surface, org_task_done's result is 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 no context key accepts any result length, as before.

  • Context packets with a record of what was injected. In an org that adopted the run_config.context surface, org_task and org_plan_graph tasks take references (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's packets.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.context surface, starting with typed brief fields. An org that sets a context key (require_brief, notes: true or session_cap) now gets objective, output, tools, boundaries and acceptance arguments on org_task and on each org_plan_graph task. They are rendered with the existing free-text brief into the one brief the assignee receives, under the same 4,000-character limit; a longer brief is rejected, not truncated. With require_brief: true a task missing objective or acceptance is rejected (for org_plan_graph, the whole graph, so no half-built plan is left), and other missing fields come back as warnings. Without it, a missing objective or acceptance is only a warning. An org with no context key keeps its tool list and prompt byte for byte. org validate no longer reports run_config.context as 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> --context prints, per role, the calls, sessions, mean and max context, cache hit ratio and the session-start cache split (--json gives 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 serve start, 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 as checklist-warning.

    • Errors: a deferred feature in the definition (sections, documents, loops, requires, and run_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_config key, 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_agents below the role count (a finished worker keeps its slot, so roles past the cap wait); no budget_usd on any priced role; budget_usd on 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 use SendMessage; 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.
  • 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 set run_config.experimental: "eval", and org run/org serve refuse to start ANY org that uses sections from the CLI regardless of that flag — only the internal eval harness (which passes evalGate: true to startOrg) 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.denyExec masks 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 without denyExec set is unchanged.

  • policy.sandbox.denyRead hides files and directories from a role's shell, in the same sandbox layer as denyWrite. Opt-in; a role without it set is unchanged.

  • policy.sandbox.homeWriteAllow keeps 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. A WriteLedger watches the bus for Write/Edit/MultiEdit/NotebookEdit calls, policy denials and tool_result ok:false. org_task_done is 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; an achieved org_complete is 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 open org_task, or an actionable message from its reports_to/the boss — that has no session after not_started_s (default 90s) or no bus event for silent_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 (and startOrg'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 a Task 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 --task still 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_send whose 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_usd closures 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 an interrupted stop 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_usd is 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's maxBudgetUsd, 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.id and 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: usage events were inflated and budget_tokens caps 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_PROCS throttled 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.

npm: monomind@2.23.0 · @monoes/monomindcli@2.23.0