@@ -38,28 +38,29 @@ prefixes (the enum is intentionally minimal: `Diagnostics` / `Runtime`).
3838
3939## Tier A — stop the crashes (highest probability for agent code)
4040
41- - [ ] ** A1 Recursion depth limit** (Inv 1). ` self.frames.push() ` is unbounded
41+ - [x ] ** A1 Recursion depth limit** (Inv 1) — DONE ( ` VmLimits.max_depth ` , default 16 384, enforced at all frame-push sites → ` EvalError::Runtime("recursion depth limit exceeded …") ` ). ` self.frames.push() ` was unbounded
4242 (` reg_vm/mod.rs:7523 ` "bounded only by memory"; pushes at 7538/7974/8019) → a
4343 self-recursive program overflows the native stack = uncatchable SIGSEGV. Add a
4444 configurable ` max_depth ` (default-on, generous) checked at every frame push →
4545 ` EvalError::Runtime("recursion depth limit exceeded …") ` . The #1 crash; trivial.
46- - [ ] ** A2 ` panic = "abort" ` ** (Inv 2). Add to workspace ` [profile.release] ` . One
47- line. ` hostile.rs ` uses ` catch_unwind ` but runs in the dev profile (unwind), so
48- normal ` cargo test ` is unaffected; verify fuzz targets still build .
46+ - [x ] ** A2 ` panic = "abort" ` ** (Inv 2) — DONE ( workspace ` [profile.release] ` ).
47+ ` hostile.rs ` 's ` catch_unwind ` runs in the dev profile (unwind); the fuzz crate
48+ is workspace-detached, so ` cargo +nightly fuzz build ` is unaffected. Verified .
4949
5050## Tier B — stop the hangs
5151
52- - [ ] ** B3 Step budget** (Inv 1). The interpreter loop (` reg_vm/mod.rs:~7927 ` ,
53- ` while let Some(instr) = func.code.get(ip) ` ) has no fuel; ` while true {} ` hangs
54- forever. Add a per-instruction counter + configurable ` step_budget: Option<u64> `
55- → ` EvalError::Runtime("step budget exceeded …") ` . Follow-up (not required this
56- pass): also poll the existing ` CancellationToken ` every N steps so cooperative
57- cancellation can preempt a tight compute loop — the preemption hook structured
58- cancellation is currently missing (it only works at await points).
59- - [ ] ** B4 Memory ceiling** (Inv 1). Configurable ` mem_budget: Option<usize> ` ;
60- account live allocation through ` ensure_regs ` and list/map growth →
61- ` EvalError::Runtime("memory limit exceeded …") ` . Harder (≈169 alloc/growth
62- sites); lands after A/B3. Best-effort byte accounting is fine.
52+ - [x] ** B3 Step budget** (Inv 1) — DONE (` VmLimits.step_budget: Option<u64> ` ,
53+ default OFF; per-instruction ` tick() ` in BOTH the interpreter loop and the
54+ tier-0 JIT loop → ` EvalError::Runtime("step budget exceeded …") ` ).
55+ - [ ] * Follow-up (not done):* poll the existing ` CancellationToken ` every N
56+ steps so cooperative cancellation can preempt a tight compute loop — the
57+ preemption hook structured cancellation currently lacks (works only at await
58+ points). The ` tick() ` counter is the natural place to add it.
59+ - [x] ** B4 Memory ceiling** (Inv 1) — DONE (` VmLimits.mem_budget: Option<usize> ` ,
60+ default OFF; best-effort cumulative ` live_bytes ` accounting at ` ensure_regs ` ,
61+ list/map construction, and list push/append → `EvalError::Runtime("memory limit
62+ exceeded …")` ). Under-counts deeply nested ` Rc` containers — documented; goal is
63+ to trip before host OOM, not exact accounting.
6364
6465## Tier C — longevity (lower urgency)
6566
0 commit comments