Episode capture (capture_kind=episode)¶
An EPISODE is a multi-step generation run — a loop that calls one model N times, feeding each step from the last — captured as ONE wrapped session product. Declare it on the ordinary capture entry:
import torch
import torchlens as tl
from torchlens.options import EpisodeOptions
class GreedyRunner(torch.nn.Module):
def __init__(self, model, n_steps):
super().__init__()
self.model = model # the stepped model
self.n_steps = n_steps
def forward(self, ids):
tokens = []
current = ids
for _ in range(self.n_steps):
next_token = self.model(current).argmax(-1, keepdim=True)
tokens.append(next_token)
current = torch.cat([current, next_token], dim=1)
return torch.cat(tokens, dim=1) # one integer tensor of emitted tokens
log = tl.trace(
GreedyRunner(model, 20), prompt_ids,
episode=EpisodeOptions(stepped_module=model, num_steps=20),
)
ledger_payload = log.annotations["episode"] # header + per-step rows
Every name on this surface is experimental and may change in any release; the semantics below are the settled contract.
Cost: a diagnostic-tier product, for tens of steps¶
The wrapped episode tier is the verification oracle / deep-dive product. Its
cost is SUPERLINEAR in step count — measured on gpt2-124M (CPU, 8-token
prompt): N=20 costs 79 s (103x native) with a 146 MB artifact and 1.9 GB peak
RSS; N=100 costs 657 s (323x native), 947 MB, 5.4 GB peak RSS. Do not plan
wrapped episode captures for hundreds of steps; the guarded-fast tier
(trace.run_transaction(inputs=..., fast=True)) is the engine for episode-scale
re-runs, and it must reproduce the wrapped tier's tokens bit-exactly (a
pinned cross-tier identity test guards this). The guarded-fast tier is NOT
reachable from the default capture above: fast=True collects functional
operations only when the capture requested them explicitly, so an episode
capture meant for fast re-runs must pass a functional save predicate --
tl.trace(runner, ids, episode=..., save=tl.func("cat")) (any tl.func(...)
selection covering the loop's functional ops) -- or the run refuses typed
(run_capability_unavailable, teaching the save= remedy). The default
capture re-runs through trace.run_transaction(inputs=...) (the full transactional
tier) instead. Either re-run is a FRESH execution: the product drops the
episode ledger to a travel note and clears the per-op episode_step stamps
(the travel policy below), and the product saves and loads as a plain
capture.
The declared-step cost ceiling. A declaration beyond
EPISODE_DECLARED_STEP_CEILING (100 steps, counting num_steps or the
forced_tokens length) refuses typed (episode_step_ceiling_exceeded) at
declaration time, before the forward runs.
EpisodeOptions(acknowledge_step_cost=True) is the explicit override: a
hundreds-of-steps wrapped capture is a decision, never an accident.
The declaration's own cost, priced. Declaring episode= changes NOTHING
about the executed model call or the recorded graph (the four-clause
invariant below), but it is not literally free: settlement derives the token
column from the already-computed root output, costing exactly one
aten.select.int plus one aten.view.default dispatcher read per declared
step — visible to any user-side dispatch-mode counter or profiler. On CUDA
the per-step .tolist() tail is one host sync per step (a named cluster
measurement, not a claim). Wall time is NOT an oracle for the declaration
(measured signs disagree across machines); the permanent regression is the
four-clause invariant: identical ordered recorded op labels, root output
bytes, post-run RNG state, and stepped-module call count between
episode-declared and undeclared runs of the same seeded fixture.
The declaration¶
EpisodeOptions(stepped_module=...) declares the STEPPED MODULE — the
nn.Module whose successive top-level calls define step boundaries. It must
be a PROPER submodule of the traced episode root. On a BOUND-METHOD root
(the ruled root contract: tl.trace(model.generate, ids,
episode=EpisodeOptions(num_steps=N)) — the owner resolves via
method.__self__ and is the root self of the TL-authored wrapper root's
tree; the stepped address recorded is self) stepped_module DEFAULTS to the owner; on a module root it is
required, and None refuses typed (episode_declaration_invalid) — wrap
the loop in a module or trace the bound generation method directly. Step
tallying is FLAT: each
top-level call of the stepped module is one step, regardless of loop nesting
inside the root's forward. Call 1 is the prefill (ledger row 0,
role="prefill"); later calls are decode steps.
Episode grouping keys on successive top-level calls of the declared stepped
module, never on pass-interior graph equality — so a ragged interior (a
routed mixture-of-experts model whose expert runs at only some steps) cannot
change the step count — and pass indices on the trace's layers are per-site
OCCURRENCE counters (pass k of a layer is the k-th time THAT layer ran),
not episode steps: a layer absent from a step has no pass there, so pass k
need not lie at episode step k.
Declared episode-carried state beyond the built-in scope (token prefix, KV
cache, RNG streams) is preflighted at DECLARATION time, unconditionally: any
item without snapshot/restore support refuses typed
(episode_state_unsnapshotable) before the forward runs.
Refused combinations (typed, episode_declaration_invalid): chunked
forwards, cache=True, value-free save policies under an evidence-deriving
kind (tokens/digest evidence derives from the retained root output;
step_output_kind="none" declares a status-only ledger and ADMITS
value-free saves — the kind-conditional save rule), a structure-only marker
(structure-only episodes are out of scope per the ratified
marker-combination table), and non-torch backends.
The declared evidence derivation¶
Per-step evidence is DECLARATION-DRIVEN, never root-output-shape guessing:
step_output_kind="tokens"(default) reads integer per-step token ids from the declared source; a float/complex source refuses with a teaching message naming the two declarations below.step_output_kind="digest"reads per-step CONTENT DIGESTS of the declared source (sha256:-prefixed, over dtype + shape + raw bytes of the per-step slice) — any dtype, so float-emitting stepped roots (hidden states, logits) are first-class.step_output_kind="none"derives no evidence at all: a status-only ledger, the honest shape for roots whose output has no per-step structure (a diffusion model returning one final image).
step_output_from declares WHICH root-output slot the evidence derives
from, as a dot-separated container path (dict keys and
namedtuple/ModelOutput field names by name, tuple positions by index —
"sequences", "0", "0.logits"). Undeclared, the root must return a
single output tensor; multi-output roots refuse with the available slots
listed. The resolved source disclosure is written to the ledger header
("output" for the single-output default).
Per-step positions are TAIL-ALIGNED along the declared step_axis by
default: the LAST num_steps positions are the per-step emissions (one
appended position per step). A root returning what real generate() returns
— prompt+completion — keeps its prompt prefix out of the evidence column, an
emitted-only root is the equal-size special case of the same rule, and a
source carrying FEWER positions than steps ran refuses typed. No cache
arithmetic is involved anywhere: the KV-cache last-token feed and the
append-all feed derive identical evidence columns from identical episodes.
The positions actually read are DISCLOSED in the header's
step_output_positions (one step-axis index per row, bound by the capture
digest; None when no column derived), so any reader re-derives the column
from the retained root output without the live measurement; grammar-v2
artifacts written before the disclosure lack the slot and read as
tail-aligned.
The tail-alignment license (token-shaped episodes). Tail alignment
presumes ONE appended position per step. On a token-shaped episode
(step_output_kind="tokens", or any kind whose step-0 entry was a
token-id-shaped integer tensor) the derivation is licensed only when the
source width is exactly num_steps (emitted-only root) or exactly the
MEASURED step-0 entry width plus num_steps (prompt+completion); any other
width -- multi-token decoding (speculative/Medusa/MTP), draft+verify loops, a
stepped module called more than once per iteration -- refuses typed
(episode_declaration_invalid) with the licensed widths named, instead of
silently misattributing tokens to steps and then grading FALSE exogenous
breaks against the misattributed column. The ONE further admission is the
declared-crossing chain arm when the root returns the FULL fed
id sequence -- every step's MEASURED entry is a prefix of it, row for row --
each step's emission is the position right after its own entry, and
positions a tool call injected between steps are admitted exactly when the
receiving step is declared in EpisodeOptions(crossings=(k, ...)); the column
derives at the measured chain positions (disclosed in
step_output_positions) and the join into the crossing grades declared.
An UNDECLARED surplus stays refused, with the crossings remedy named:
measured alone, two extra positions in a step's entry are indistinguishable
from multi-token decoding by the stepped module, so the declaration -- never
a root-shape guess -- decides. A digest column over a float
carried state (fixed-point / diffusion loops, where no token width exists)
is a disclosed positional slice convention keyed by the header's
step_output_from / step_axis -- recomputable by any reader, consumed by
no join re-grade -- and keeps the >= num_steps admission.
The header's capture_digest binds the ledger to ITS product: minted at
settlement (mint_capture_digest, schema tag episode_capture_digest_v2)
as hex SHA-256 over canonical JSON of the product's recomputable identity
facts — ordered recorded op labels, stepped-module address and call count,
and the managed entry_seed — PLUS every persisted ledger fact (the header
declaration and disclosure fields, the step_join envelope, the
intervention_digest, every row's status, coord, step_output, and witness
slots; the random episode_id is excluded so identical re-runs mint
identical digests). A consumer recomputes it from the product and its
persisted ledger and compares: any post-mint rewrite of ledger content
reads unbound (episode_coupling_unbound). The digest is an INTEGRITY
binding, not a signature — a forger who re-mints over swapped content is
caught by the evidence RE-DERIVATION cross-check: attestation and load
re-derive the evidence column from the product's retained root output and
compare it to the rows (CouplingAttestation.evidence_rederived is True
when it matched, None when nothing could be compared — kind="none" or an
unretained payload — and a mismatch refuses/quarantines). The historical
v1 form hashed identity alone, so two equal-length prompts minted identical
digests and a rewritten ledger still attested bound; a persisted v1 digest
now reads as PRE-BINDING (episode_coupling_unmintable, re-capture to
bind), never as foreign. The attested-coupling work consumes the
binding; the travel policy below stays the negative guarantee it
complements.
Attested coupling (episode= x intervene=)¶
The combination runs COUPLED (the foldA flip — the historical pre-execution refusal retired when the verdict's evidence bar was met, and every name here is experimental):
- Product binding. The ledger's
capture_digestbinds it to the exact product (above);trace.episode_couplingrecomputes and compares — the positive "this ledger belongs to this product" claim. A mismatch refuses typed (episode_coupling_unbound); a pre-binding artifact refusesepisode_coupling_unmintable. - Step-qualified selectors.
torchlens.at_step(*steps)matches ops recorded inside the named 0-based episode steps — live (the armed join session's step position) and post hoc (the persistedOp.episode_stepstamps, written at settlement on every episode product). It composes with any selector and with pass-qualified labels (the step-x-pass cross-product). Evaluating it where no steps exist refuses typed (episode_step_selector_without_episode,episode_step_unstamped,episode_step_selector_invalid). - Perturbed fidelity and fire counts. A coupled capture arms a
fire-attribution session: every live
FireRecordis attributed to its step (or the outside-step bucket for root-loop fires before/between/after steps — disclosed in the digest, never guessed into a row). Settlement writes the reserved slots: per-rowfire_count(a measured0on zero-fire started rows — zero and multiple fires are first-class facts;Noneonly on rows that never started), the headerintervention_digest(hex SHA-256 over the ordered fire facts and the armed rule identity, schemaepisode_intervention_digest_v1; deterministic, so identical re-runs mint identical digests — compare digests, never output equality alone), andfidelity_basis="perturbed"whenever a fired edit replaced a value (outranks theforced/escalation bases;token_feedstill discloses a forced feed separately). - Per-segment facts.
trace.episode_coupling.segments(alsotorchlens._capture._episode_coupling.coupling_segments) derives contiguous step runs that never span a measured break join (exogenous/declaredgrades): per-segment step ranges and fire-count totals, with unchecked joins and unmeasured envelopes disclosed. A claim never spans a break. - Replay derives a fresh ledger or refuses.
run()on a coupled product refuses typed on BOTH engines (episode_coupled_replay_underivable): no provider re-arms the capture-time intervention, so no engine can derive a fresh ledger; re-capturing withepisode=xintervene=is the fresh-ledger arm. Ado()edit on a product carrying episode evidence quarantines that evidence on the edited product (episode_evidence_dropped_perturbed_replaynote grammar) — a perturbed replay is a different execution, and the ledger never rides it silently.
Discoverability¶
trace.provenance.kind reads "episode" on episode-declared products and
"plain" otherwise (including fresh re-execution products whose evidence
the travel policy dropped, below). trace.episode is the public step
access: the parsed ledger with header, ordered per-step rows,
steps_completed, and truncated_at_step — None for plain captures and
quarantined/dropped payloads. Both names are experimental and may change in
any release. The declaration class is tl.options.EpisodeOptions.
Settlement refusals keep the product¶
A settlement refusal (episode_ledger_incoherent, the declaration-mismatch
derivation refusals) fires AFTER the capture ran and postprocessed, so the exception
carries the settled product on exc.partial_trace — inspect it there instead of paying the
forward again.
Evidence never rides a fresh execution (the travel policy)¶
The per-step ledger is evidence about ONE executed forward. A fresh
re-execution product — trace.run_transaction(inputs=...) on any provider, including
guarded-fast — never carries the original capture's episode ledger: the
annotations travel policy (torchlens/_capture/_annotations_travel.py)
replaces it with an inert in-key note
(episode_evidence_dropped_fresh_execution) at the provider settlement
finalizer, and capture-time observer values (logged_values) are removed
the same way. A product reporting path_faithfulness=VERIFIED therefore
never presents another execution's step evidence. Plain trace.fork() is
not a fresh execution and carries the ledger until an engine re-executes.
Re-capture with episode= to derive step evidence for new inputs.
Cross-step continuity: the measured step_join¶
The header's token_feed reflects the user's DECLARATION of how step inputs
were fed. The step JOIN — that step k+1's input actually CONTINUED step k's
output — is MEASURED (the trace-verb verdict's mid-call oracle
break contract): live boundary hooks on the stepped module snapshot each
step's entry/exit under pause_logging (invisible to the recorded graph;
the four-clause declaration invariant holds), and settlement re-grades every
join exactly, persisting the episode_step_join_v1 envelope in the header's
reserved step_join slot. The check grades the join; it never claims to
know whether the cause was a tool, a human, a file, a network, or a process.
Per-row grades (closed vocabulary; the join INTO step k, grades[0] is null):
continuous— direct continuation: token entries align as a suffix of the prior entry plus the evidence-column emission (append, last-token KV-cache, and sliding-window feeds are all one rule), or the entry is byte-identical to the prior step's output (digest_directbasis).forced— a teacher-forced join (EpisodeOptions(forced_tokens=...)): the entry continues the prior entry plus the DECLARED forced token for that step (forced_tokensbasis). Graded against the declaration, never the evidence column — a forced root may return the model's predictions, which are not what was fed. Not a break: the feed is declared and chain-shaped by construction; it is also never read as a freecontinuousjoin. An entry that does not continue with the declared token gradesexogenous(the declaration does not match the executed feed).transformed— the entry derives from the prior step's output through ops observed inside the capture (the pass-qualified graph witness; the scheduler/diffusion shape).declared— the entry crosses a boundary declared inEpisodeOptions(crossings=(k, ...))(the tool-call shape): disclosed, never silently continuous — the run is chain-shaped by declaration.exogenous— a MEASURED undeclared break: content entered the loop from outside the capture (the envelope counts the exogenous positions on the token basis).unchecked— the join was not measurable (no tensor entry, snapshot failure, oversize entry, a declaredstep_input_fromthat names no tensor argument); the reason is recorded, never a guess.
Which argument is the entry. The join measures the stepped call's
CARRIED INPUT, chosen by a disclosed preference rule and persisted per step
in the envelope's entry_basis slot ("positional[i]" / "kwarg[name]"):
an explicit step_input_from declaration (positional index or keyword name)
is honored exactly; otherwise a tensor under a preferred keyword
(input_ids, decoder_input_ids, inputs_embeds, sample,
hidden_states, x) wins, then the first non-auxiliary tensor argument
(masks, position_ids, cache_position, timestep/t, labels are
auxiliary; a 0-d/one-element float tensor beside a wider tensor is read as a
timestep and passed over), then any tensor. The historical rule was "first
tensor argument" with no disclosure, so a kwargs-first masked LM graded its
attention MASK (false exogenous positions on a true continuation) and a
timestep-first denoiser graded its TIMESTEP. Envelopes written before this
slot existed load with the argument undisclosed (entry_basis absent).
Three arms (both arms are BUILT; the ruling selects the default's shape):
- DEFAULT (
feed="open",on_feed_break="disclose"): the one Trace returns with the break marked (break_step, pluslive_break_step— the live check detects an undeclared break at the NEXT step entry, the first observable point: one-step detection latency). Episode-dependent claims across a measured break refuse typed — step-series reads (EpisodeLedger.step_output_series()), whole-episode replay (run()/run(fast=True)), the blessing fold (derive_episode_status), and escalation (episode_feed_break_exogenous/episode_join_declared_crossing). Ops, values, graph, audit, and per-segment reads are unaffected; the root call may truthfully be COMPLETE — the closed-episode claim is what breaks. - arm (b) (
on_feed_break="refuse"): the settlement write raisesepisode_feed_break_exogenoustyped, carrying the settled product (with its measured envelope) onexc.partial_trace— recoverable partial evidence, nothing discarded. - STRICT (
feed="closed"): the feed is declared closed (direct continuation). An UNDECLARED crossing halts capture at the next step entry (episode_feed_closed_violation; rows 0..k-1 complete, row k interrupted, later rows absent, all onexc.partial_trace) — or settles typed over the completed capture in the residual case where the break is only measurable against the settlement evidence column (a single-position injection is live-indistinguishable from an emission). A DECLARED crossing under strict mode stops BEFORE entering the crossing step (episode_declared_crossing_stop).
The claim stays PER-ARTIFACT: a ledger without the envelope (a
pre-measurement artifact, or a failed live measurement) reads
step_join=unmeasured on every surface, its series claims refuse
episode_join_unmeasured, and trace.explain prints the disclosure
line — an unmeasured join can never read as a measured one, whatever the
build measures. Loads validate the envelope FAIL-CLOSED against the rows
(schema, closed grades, prefill null, break-step coherence); any violation
quarantines episode_ledger_incoherent.
The ledger¶
The per-step status ledger lands ON the product at
trace.annotations["episode"] after settlement, in the GRAMMAR v2 shape
(the coordinated tlspec-v9 amendment; episode_ledger_version=2,
family-local — future grammar changes bump the family version, never the
tlspec version): a header (episode_id, stepped-module address, the
managed-RNG entry_seed, token feed, provenance tier, escalation
disclosures, the declared step_output_kind — tokens (default) /
digest / none — with the step_output_from source disclosure, the
declared step_axis, and the minted capture_digest binding (all written
by the declared derivation above), plus the measured step_join envelope
(the section above) and the intervention_digest coupling slot
written on intervened captures (the attested-coupling section
above)) and one row per step
(episode_step, role, status, coordinates, the generic step_output
under the declared kind, the carried-state witness slots
entry_state_digest / exit_state_digest, and the fire_count coupling
slot — measured on coupled captures, None on uncoupled ones). Row status
is a closed vocabulary:
complete— step k's forward returned (row-scoped truth, never a claim about the product; product truth is the settledCaptureOutcome).interrupted— the settled outcome's frontier lies INSIDE step k (a halt or failure mid-step; the frontier disclosure rides the row).absent— the step never started.
Rows obey the MONOTONE PREFIX LAW (complete prefix, at most one interrupted
row, absent tail) and are write-once at settlement. The ledger is a
DISCLOSURE, never a settlement authority: the episode product settles through
the one existing CaptureOutcome authority with its vocabulary unchanged,
and every capability gate (N1–N5) applies verbatim. Truncation stays a
run/report term: an episode halted at a step boundary settles STOPPED with a
clean prefix and the truncation disclosed on the ledger
(steps_completed, truncated_at_step) — nothing rounds up.
THE cache_len FIELD IS DELETED (grammar v2). It was a derived arithmetic
guess (prompt length + step) that is wrong on real KV-cache feeds and never
a measured cache fact. Its replacement is the MEASURED GENERIC carried-state
witness (the SV-6 semantic ruling): entry_state_digest /
exit_state_digest are channel-keyed digests of the carried state at the
step's boundaries, None means NOT MEASURED — the only value any shipped
writer emits — and no field may imply an unmeasured cache fact. The
F-WITNESS work designs the full witness schema after the retention seam
and writes real digests; until then absence is the honest disclosure.
Loads validate fail-closed: an episode ledger on a capture without the
declaration refuses typed (episode_ledger_without_declaration); a
structure-only ledger carrying step output refuses typed
(episode_ledger_payload_in_structure_only); any other geometry violation
QUARANTINES the payload with one warning (episode_ledger_incoherent) — the
rows stop being claims and the outcome derivation treats the ledger
fail-closed. The family version field is CONSUMED at load: any
episode_ledger_version other than 2 — including the version-less
pre-amendment grammar v1 payload (cache_len/tokens rows) — quarantines
with the grammar named, and never normalizes (the old-payload row
is a quarantine record, never a normalization ladder). The generalized
step_output_from/step_output_kind derivation is active on this grammar, with its own
MIGRATIONS row (the authorization line lives in MIGRATIONS.md).
Persistence¶
annotations["episode"] persists plainly as of the tlspec v8 coordinated
bump, as does the Bundle member_relations key (below). Loads validate
fail-closed: an undeclared ledger refuses episode_ledger_without_declaration
and geometry violations quarantine episode_ledger_incoherent. Pre-v8
artifacts never carry the key (the v7 scrub dropped it; the live trace kept
its session-time ledger).
Loads anchor the ledger to the product. A grammatically valid ledger is
not enough: the presence of the key was once the ONLY anchor, so a ledger
copied from a real episode artifact into any artifact's
annotations["episode"] loaded with zero warnings and made the product an
"episode". Every load now checks, in order, that the header's
stepped_module recorded at least one call on this product (from the
restored op records' module entries), that the started-row count equals
that call count and every row's member_call_index names a recorded call,
that the persisted capture_digest equals the digest recomputed from the
product and the ledger's own content (a legacy v1 identity digest is
admitted at load and reads pre-binding at attestation), and that the
evidence column re-derives from the retained root output (skipped, never
failed, when the payload is not retained). Inside a .tlspec load the
payload blobs attach only after the metadata restores, so the bundle loader
runs that fourth anchor a second time once the payloads are attached: a
ledger re-minted over another execution of the same program is refuted by
the LOAD itself, not on the first trace.episode_coupling read (which
still re-derives against the attached payload). Any failure quarantines
episode_ledger_incoherent with the failed anchor named in the note's
detail; the product still loads, its rows stop being claims, and
Trace.provenance.kind keeps reading episode (a quarantined ledger is a
declaration in doubt, not an absent one — the per-op episode_step stamps
of a corrupted genuine artifact must stay loadable).
Teacher forcing (disclosed, non-verifying)¶
EpisodeOptions(forced_tokens=...) declares a teacher-forced feed: the driver
feeds the declared tokens instead of the model's emissions. The ledger header
records token_feed="forced" and fidelity_basis="forced" — an explicitly
NON-VERIFYING disclosed mode; token-fidelity obligations never verify a
forced episode. Recompute-and-compare remains the verifying default
everywhere else. The measured step_join of a forced episode grades each
join against the DECLARED token (forced grade, forced_tokens basis), so
a root that returns the model's predictions instead of the fed tokens is not
misread as an exogenous break; step-series reads, replay, and the fold stay
open across forced joins.
The managed RNG recipe¶
Episode captures use the managed seeding discipline: the capture's effective
seed (drawn or passed, exactly as for any capture) is recorded as
the ledger entry_seed. Re-running the same declaration with the same seed
reproduces a sampled episode bit-identically; pass
capture=CaptureOptions(seed=...) explicitly for cross-run
reproduction.
Escalation (session semantics)¶
A failed or suspect cheap-tier episode escalates by re-running the WHOLE
episode wrapped, disclosed — never a partial product. Build the declaration
with torchlens._capture._episode_ledger.escalation_spec(producer,
stepped_module=..., reason=...): it derives escalated_from (a digest over
the producer's persisted outcome payload + ledger rows), the closed-vocabulary
reason (step_failed / divergence / requested), and the producer's
token column, so the escalated capture discharges the fidelity obligation at
write time: prefix-equal token columns record fidelity_basis="tokens"; a
mismatch records "diverged" — the escalated product is still a valid
capture of what it ran, it just is not an escalation of the original episode,
and says so. Fidelity is a disclosure, never a settlement input.
The floor: per-step Bundles + the derived fold¶
The declared minimum episode product (invocable by the sprint orchestrator on
size evidence only) is N per-step captures composed in a Bundle with
episode_member relation rows, under OBSERVER DISCIPLINE: the driver's
native forward owns the live carried state; each capture observes an isolated
copy; a capture product is never the source of carried episode state.
Bundle.episode_status(episode_id, episode_ledger=...) folds member outcomes
into a DERIVED episode status (episode_complete /
episode_halted_at_step / episode_aborted_at_step /
episode_failed_at_step / episode_unknown) — a total, fail-closed
derivation, never a Bundle-level settlement. Fold arms that consume
ledger-only declaration facts (the declared step count; a driver-declared
boundary halt) degrade to episode_unknown without a re-supplied ledger —
disclosed truth loss until the schema bump persists the ledger. The episode's
provenance tier is the MINIMUM of its members' tiers, never the maximum.
Bundle relation rows never require alignment, never assert structural
comparability, and never dangle: mutators either cascade explicitly
(cascade_relations=True) or refuse typed. See
docs/reference/error_refusal_contract.md for the full episode/bundle
refusal code family.
Relation grammar v2 (the amendment)¶
As of the coordinated tlspec-v9 amendment, relation-kind param keys
split REQUIRED/OPTIONAL per kind (both sets closed; an undeclared key still
refuses). successor_of — whose direction is PINNED: from is always the
LATER member (the successor), to the earlier member it succeeds — admits
three optional params: the evidence envelope ({schema, items[],
facts_digest}, items graded over the closed
verified/consistent/disclosed/unchecked/divergent vocabulary with
a contracted unchecked-reason menu and a mandatory nullable basis; one
1 MiB canonical-JSON per-row budget), carry_mode, and state_source.
Loading is preserve-and-disclose: rows of unknown NAMESPACED kinds load
opaque (OpaqueRelationRow — preserved verbatim on re-save, never
executed; tl.load(unknown_relations="refuse") restores strictness; bare
unknown kinds always refuse), unknown evidence schema ids load opaque
inside a valid envelope, and unknown namespaced top-level bundle.json
sections preserve through Bundle.preserved_sections while bare-unknown
sections refuse typed. Version-axis rows still ORDER members only — no
relation row ever licenses a cross-member parameter value read.