fix: add native-call batch cardinality admission (#323) #367

Closed
nsaspy wants to merge 2 commits from fix/323-native-batch-cardinality into main
Owner

Stacked on #331 (issue #328); base flips to main when that lands.

Runtime invariant

max_native_calls_per_batch (trusted completion_budget field, bounded default 8, positive-integer validated, host-configurable) is the FIRST batch-fatal invariant of classify_calls/4. It is evaluated against the ORIGINAL provider-requested batch before any per-call classification, preflight, or execution work:

  • every requested call counts, including malformed/recoverably-invalid calls (length/2 over the original call list, not the resolved subset);
  • an oversize batch fails closed with one deterministic structured fault direct_error{phase:native_call,kind:native_batch_too_large,requested,limit} — no valid sibling executes, no malformed sibling is converted into a repair observation first, and no tool/context execution charge occurs;
  • max_tool_calls / max_context_ops accounting is untouched: they keep meaning EXECUTED operations (resolved_batch_budget/3 unchanged); accepted batches keep their existing accounting;
  • nested typed-plan sessions inherit the parent admission limit in direct_model_step_budget/3 and never widen it;
  • malformed/non-list tool_calls envelopes retain the existing fail-closed normalize semantics (unsupported_tool_call_format), and normalize-level duplicate-ID rejection remains the pre-execution fatal envelope invariant that precedes classification-level admission.

Regression matrix (test/rlm_direct_test.pl, scripted provider)

exactly-at-limit valid batch succeeds and executes · one-over-limit valid batch fails before execution · exactly-at-limit all-fault batch keeps normal recoverable observations · one-over-limit all-fault batch fails immediately with no tool messages · mixed valid/fault batch over limit fails before classification in both call orders · effectful calls in an oversized batch produce zero mutations (durable effect store) · oversized duplicate-ID batch still fails closed pre-execution · configured low limit is honored across smaller accepted batches · default 8 pinned and non-positive integers rejected (completion_fault(invalid_positive_integer/2)) · non-list envelope fail-closed · accepted-batch accounting unchanged.

Non-goals

  • No change to max_tool_calls / max_context_ops semantics or to resolved_batch_budget/3.
  • No new scheduler/parallel-call contract; the loop stays sequential.
  • Admission does not move above classify_calls/4: normalize-level envelope fatals (duplicate IDs, malformed envelopes) keep their existing precedence.

Verification (exact head 20c94a5)

  • test/run_tests.pl: 1154/1154 (12 new #323 tests), test/check_runtime.pl, test/load_all.pl: pass
  • benchmark/run.pl -- deterministic + -- deep-experiment: pass
  • scripts/validate_research_approval.pl, scripts/design_gate.pl: pass
  • clean copied-pack install + load: pass; bin/prolog-rlm.pl -- demo --json: pass; git diff --check: clean
  • REAL OpenRouter (credentialed): test/run_live_openrouter.pl (incl. direct_native_tool loops) and test/run_live_repair_openrouter.pl pass — real-provider evidence, not fake providers
  • docs/direct-runtime.md updated: execution state machine + budget admission invariant

Follow-ups

  • #288/#219 retrieval and SPEC/PLAN epics remain out of scope for this stabilization slice.
Stacked on #331 (issue #328); base flips to main when that lands. ## Runtime invariant `max_native_calls_per_batch` (trusted `completion_budget` field, bounded default 8, positive-integer validated, host-configurable) is the FIRST batch-fatal invariant of `classify_calls/4`. It is evaluated against the ORIGINAL provider-requested batch before any per-call classification, preflight, or execution work: - every requested call counts, including malformed/recoverably-invalid calls (`length/2` over the original call list, not the resolved subset); - an oversize batch fails closed with one deterministic structured fault `direct_error{phase:native_call,kind:native_batch_too_large,requested,limit}` — no valid sibling executes, no malformed sibling is converted into a repair observation first, and no tool/context execution charge occurs; - `max_tool_calls` / `max_context_ops` accounting is untouched: they keep meaning EXECUTED operations (`resolved_batch_budget/3` unchanged); accepted batches keep their existing accounting; - nested typed-plan sessions inherit the parent admission limit in `direct_model_step_budget/3` and never widen it; - malformed/non-list `tool_calls` envelopes retain the existing fail-closed normalize semantics (`unsupported_tool_call_format`), and normalize-level duplicate-ID rejection remains the pre-execution fatal envelope invariant that precedes classification-level admission. ## Regression matrix (test/rlm_direct_test.pl, scripted provider) exactly-at-limit valid batch succeeds and executes · one-over-limit valid batch fails before execution · exactly-at-limit all-fault batch keeps normal recoverable observations · one-over-limit all-fault batch fails immediately with no tool messages · mixed valid/fault batch over limit fails before classification in both call orders · effectful calls in an oversized batch produce zero mutations (durable effect store) · oversized duplicate-ID batch still fails closed pre-execution · configured low limit is honored across smaller accepted batches · default 8 pinned and non-positive integers rejected (`completion_fault(invalid_positive_integer/2)`) · non-list envelope fail-closed · accepted-batch accounting unchanged. ## Non-goals - No change to `max_tool_calls` / `max_context_ops` semantics or to `resolved_batch_budget/3`. - No new scheduler/parallel-call contract; the loop stays sequential. - Admission does not move above `classify_calls/4`: normalize-level envelope fatals (duplicate IDs, malformed envelopes) keep their existing precedence. ## Verification (exact head 20c94a5) - `test/run_tests.pl`: 1154/1154 (12 new #323 tests), `test/check_runtime.pl`, `test/load_all.pl`: pass - `benchmark/run.pl -- deterministic` + `-- deep-experiment`: pass - `scripts/validate_research_approval.pl`, `scripts/design_gate.pl`: pass - clean copied-pack install + load: pass; `bin/prolog-rlm.pl -- demo --json`: pass; `git diff --check`: clean - REAL OpenRouter (credentialed): `test/run_live_openrouter.pl` (incl. `direct_native_tool` loops) and `test/run_live_repair_openrouter.pl` pass — real-provider evidence, not fake providers - docs/direct-runtime.md updated: execution state machine + budget admission invariant ## Follow-ups - #288/#219 retrieval and SPEC/PLAN epics remain out of scope for this stabilization slice.
A provider response can request an arbitrarily large native-call batch;
the runtime normalizes and classifies every call before any batch-level
admission, so one-over-limit batches surface duplicate/effectful
classification or execute valid siblings instead of a single
deterministic cardinality fault. Pins the required matrix: exactly-at
and one-over limits for valid, all-fault, and mixed batches, admission
before effect isolation, no effects from oversize batches, order
independence, low configured limits, non-list envelope fail-closed
semantics, and unchanged executed-operation accounting for accepted
batches.
fix: add native-call batch cardinality admission (#323)
Some checks failed
Tree-sitter FFI / Direct SWI-Prolog Tree-sitter FFI (pull_request) Successful in 2m18s
CI / REAL OpenRouter integration (pull_request) Has been skipped
Nix flake / Flake package and clean runtime load (pull_request) Failing after 4s
Clean SWI pack install / Install and load copied pack (pull_request) Successful in 48s
Paid OpenRouter / Pinned paid OpenRouter integration (pull_request) Failing after 2m24s
CI / Deterministic unit and load checks (pull_request) Successful in 3m52s
31b9de89e7
Introduce the trusted max_native_calls_per_batch budget (bounded default
8, positive-integer validated, host-configurable) as the first
batch-fatal invariant of classify_calls: it counts the ORIGINAL
provider-requested batch, including recoverably-invalid calls, before
any per-call classification or preflight work. An oversize batch fails
closed with one native_batch_too_large fault; no sibling executes, no
malformed call becomes a repair observation, and no tool/context
execution charge occurs. max_tool_calls / max_context_ops keep meaning
executed operations, and nested typed-plan sessions inherit the limit
without widening it.
nsaspy closed this pull request 2026-09-04 22:56:33 +00:00
Some checks failed
Tree-sitter FFI / Direct SWI-Prolog Tree-sitter FFI (pull_request) Successful in 2m18s
CI / REAL OpenRouter integration (pull_request) Has been skipped
Nix flake / Flake package and clean runtime load (pull_request) Failing after 4s
Clean SWI pack install / Install and load copied pack (pull_request) Successful in 48s
Paid OpenRouter / Pinned paid OpenRouter integration (pull_request) Failing after 2m24s
CI / Deterministic unit and load checks (pull_request) Successful in 3m52s

Pull request closed

Sign in to join this conversation.
No description provided.