Planner retries parse failures but not structurally invalid typed plans #245
Labels
No labels
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
nsaspy/prolog-rlm#245
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Live regression evidence
Discovered while running the approved #183 live RLM operator behavior acceptance on PR #242.
At exact candidate
cefa11974cb94c65eb7e5eef5c6a887653e54350, pinned paid OpenRouter (openai/gpt-oss-120b) produced a parseable typed plan on the first planner model call, butplan_run/5rejected it as:plan_error{phase:validate,kind:invalid_plan,detail:final_must_be_unique_and_last,...}Despite
planner_attempts(2), usage showsmodel_calls:1; no second planner attempt occurs.Current architecture explains why:
planner_loopretries onlyplan_parse/2failures. Once parsing succeeds,completion_after_planner/12proceeds through recursive validation/budget shaping and thenplan_run/5; structural plan validation therefore happens after the retry loop.Required contract
TDD first from canonical
main267697bef10a3fffff7c093e1435ece770e7444b:final), then a valid plan on attempt 2.planner_attempts(2), completion must recover through the existing bounded planner retry budget and execute only the valid second plan.Design boundary
This is planner repair/retry orchestration, not validation weakening. Do not normalize away structural errors, auto-delete duplicate finals, execute a partially valid plan, or add provider-specific special cases. Reuse the existing planner attempt loop and canonical
rlm_planvalidation semantics.Cross-link: blocks behavioral proof in #183 / PR #242, where live-provider evidence exposed the gap.
RAGE analyze / design / adversarial gate
Analyze
Canonical
mainremains267697bef10a3fffff7c093e1435ece770e7444b. Source confirms the live failure mechanism:planner_loopretries onlyplan_parse/2errors. A parseable candidate exits asok(planner_result{...});completion_after_plannerthen performs recursive checks and ultimately calls canonicalplan_run/5, where structural validation such asfinal_must_be_unique_and_lastcan fail. That later failure has no path back into the bounded planner attempt loop.Design
Keep canonical plan validation unchanged. Introduce a pre-execution candidate-validation step inside the planner-attempt lifecycle that uses the same structural validation contract as
rlm_plan, without executing the candidate. A structurally invalid candidate consumes its planner usage, is rejected before any plan operation runs, and may trigger the next configured planner attempt. After attempts are exhausted, return an explicit structured planner validation error carrying aggregate attempt usage and the last validation cause.The smallest implementation should reuse existing validation/public API if available rather than duplicate the validator. If no non-executing validation entrypoint exists, add the smallest reusable one in
rlm_planand keepplan_run/5validating again at execution as defense in depth.TDD convention
First add a deterministic classifier that proves current
mainsees a parseable structurally-invalid first candidate, returns the structural error, and calls the injected planner only once even withplanner_attempts(2). This expected-failure regression remains green. Then invert it with the implementation to assert recovery on attempt 2 plus exhaustion semantics on two invalid candidates.Adversarial review
Decision: GO for a focused #245 branch from exact current
main. #242 remains HOLD until #245 lands and its live behavior candidate is rebased/reverified.TDD baseline verified — exact head
37637cab5b2be4f04cf4458744e9037fe915d9a7The deterministic regression classifier is now correctly registered in the canonical corpus and passes without intentionally redlining CI.
It proves the current defect from exact canonical base
267697bef10a3fffff7c093e1435ece770e7444b:planner_attempts(2)is configured;finalsteps;phase:validate / kind:invalid_plan / detail:final_must_be_unique_and_last;The earlier classifier run exposed a test bug (
assertion/1binding was not preserved); that was fixed rather than treated as runtime evidence. Current PlUnit has passed this classifier on37637cab….This satisfies the BUG/TDD gate. Realization remains the approved narrow design: bring existing non-executing
rlm_plan:plan_validate/4into the bounded planner-attempt lifecycle, retainplan_run/5validation as defense in depth, retry only pre-execution invalid candidates, aggregate planner usage across attempts, and never execute/auto-repair an invalid plan.Adversarial correction — do not retry capability/budget rejection as planner repair
Fresh source audit against exact
main267697bef10a3fffff7c093e1435ece770e7444bfound an important boundary in the realization design.rlm_plan:plan_validate/4is non-executing, but it is not structural-only. It currently performs, in one call:require_capability/2throughoutvalidate_plan_structure/...);final, bindings, expressions, retry/parallel shape;Therefore wiring full
plan_validate/4directly intoplanner_parse_result(ok(Plan),...)and retrying everyerror(...)would broadenplanner_attempts/1into retries for capability denial and plan-budget rejection. That conflicts with the existing adversarial contract: planner repair is for malformed/structurally invalid candidates, not authority/capability policy or budget failures.Revised realization boundary
GO remains, but HOLD the naive
plan_validate/4 -> retry on any errorimplementation.Use/reuse the smallest canonical structural preflight that validates the closed typed plan before execution without consuming authority semantics. If current public API cannot express that boundary, add the smallest reusable
rlm_planentrypoint/refactor so:The regression matrix should include at least one explicit guard proving a parseable candidate denied by capability is not converted into a planner-repair retry, plus the existing invalid-candidate zero-execution proof.
This preserves the original security invariant and avoids accidentally teaching the model that host policy denial is something it may retry around.
RAGE realization update on exact branch head
d65a233cf098b8951db9cc6cfd9c102ac113900c(base remains canonicalmain267697bef10a3fffff7c093e1435ece770e7444b).The adversarial boundary is now realized without adding a second validator:
rlm_plan:plan_validate/4before leaving the planner attempt loop;phase:validate / kind:invalid_planis classified as a repairable planner candidate and consumes another configured planner attempt;capability_deniedandbudget_exceededare explicitly not repair signals: the candidate exits the planner loop unchanged and the existing authoritative downstream validation rejects/allows it exactly as before;planner_loop, so retries consume the same global model-call/token/cost budget;plan_run/5still performs full validation immediately before execution, preserving defense in depth.The deterministic acceptance contract now proves:
phase:planner / kind:plan_validation_failedwith aggregate planner usage;The branch was advanced as one atomic two-file commit after inspecting the synthetic commit diff. Fresh exact-head repository-native gates are queued; no prior-head green result is being reused.
RAGE realization/adversarial reconciliation — current implementation shape is acceptable, exact-head gate still HOLD
Re-read against live PR #246 head
39d8e1ff030e011e8e0c37619c0c2ba053d83c47(implementation tree fromd65a233cf098b8951db9cc6cfd9c102ac113900c) and unchanged canonicalmain267697bef10a3fffff7c093e1435ece770e7444b.The current realization does call canonical
plan_validate/4, but it does not implement the rejected naiveplan_validate -> retry on any errordesign. It classifies the canonical structured validation outcome:phase:validate, kind:invalid_plan=> planner-repair candidate;kind:capability_denied=> deferred to the existing authoritative validation/execution path, no planner retry;kind:budget_exceeded=> likewise deferred, no planner retry;That preserves the adversarial boundary without introducing a second structural validator or a new public
rlm_planAPI. The deterministic matrix on the branch also explicitly asserts that a parseable capability-denied plan gets exactly one planner call and executes zero model operations.The realization tests now cover recovery on attempt 2, two-invalid-attempt exhaustion with aggregate usage, zero execution of invalid candidates, and capability denial not becoming a repair signal. This is the smallest coherent reuse of the canonical validator unless exact-head evidence exposes another validation class that needs separation.
Decision: implementation design GO; exact-head verification HOLD. Current head is not promotable yet: the fresh GitHub Actions runs attached to
39d8e1ff...are in a bad startup/queue state (CI and Clean SWI pack reportstartup_failure; Paid OpenRouter is failed/queued inconsistently; Nix and Tree-sitter are queued). The previous37637cab...green evidence is TDD-baseline evidence only and cannot verify the realization.Do not weaken or bypass those gates. Once GitHub executes the current exact-head workflows normally, use that evidence; if a real test/provider failure appears, preserve it and repair the root cause.
RAGE reconciliation after canonical-main merge burst
Canonical
mainhas advanced from the original #245 baseline267697bef10a3fffff7c093e1435ece770e7444bto exact head340dd0aec4d2e1b555aac7b108beed9c29706a65after merging tool-result projection, provider tool-choice normalization, numeric schema bounds, subagent deadline policy, bounded parent resume, and proof-carrying child-result acceptance.Fresh source inspection on that exact head shows the #245 defect still exists:
planner_call_result/12callsplan_parse/2, andplanner_parse_result(ok(Plan), ...)immediately returnsok(planner_result{...}). Structural validation is still later in completion/plan execution, outside the configured planner retry loop. So #245 is not obsolete and still blocks #183 / PR #242.PR #246 is now a recovery transaction rather than a merge-ready candidate. Its head
39d8e1ff030e011e8e0c37619c0c2ba053d83c47is 7 commits ahead / 15 behind currentmain, GitHub reports it non-mergeable, and its merge base remains the old267697be...head. The branch still owns exactly the intended three-file slice:prolog/rlm_completion.pl, deterministic-corpus registration, and the focused planner-validation retry test.Adversarial comparison matters here: the 15 new canonical commits do not modify
prolog/rlm_completion.pl; they do modifytest/deterministic_corpus.plby registering the newly merged deadline/result-acceptance suites. Therefore recovery must preserve both sides of that corpus inventory rather than choosing one and silently dropping tests.Decision
GO to recover the existing #246 transaction onto exact
340dd0ae...; HOLD promotion/merge until fresh changed-head evidence.Recovery requirements:
plan_validate/4/plan_run/5;No old exact-head CI result is reusable after this recovery.