Skip to main content
What it does. Lets one agent stay productive for hours or days without its context window filling up. A coordinator lead spends its window on coordination — it delegates every heavy, long, or high-volume task to a fresh-window child, and that child returns only a bounded summary plus a ResultRef handle to its full output on disk. The lead’s window grows by a sentence per task, not by the task. Long children surface a content-free progress signal every ~30 seconds so you can watch them advance without them completing, and every child runs isolated under an attenuated lease in its own jailed workspace — so this longevity is never bought with a loss of safety.
Coordinator vs. profile. autonomy.role is a sibling of autonomy.profile, not a replacement. The profile decides how much an agent may do (its capability set and guard rails). The role decides how it spends its own windowworker (the default) does the work inline; coordinator narrows the lead to an orchestration-only tool surface so heavy work must be delegated. The role never widens a capability; it only narrows the tool surface. The two dials are orthogonal.

The run-for-days pattern

A normal agent does its work in its own context: it reads the files, runs the searches, and accumulates every intermediate result in the window it is reasoning in. That is fine for a short task and fatal for a long one — the window fills, compaction kicks in, and the lead starts losing the thread of what it was coordinating. The coordinator pattern inverts this. The lead does not do heavy work inline. For each heavy task it spawns an isolated child (via sessions_spawn), the child does the work in its own context window, and it returns:
  1. a bounded summary sized for the lead’s context, and
  2. a ResultRef — a handle to the child’s full, unredacted output on disk.
The lead reads the summary, keeps the handle, and moves on. It only grows by the summary. The full output never enters its window unless it explicitly drills in. This is the longevity invariant. In words:
The lead’s context is roughly the sum of the per-child summaries (plus the context engine’s normal compaction), not the sum of the work. Because each summary is small and bounded, the lead stays roughly context-flat no matter how much total work the children do — which is what lets the same coordinator run for days. (This invariant is pinned by an architecture test, so a refactor that lets a child’s transcript leak into the lead window turns the build red.)

Tier-0 micro-compaction (every turn)

Underneath the periodic compaction, a Tier-0 micro-compaction pass runs on every turn — no LLM, no cost. It evicts stale tool results for the read-only tool set (read, grep, find, ls, exec, web_fetch, web_search), keeping the last K (the observationKeepWindow knob, default 25). It never touches user or assistant messages, thinking blocks, or recalled memory — only the ephemeral tool output beyond the keep window. Unlike the TTL-expiry and token-ceiling triggers (which only fire after an idle gap or once the context crosses a size ceiling), this runs unconditionally so the long-running coordinator’s context stays flat continuously, not only after an idle period. The replacement placeholder is cache-stable: frozen by tool-call-id and protected behind the cache fence, so a cleared result is byte-identical turn-over-turn and never busts the prompt cache (see the cache-stable prompt architecture).

autonomy.role: coordinator

Set the role in config — it is a single field on the autonomy block:
~/.comis/config.yaml -- a long-running coordinator lead
role defaults to worker, which is exactly today’s behavior — existing agents are unchanged, and a default install is byte-identical. Setting coordinator does one thing: it narrows the lead’s tool surface to the orchestration set, so the lead cannot do heavy work inline and must delegate. The coordinator surface. A coordinator lead is stripped to:
  • Orchestrationsessions_spawn, pipeline, cron, message, plus the session-management tools (sessions_list, sessions_history, session_status, session_search).
  • Read-only drill-inread, grep, find, ls, memory_search, memory_get, extract_document. These are how the lead inspects a child’s ResultRef on demand (next section) without ingesting it.
  • Observability — the obs_query tool, so the lead can ask about the health and outcome of the children it is coordinating.
And it excludes the heavy-work tools entirely: exec, edit, write, and browser are not on a coordinator’s surface. That exclusion is the delegate-then-synthesize doctrine made structural — the lead has no inline way to run a command, edit a file, or browse, so a heavy task has to become a child spawn, and the lead’s job becomes routing the work out and synthesizing the results back.
Narrows the tool surface; never widens a capability. role: coordinator changes only which tools the lead may call. The agent’s resolved capabilities (the orch:* permission tokens from its profile) are byte-identical with and without the role — a no-escalation invariant pinned by an architecture test. obs_query is a tool name on the coordinator surface, not a new capability: there is no obs:read capability to grant. The role can only ever take tools away, never add authority.
Explicit tool_groups still wins. If you set role: coordinator and an explicit tool_groups (or full) on the agent, your explicit choice overrides the coordinator default — progressive disclosure. The role is the convenient posture; an explicit tool list is the override.

Window isolation

The reason a coordinator stays context-flat is that each child runs in a genuinely isolated context loop — not a fork.
  • Isolated, not fork-mode. A forked child copies the parent’s prefix into its own window, which is the opposite of what longevity needs. A coordinator’s children are isolated spawns: each gets its own fresh context budget and its own message list. The child does not start from the lead’s transcript and, critically, the child loop never writes back into the lead’s message list.
  • Only the summary crosses back. The single thing that enters the lead’s window from a finished child is its bounded return summary (built from the child’s condensed result, not its raw transcript). The child’s full conversation, tool calls, and intermediate output stay in the child — reachable later via the ResultRef, never injected.
  • Parent history defaults to none. When the lead spawns a child it passes include_parent_history: "none" by default, so the child does not even start with the lead’s history. (You can opt into a condensed parent summary per spawn, but never the raw transcript — see Subagent Context Lifecycle.)
The net effect is the structural backbone of the longevity invariant: work flows out to children and only summaries flow back, so the lead’s window is bounded by the number of tasks, not their size. See Context Management for how the lead’s own window is budgeted and compacted on top of this.

Summary + ResultRef

A child can produce megabytes — a long file dump, a full crawl, a giant analysis. The point of the ResultRef is that none of that has to enter the lead’s window for the lead to have it. When a child finishes, Comis materializes its full output to the child’s own jailed workspace and hands the lead two things in the completion announcement:
  • the bounded summary (for context), and
  • a ResultRef handle — a reference plus the byte size and kind of the stored output.
The announcement carries a drill-in line shaped like:
The lead reads the summary to decide what to do next, and only if it actually needs the detail does it drill into the handle. It pulls in just the slice it needs, not the whole artifact. A megabyte child grows the lead’s window by a summary and a handle; the megabyte stays on disk. A ResultRef carries a full query engine — five in-jail extraction methods, all on the coordinator’s read-only surface:
The sql/jsonpath methods run the DuckDB CLI daemon-side (the jail stays --unshare-net), hardened to a read-only slicer: --readonly :memory:, extension auto-load disabled, and any extension/file-write/network statement refused before DuckDB is ever spawned. DuckDB is a prerequisite the daemon host provides; if it is absent, sql/jsonpath honest-degrade and read/grep/jq keep working. See the orchestrate tool reference for the full extraction API and the hardening details.

Measured savings

The run-for-days pattern keeps a lead context-flat by construction; an orchestrate run additionally puts a measured number on the saving. When a run materializes a high-volume tool return as a ResultRef on disk instead of re-entering it into context, Comis records how many tokens that avoided — a labeled estimate:
Both terms convert to tokens at the standard ~4-bytes-per-token approximation. It is a counterfactual estimate, not exact accounting, and it is deliberately conservative on both sides:
  • Materialized-only. Only bytes actually written to the run’s results/ directory count — a payload the script streamed past without materializing is never credited.
  • Post-bounce actual. The subtracted term is what the script actually re-entered (its console.log stdout, after in-jail slicing), so a run that re-logs its whole ResultRef nets ~zero saving, honestly.
  • Never negative. estSavedTokens clamps at 0; savedRatio — the saved fraction of the would-be cost — is reported only when there was a would-be cost to divide by.
The estimate surfaces in two places, both content-free (counts and token estimates only):
  • Per runcomis explain <sessionKey> carries it as savings: { estSavedTokens, savedRatio } on each entry of its orchestrate section.
  • Daemon-widecomis fleet rolls it into an orchestrate_efficiency finding: the run count and the summed estimated tokens saved across the window.
Because it is labeled an estimate at every surface (estSavedTokens, never savedTokens), it reads as a directional efficiency signal — “this run kept ~30k tokens of fetched data out of context” — not a billing figure.

Resumable runs

A genuinely long pipeline should survive a daemon restart mid-run instead of starting over. That is opt-in and nested under the durability posture: with autonomy.durability.orchestrateResume on (off by default, inside the already-off-by-default autonomy.durability block), an orchestrate run gains three durability affordances.
  • Checkpoint / resume inside the script. The in-jail SDK exposes checkpoint(stateJson) and resume(): a run persists a small progress blob (a longer-TTL ResultRef, same per-file/per-run caps as any result) and reads the last one back on a later attempt. resume() returns that state wrapped as untrusted data — it re-enters as data the script inspects, never as control it executes.
  • Timeout → a resumable row. When a resume-enabled run hits its wall-clock timeout, instead of only being killed and rejected it records a content-free resumable row { runId, scriptRef, checkpointRef } and skips the run-end cleanup so the pinned script + checkpoint survive. orchestrate({ resumeRunId }) then re-spawns the pinned original bytes (the script argument is ignored on resume — no new bytes are ever accepted) and resume() hands back the last checkpoint, so the run picks up where it left off.
  • Restart survival. The resumable row rides the durable-runs boot sweep: after a restart the daemon surfaces the run as resumable once it verifies the pinned script + checkpoint are still on disk (it does not re-execute the bytes on boot — resuming is an explicit resumeRunId call). If the checkpoint is gone, the run is surfaced as an honest orphan (a closed-enum reason) rather than silently lost, and its leftover workspace artifacts are reclaimed.
All of this is default-off: with orchestrateResume off, a timed-out run is killed and cleaned exactly as before, and no durable row is written. For debugging, an operator can deterministically re-run a durable run’s pinned bytes against a separate replay socket with comis orchestrate replay <runId>.

30-second progress fork

A delegated task can run for a long time, and an isolated child by design does not stream its work into the lead’s window. So that a long child does not look stuck, Comis attaches a read-only progress fork to it. Roughly every 30 seconds, while a child is still running, the fork emits a content-free session:sub_agent_progress event:
  • It is an advance signal, not a result. The progressLine is a short status (a few words like running, step N); the payload carries no child output, message body, or tool result — it is content-free by design (the same counts-and-ids-only observability rule the rest of the platform follows).
  • It is read-only. The fork does not re-run the child, call a tool, or spawn anything — it only reads the child’s elapsed time and step count and emits the event. It is read-only by construction: the fork has no execution channel at all, so it cannot burn budget or fan out.
  • It is lifecycle-bound. The fork starts when the child is spawned and stops on the child’s terminal boundary, so it never outlives the child and never leaks a timer.
The operator (and the coordinating lead, via observability) can watch a long child advance — “still going, on step 4, 90 seconds in” — without the child having to complete and without anything re-executing.

Security: isolation is never escalation

Spinning up a fresh-window child must not become a way to gain authority. It does not.
  • Each child inherits an attenuated lease. A child’s capabilities are the intersection of the parent’s capabilities and what the child requested (child_caps = parent_caps ∩ requested_caps) — a child can only ever be a subset of its parent, never a superset. A narrow lead spawns only narrow children.
  • Each child writes to its own jailed workspace. A child’s output is materialized to its own per-child jailed workspace, resolved from the child’s agent id — never the lead’s workspace and never a shared, un-jailed location.
  • The longevity machinery does not touch the lease. Window isolation, the ResultRef materialization, and the progress fork are all built on top of the existing attenuated-lease and jailed-workspace primitives — they read and surface, they never mint or broaden. A coordinator that runs for days spawns hundreds of children, and every one of them is bounded by the same intersection rule as the first.
The result is that the run-for-days pattern is also a least-privilege pattern: each unit of work runs in the smallest window, with the smallest capability set, in its own jail — and the lead that coordinates them all stays both context-flat and authority-flat. See Autonomy for the lease attenuation and capability model that bounds every child.

Autonomy

Named profiles, the capability set the role narrows around, and the attenuated lease that bounds every child.

Subagent Context Lifecycle

How a child is prepared, run, condensed, and announced back to the lead.

Context Management

How the lead’s own window is budgeted — the layer the longevity invariant sits on top of.

Compaction

The periodic compaction term in the longevity equation.

Config YAML Reference

The autonomy.role field and every other autonomy.* knob.

Sessions Tool

sessions_spawn and the session tools a coordinator orchestrates with.