Guide

Depth & Recursion Queries

Separate the modeled base case, group-local depth evidence, and hard birth refusal.

Every recursive model needs a base case. Every recursive runtime also needs a hard boundary for the case where the model gets that decision wrong. Those are different responsibilities.

Make the base case part of the model

Use the recursive reference's depth Query to decide whether another node belongs in the domain:

const descendants = Query.gen(function* () {
  const depth = yield* Self.depth;
  return depth < 3 ? [`depth-${depth + 1}`] : [];
});

At depth three, this Query returns no requested children. The family settles through its ordinary empty or completed path. No runtime failure is involved because the model did not ask for an impossible birth.

recursive: { maximumDepth: 3 } remains on the contract as a safety fence. It is not an alternate way to express that base case, and the engine does not treat a crossed fence as an empty child family.

Query + base case + hard refusal

A base case ends the model; a fence refuses the impossible birth.

Follow one WorkNode group from its depth-zero root through normal completion, then request the one child the contract forbids.

1 · Read group-local depth

Recursive depth starts at zero for the group's first instance.

`Self.depth` counts consecutive ancestry edges inside this recursive group. An unrelated parent does not consume the group's budget, and absolute ownership depth remains a different query.

Recursive group · WorkNode1 of 3
Current querySelf.depth = 0
Maximum depth3
Birth resultNot requested
  1. 0rootexact instance
  2. 1depth-1exact instance
  3. 2depth-2exact instance
  4. 3depth-3exact instance
Depth is evidence, not a loop counter.

2 · Model the base case

The ordinary path completes before the safety fence.

Depths zero through two request one child. Depth three returns its ancestry instead. Four exact instances settle normally, and `maximumDepth` never participates in successful domain behavior.

Recursive group · WorkNode2 of 3
Current querySelf.depth = 3
Maximum depth3
Birth resultBase case
  1. 0rootexact instance
  2. 1depth-1exact instance
  3. 2depth-2exact instance
  4. 3depth-3base case
The model stops itself.

3 · Prove the failure direction

A forbidden birth is refused, never silently omitted.

If depth three requests another child, the engine produces `RecursiveDepthExceeded` before allocating child identity or durable Birth. The exact child site receives its ordinary Defect outcome with incident evidence.

Recursive group · WorkNode3 of 3
Current querySelf.depth = 3
Maximum depth3
Birth resultRefused
  1. 0rootexact instance
  2. 1depth-1exact instance
  3. 2depth-2exact instance
  4. 3depth-3base case
  5. 4No child allocatedRecursiveDepthExceeded
No generation, Ref, Birth, or mailbox exists.

Distinguish group depth from ancestry depth

Self.depth is derived from engine-owned ancestry for one exact recursive group. The group's first instance is depth zero. Consecutive child edges whose ancestry frames carry the same recursive group id increment the value. A nonrecursive parent above that first instance does not consume the group's depth budget.

Query.ancestryDepth answers a different question: how far is this instance from the ownership root across every child edge? In a chart embedded below an ordinary parent, ancestry depth may be greater than recursive depth. Use the former for ownership location and the latter for a recursive base case.

Let query strictness carry the membership proof

Inside a member of the recursive group, yielding Self.depth is a strict read: the chart topology proves group membership. The result is a number, not an optional guess reconstructed from repeated chart names.

Reusable logic that may run outside the group can yield Query.option(Self.depth) instead. It returns Option.none() when the current ancestry does not end in that group. A strict read without membership fails with RecursiveDepthRequiresGroupMembership; it never falls back to zero.

This is the same rule as other strict and optional Queries: absence belongs in the type until the placement topology proves presence.

Refuse the birth before identity exists

When a parent at depth three requests a same-group child, the requested depth is four. The engine compares that value with the recursive contract before minting a generation, Ref, child InstanceId, durable Birth, mailbox, runner, or partial parent plan.

The refusal produces StatechartError.RecursiveDepthExceeded with the requested depth, maximum depth, and parent InstanceId. It is recorded as an incident and delivered through the exact child site's ordinary Defect channel. A modeled handler may supervise that failure locally; an escaped defect follows the normal incident and Park rules.

Silent truncation would make “no children” indistinguishable from “the engine refused a child the model required.” Motive preserves that distinction as durable evidence.

Bound width and authority elsewhere

A lineage fence limits consecutive depth. It does not limit an each family's number of siblings, concurrent Activities, model calls, tokens, open files, or external capacity. Use family membership and weighted Resource capacity for width and scarce work.

Depth also grants no external authority. Recursive child edges inherit or bind only the Resource requirements their declared boundary permits; attenuation may narrow that authority at every descent.

Test the model and the fence separately

  1. Prove the modeled base case completes without touching maximumDepth.
  2. Prove Self.depth is zero at the group's first instance even beneath an unrelated parent.
  3. Prove Query.ancestryDepth still reports the complete ownership path.
  4. Request one forbidden child and verify no generation, Ref, InstanceId, Birth, or mailbox exists.
  5. Verify the exact child site receives RecursiveDepthExceeded and the incident retains its full Cause and parent coordinate.
  6. Bound breadth, work, and Resource authority with their own mechanisms.

Next, Child Lifecycle & Restart follows one exact recursive child across runner loss and checkpoint recovery.