feat(voyage): document recovery/retry iteration caps in trekexecute

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-06-20 21:31:07 +02:00
commit e34082d79a
2 changed files with 64 additions and 0 deletions

View file

@ -1298,6 +1298,38 @@ Status: partial (recovery_depth=2, escalated to user)
Do NOT dispatch a third recovery. Report to the user.
### Iteration caps + global recovery/retry budget
Every recovery/retry loop in this executor has a documented numeric bound.
These are **three distinct axes** — not one nested counter — plus a separate
child turn cap:
- **Per-step retry cap**`attempts ≤ 3` (initial + **maximum 2 retries**;
Hard Rule 5 / Sub-step E). Bounds the retries of a *single* step.
- **Per-session recovery cap**`recovery_depth < 2` (Phase 7.6 hard cap).
Bounds how deep recovery *dispatch* nests.
- **Global recovery/retry budget** — a new env-overridable constant
`TREKEXECUTE_MAX_RECOVERY_ITERATIONS` (default **25**) — the aggregate ceiling
on the **total count of recovery + retry iterations across the whole
execution**. This is the case the two caps above do NOT bound: many steps each
retrying up to 3× can sum unbounded. It primarily backstops the **retry
aggregate** (recovery dispatch is already hard-capped at 2). Surfaced and
enforced via the `iterations_remaining` counter (Phase 3 schema; decremented
on every recovery/retry iteration; reconciled at the Phase 7.5 completion
gate).
- **Child turn cap**`--max-turns` (`TREKEXECUTE_MAX_TURNS`, default 50)
bounds model turns inside a single `-p` child; a *different* axis from the
three above.
Default **25** is an independent value from the cap research (practitioner range
1050; OWASP Agentic ASI08 financial-DoS), tuned down from the research ~50
starting point for a tighter backstop. It is **not** a reconciliation with
`--max-turns` (50) — the axes are incomparable. Operators may override:
`TREKEXECUTE_MAX_RECOVERY_ITERATIONS=10 /trekexecute --project ...`. The
`effort=='low'` single-foreground-loop path and the wave path are both subject to
this aggregate ceiling; it is an execution-spec ceiling (enforced via the
`iterations_remaining` counter), not a code-loop edit.
## Phase 8 — Final report
Always produce a final report.
@ -1698,3 +1730,10 @@ code-path.
cleanup proceeds regardless. Rationale: this converts an unrecoverable
failure (worktree removed, branch deleted, work lost) into a recoverable
one (push succeeded, branch preserved on remote). Source: research/02 R3.
20. **Three distinct iteration axes.** Per-step `attempts` (≤3), per-session
`recovery_depth` (<2), and the global `TREKEXECUTE_MAX_RECOVERY_ITERATIONS`
budget (default 25 — aggregate recovery+retry backstop) are independent
bounds, not one nested counter; child `--max-turns` (50) is a fourth,
separate axis. Every recovery/retry loop is bounded by at least one of them.
See "Iteration caps + global recovery/retry budget".

View file

@ -93,3 +93,28 @@ test('trekexecute — completion gate anchors result:completed to Phase 7.5 audi
assert.match(section, /DONE/, 'gate must reference the DONE stop-signal token');
assert.match(section, /emitted AFTER/, 'gate must require DONE emitted AFTER the audit ran');
});
// --- S38 loop-discipline hardening: caps + global recovery budget (Step 3) ---
test('trekexecute — global recovery/retry budget TREKEXECUTE_MAX_RECOVERY_ITERATIONS default 25 (SC b)', () => {
const text = read();
assert.ok(text.includes('TREKEXECUTE_MAX_RECOVERY_ITERATIONS'),
'global recovery/retry budget constant must be documented');
assert.match(
text,
/TREKEXECUTE_MAX_RECOVERY_ITERATIONS[^\n]{0,40}\b25\b|default[^\n]{0,20}\b25\b[^\n]{0,40}aggregate/i,
'global budget must document numeric default 25 adjacent to the constant',
);
});
test('trekexecute — every recovery/retry loop bounded + 3-axis cap hierarchy (SC b)', () => {
const text = read();
assert.match(text, /maximum 2 retries|Retry cap = 3 attempts/, 'per-step retry cap must be explicit');
assert.match(text, /recovery_depth < 2/, 'recovery-dispatch cap must be explicit');
const capIdx = text.indexOf('Iteration caps');
assert.ok(capIdx >= 0, 'cap-hierarchy section ("Iteration caps") must exist');
const section = text.slice(capIdx, capIdx + 1500);
assert.match(section, /attempts/, 'hierarchy must name per-step attempts axis');
assert.match(section, /recovery_depth/, 'hierarchy must name per-session recovery_depth axis');
assert.match(section, /TREKEXECUTE_MAX_RECOVERY_ITERATIONS/, 'hierarchy must name the global budget axis');
});