Repository navigation
fix(OMN-20390): handle null terminal_payload in failed delegate receipt - #3271
Conversation
The delegated edit loop (omnimarket) crashed with AttributeError at loop_ports.py:435 when a failed delegate's receipt had terminal_payload null, leaving loop_claim on disk and no loop receipt. The delegate receipt is now read null-safely; the delegate port returns a typed failed reply carrying the receipt's failure cause; the loop receipt is written atomically and the claim released on every exit path; unexpected turn-phase exceptions end the loop as infra_error with a receipt. Tests: 99 pass on the branch, 42 of them fail on the dev source.
✅ Architectural Review — PASSEDErrors: 0 What this checks
No architectural violations found. Static architectural lint — no model inference (OMN-14176). |
✅ Hostile Reviewer — PASSEDBlocking findings (critical/error): 0 Gate semantics
Powered by omniintelligence.review_pairing.cli_review — node-based adversarial review via HandlerLlmCliSubprocess (OMN-8524) |
|
Lab: host=h201 (hostname omninode-pc); lane=codeedit-loop-fix-2a21; head=9dff5631a5ea38ac772a55f360a34cb274811fcb; command: uv run onex code-edit run --worktree --task-file --writable 'src/*.py' --check 'tests= -m pytest -q tests/test_m.py' --omnibase-path $OMNI_HOME --delegate-in-process --max-turns 6 (real onex delegate, in-process); observed on dev source (dadcc91): loop ada0a179-449d-4e0e-9d20-996f4833f540 raised AttributeError at loop_ports.py:435, loop_claim left on disk, no loop_receipt.json; observed on this head: loop d584b9f2-8865-4f4d-8f10-89e9ac54b81c ended delegate_failed after 2 turns with loop_receipt.json written and no loop_claim; the receipt named the real cause (terminal_payload is null; DelegatePhaseTimedBus.subscribe() takes 2 positional arguments but 4 positional arguments (and 1 keyword-only argument) were given). |
Summary
Fix a crash in the delegated code edit loop when a delegate fails (OMN-20390).
DelegatedCodeEditPorts.delegate(src/omnimarket/delegated_code_edit/loop_ports.py) read thedelegate receipt as
receipt["receipt"]["result"]["terminal_payload"]["payload"]. A faileddelegate's receipt carries
"terminal_payload": null(key present, valueNone); becausedict.get(key, {})returnsNonefor a present null key, the next.getraisedAttributeErroratloop_ports.py:435. The exception escapedHandlerDelegatedCodeEditOrchestrator.run, so no loop receipt was written and<state root>/runs/<correlation id>/loop_claimstayed on disk. First seen on a failed run on.202(rund642a86f-8780-45cd-8c4f-412fa938dc68).dev· Head:codeedit-loop-fix-2a21/OMN-20390-null-terminalChanges
loop_ports.py,_read_delegate_receipt). Any level(
receipt,result,terminal_payload,payload,metrics) may be null or not a dict;receipt.jsonmay be unreadable or not JSON; non-numeric token counts become0. It alsobuilds a one-line failure description from the receipt (
status,terminal_failure_cause,terminal_failure_reason,failure_reason,result.error,result.runtime_error_type,and
terminal_payload is null).delegate.DelegatedCodeEditPorts.delegatereturns a typedfailed
ModelTurnReply(ok=False, run id, model and tokens from the receipt) wheneverthere is no result text, with the receipt failure description appended to
invalid_reason.claim_loop_receiptrefuses when a loop receipt already exists(checked before and after the exclusive create).
write_loop_receiptwrites atomically(temp file then
os.replace) and always releasesloop_claimin afinally. A receipt'sexistence is what refuses a rerun.
HandlerDelegatedCodeEditOrchestrator.runcatchesany unexpected exception from the turn phase as status
infra_errorwith detailunexpected <Type>: <message>, and guards the diff and score ports, so a loop receipt iswritten on every exit path that raises an exception.
Proof (RED/GREEN)
Test paths:
tests/unit/delegated_code_edit/test_loop_ports.pytests/nodes/node_delegated_code_edit_orchestrator/test_handler_delegated_code_edit_orchestrator.pyRED — the same tests against the dev source (
origin/devatdadcc91b7):42 failed, 57 passed. The headline test fails there with
AttributeError: 'NoneType' object has no attribute 'get'atloop_ports.py:435.GREEN — on this branch, a focused run of those two paths: 99 tests pass.
Lab runs
Real
onex code-edit run, in-process delegate, on host.201, task: makeaddreturn thesum, max 6 turns.
ada0a179-449d-4e0e-9d20-996f4833f540raisedAttributeErroratloop_ports.py:435;loop_claimleft on disk; noloop_receipt.json.d584b9f2-8865-4f4d-8f10-89e9ac54b81cended statusdelegate_failedafter 2 turns,loop_receipt.jsonwritten, noloop_claim; delegate runs44c4aeb3-e3ee-4544-b269-14dc76087db9andbfbf2ec9-0bd7-4e3e-8c82-9212bb2f9dbe. The realreceipt had
terminal_payloadnull and failure_reason"DelegatePhaseTimedBus.subscribe() takes 2 positional arguments but 4 positional arguments (and 1 keyword-only argument) were given";the loop receipt now names it.
0040ad07-4dfc-423f-954e-a992bee7801aendeddelegate_failedwith a receipt and no claim.Notes
Resuming a
delegate_failedloop and keeping the full error in the receipt are trackedseparately under OMN-20391 and are not in this change.
Evidence-Ticket: OMN-20390
Evidence-Source: OCC#12445