All docs

Docs · Deploy & operate

Fuel and TTL

loop and cost budgets

Operator reference for the per-request budget guards: how the chassis stops runaway loops and expensive work, and how to read the errors when it does.

Every request carries two budgets, working as a pair:

GuardEnvelope fieldQuestion it answersExhaustion error
TTL_txc.ttl“Is this a loop?”txcl_scope_ttl_exhausted
Fuel_txc.fuel_used“Is this expensive work?”txco_fuel_exhausted

The two codes are deliberately distinct so an operator can tell a loop from costly-but-legitimate work at a glance. Implementation: chassis/processor/budget.go.

TTL — the hop counter

_txc.ttl is a countdown, decremented once per stage entry (every scope advance, @goto, or stage-jump EXEC). It starts at --op-scope-ttl-max (default 500); 0 disables the guard.

A rule may voluntarily lower its remaining budget — EMIT @ttl = 20 before entering a polling loop — but can never raise it (the IP-TTL idiom: writes are clamped to the current value). Use this to give a known-risky subflow a tight sub-budget without touching the chassis-wide cap.

Fuel — the work meter

_txc.fuel_used counts up against --max-fuel-per-request (default 100000; 0 = unlimited). Costs are weighted per action:

ActionFuel
Entering a scope10
EXEC dispatch25
Nano-op compute, per ms10
Secret materialization100
Repeated stage transition50

Calibration: 1 fuel ≈ 100 µs of typical chassis work. So a 1 ms nano-op costs 35 total (25 dispatch + 10 compute); an op wrapping a 1-second LLM call costs ~10,000 — meaning the default cap tolerates roughly ten such calls per request before cutting off.

The final fuel value is logged on the per-request usage line (fuel=N) — single-tenant deployments can ignore it; tenant-aware deployments aggregate it for quota or billing.

Repeat transitions: backpressure before the kill

The chassis keeps a per-request seen-set of stage transitions ("from->to", carried as _txc._seen). The first time a transition happens it’s free; every repeat charges 50 fuel and sleeps --op-repeat-penalty-ms (default 20, 0 disables) before proceeding. A tight loop therefore degrades gracefully — it slows down, burns fuel measurably, and shows up in traces — rather than spinning the CPU until the hard cap lands.

Envelope mechanics

All three fields (_txc.fuel_used, _txc.ttl, _txc._seen) ride the envelope, so budgets propagate through @goto, EXEC stage jumps, continuations, and deferred-join fan-out the same way _txc.tenant does — a flow can’t shed its budget by jumping stacks. They are stripped from the response before it reaches the inlet client (after the fuel value is captured for usage accounting).

Reading an exhaustion

Both errors return a structured JSON payload as the request’s final response, including the stage where the request gave up and the last three transitions — usually enough to spot the cycle without opening a trace:

{
  "code": "txco_fuel_exhausted",
  "max_fuel": 100000,
  "fuel_used": 100025,
  "last_stage": "billing/0",
  "last_transitions": [
    "retry/0->billing/0",
    "billing/0->retry/0",
    "retry/0->billing/0"
  ]
}
{
  "code": "txcl_scope_ttl_exhausted",
  "max_ttl": 500,
  "consumed": 500,
  "last_stage": "poll/0",
  "last_transitions": [
    "poll/0->poll/0",
    "poll/0->poll/0",
    "poll/0->poll/0"
  ]
}

For the full step-by-step picture, pull the trace for that rid.

Apply-time lint: catching typos before runtime

The runtime guards always catch loops eventually; txco apply also lints the assembled stack for the unambiguous mistakes (chassis/cli/loop_lint.go):

  • Unconditional self-loop — a rule with no WHEN and no EMIT @halt that @gotos or EXECs back into its own (stack, scope). Usually a typo: @goto = "self/0" meant as "self/1".
  • Unconditional 2-stack ping-pong — stage A unconditionally points to B, and B unconditionally back to A.

Warnings only (printed to stderr; apply proceeds). The lint is deliberately conservative: conditional state-machine loops and intentional polling pass unflagged — those are the runtime guards’ job.

Flags

FlagDefaultMeaning
--max-fuel-per-request100000Fuel cap; 0 = unlimited
--op-scope-ttl-max500Starting hop budget; 0 = disabled
--op-repeat-penalty-ms20Sleep per repeated transition; 0 = off

Edit this page · View as markdown