

Canopy
Graph Operations
Last updated
Archive and restore
Graphs can be archived instead of deleted — they leave the main
graph list/sidebar view but their row, specs, and full run history
are untouched and can be restored at any time. The surface is MCP:
graph_edit archive / restore (graph_id) — there is no canopy graph archive or canopy graph restore subcommand.
Archiving refuses a running graph — pause it first. The archived graph
is still reachable directly by id via graph get regardless of
archived state. Restoring is a plain flag flip — no data is lost or
moved.
Node run history
When a graph fails, the node run history lets you diagnose exactly what happened:
The CLI prints the most recent node runs in canopy graph info <graph>.
Over MCP: graph node_runs (graph_id, spec_id?, node_id?, limit?,
offset?, compact?) returns the most recent runs first (default 20,
capped at 200); offset pages back through older runs and compact trims
each run to its summary fields.
graph node_run (run_id) fetches one run’s full stored input and
output, including infra_attempt/infra_crash markers when present, and
ended_after_verdict/session_end when an agent filed a verdict.
Secret-shaped substrings are redacted before the output crosses the
boundary.
This is the second step of failure diagnosis: list to find the offending run, then fetch its output here — the exact stderr/stdout/ reported_output the engine recorded.
Event log
Every graph keeps its own durable, structured history in the graph_events
table — the engine writes one row per event as the run happens:
| Group | Kinds |
|---|---|
| Graph | graph_started, graph_paused, graph_resumed, graph_completed, graph_failed, graph_blocked, pause_requested, pause_cancelled |
| Spec | spec_started (payload: spec_start_head, spec_start_dirty), spec_completed, spec_failed, spec_skipped |
| Node | node_started, node_finished (payload: verdict, platform, model, duration_s) |
| Mutex | mutex_waiting (payload: resources, waiting_for), mutex_acquired (payload: resources) |
| Hooks | hook_fired (payload: event, mode, ok, error) |
| Config | config_changed (a node, ensemble or hook edit made while the graph runs) |
| Notes | note (reserved for the CM47 spec-note mechanism; payload: owner_name, chars) |
Read it from the CLI:
canopy graph log <graph-id-or-name> [--since <id|RFC3339>] [--kind <k>] [--spec <id>] [--follow]
Each line is <created_at> <KIND> spec=<name> node=<name> <message>.
--follow polls the daemon every 2 s and prints new events until Ctrl+C.
Over MCP, graph events (graph_id, since_id?, since_ts?, spec_id?, kinds?, limit?,
include_payload?) returns events oldest-first with next_since_id for
paging.
This replaces the printf … >> file logging-hook pattern. You no longer
need a command hook that appends to a per-graph log file on on_completed /
on_failed — the engine records that history itself, intact and queryable.
Ordinary logging hooks keep working; they simply stop being necessary.
Unlike the shared activity stream (bounded per workdir, free text), this log
is never pruned by count. canopy clean removes events older than
graph_event_retention_days (default 90) for graphs that are not running.
Pending pauses
Pausing is graceful and visible: a pause requested while a node runs is
recorded at once as a pause_requested event naming the node(s) it waits
for, and the graph reaches paused (with graph_paused) at the next node
boundary. Meanwhile graph get/list still report status running,
with pause_pending: true and the waited-for names in pause_waiting_for;
the TUI shows pausing — waiting for <node> instead of the running
indicator. Requesting pause again while one is pending is a no-op answering
pause already pending, and continue/run while one is pending cancels
it — one pause_cancelled event, then the graph keeps running.
Tuning the engine
These [graphs] settings (see canopy config list) adjust the engine’s
retry budgets and caps. Each keeps its default until you set it.
| Setting | Default | What it does |
|---|---|---|
graphs.max_iterations_per_node |
5 | Node visits a spec may make before it is blocked (the iteration ceiling). |
graphs.infra_retry_limit |
2 | Infra-crash retries before the infra failure falls through the error edge. |
graphs.infra_crash_max_seconds |
60 | Longest an infra crash may last before the retry budget is exhausted. |
graphs.infra_backoff_seconds |
30 | Backoff between infra retries. |
graphs.straggler_timeout_minutes |
30 | Default timeout (1–480) for a new ensemble; existing ensembles keep theirs. |
graphs.ensemble_max_members_parallel |
8 (range 2–16) | Largest member count a parallel ensemble may have. Lowering it never edits an existing ensemble. |
graphs.ensemble_max_members_sequential |
16 (range 2–32) | Largest member count for a cascade/round_robin ensemble, and the largest crew. |
graphs.check_output_max_kib |
64 (range 16–4096) | Check-output tail kept, in KiB. |
graphs.verdict_grace_seconds |
15 (range 0–300) | Seconds an agent has to exit after filing its verdict before canopy terminates it. 0 disables the shutdown. |
graphs.catalogue_url |
https://graphs.univerlab.org |
Base URL of the catalogue canopy graph install and canopy node install fetch from. |
Two more engine settings sit outside the graphs. prefix (see
CLI reference):
spec_attempt_limit (default 3) caps how many separate executions of a spec
may fail with a verdict before it is blocked, and ensemble_concurrency_cap
(default 4) caps how many ensemble members run at once across every graph.
The three graphs.infra_* settings resolve with this precedence, strongest
first: member > ensemble > the platform’s clis[] entry > the global
setting. Nothing else supplies a value.
The member caps (graphs.ensemble_max_members_parallel /
graphs.ensemble_max_members_sequential) bound new writes only:
lowering a cap below an existing ensemble’s member count never edits that
ensemble, and an over-cap create is refused with the setting key named.