Skip to content

fix(OMN-20390): handle null terminal_payload in failed delegate receipt - #3271

Merged
jonahgabriel merged 2 commits into
devfrom
codeedit-loop-fix-2a21/OMN-20390-null-terminal
Oct 3, 2026
Merged

jonahgabriel merged 2 commits into
devfrom
codeedit-loop-fix-2a21/OMN-20390-null-terminal

Conversation

@jonahgabriel

@jonahgabriel jonahgabriel commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

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 the
delegate receipt as receipt["receipt"]["result"]["terminal_payload"]["payload"]. A failed
delegate's receipt carries "terminal_payload": null (key present, value None); because
dict.get(key, {}) returns None for a present null key, the next .get raised
AttributeError at loop_ports.py:435. The exception escaped
HandlerDelegatedCodeEditOrchestrator.run, so no loop receipt was written and
<state root>/runs/<correlation id>/loop_claim stayed on disk. First seen on a failed run on
.202 (run d642a86f-8780-45cd-8c4f-412fa938dc68).

  • Base: dev · Head: codeedit-loop-fix-2a21/OMN-20390-null-terminal

Changes

  1. Null-safe receipt reader (loop_ports.py, _read_delegate_receipt). Any level
    (receipt, result, terminal_payload, payload, metrics) may be null or not a dict;
    receipt.json may be unreadable or not JSON; non-numeric token counts become 0. It also
    builds 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).
  2. Typed failed reply from delegate. DelegatedCodeEditPorts.delegate returns a typed
    failed ModelTurnReply (ok=False, run id, model and tokens from the receipt) whenever
    there is no result text, with the receipt failure description appended to invalid_reason.
  3. Claim/receipt hygiene. claim_loop_receipt refuses when a loop receipt already exists
    (checked before and after the exclusive create). write_loop_receipt writes atomically
    (temp file then os.replace) and always releases loop_claim in a finally. A receipt's
    existence is what refuses a rerun.
  4. Receipt on every raising exit path. HandlerDelegatedCodeEditOrchestrator.run catches
    any unexpected exception from the turn phase as status infra_error with detail
    unexpected <Type>: <message>, and guards the diff and score ports, so a loop receipt is
    written on every exit path that raises an exception.

Proof (RED/GREEN)

Test paths:

  • tests/unit/delegated_code_edit/test_loop_ports.py

  • tests/nodes/node_delegated_code_edit_orchestrator/test_handler_delegated_code_edit_orchestrator.py

  • RED — the same tests against the dev source (origin/dev at dadcc91b7):
    42 failed, 57 passed. The headline test fails there with
    AttributeError: 'NoneType' object has no attribute 'get' at loop_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: make add return the
sum, max 6 turns.

  • dev source: loop run ada0a179-449d-4e0e-9d20-996f4833f540 raised AttributeError at
    loop_ports.py:435; loop_claim left on disk; no loop_receipt.json.
  • this branch: loop run d584b9f2-8865-4f4d-8f10-89e9ac54b81c ended status
    delegate_failed after 2 turns, loop_receipt.json written, no loop_claim; delegate runs
    44c4aeb3-e3ee-4544-b269-14dc76087db9 and bfbf2ec9-0bd7-4e3e-8c82-9212bb2f9dbe. The real
    receipt had terminal_payload null 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.
  • dev lane (deployed locus): branch run 0040ad07-4dfc-423f-954e-a992bee7801a ended
    delegate_failed with a receipt and no claim.

Notes

Resuming a delegate_failed loop and keeping the full error in the receipt are tracked
separately under OMN-20391 and are not in this change.

Evidence-Ticket: OMN-20390
Evidence-Source: OCC#12445

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.
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown

✅ Architectural Review — PASSED

Errors: 0
Warnings: 0

What this checks

Rule Description
ARCH-TOPIC-001 No hardcoded Kafka topic strings in handler code
ARCH-DI-001 No event_bus=None bypass
ARCH-DI-002 No direct Handler instantiation outside workflow_runner/adapters
ARCH-DI-003 No reinvented DI containers
ARCH-TOPIC-002 contract.yaml topics follow `onex.{cmd

No architectural violations found.

Static architectural lint — no model inference (OMN-14176).

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

✅ Hostile Reviewer — PASSED

Blocking findings (critical/error): 0
Total findings: 13
Models succeeded: qwen3-review,gpt-oss-review


Gate semantics

Verdict Meaning Blocks merge?
passed >=2 models succeeded, no critical/error findings No
blocked CRITICAL or ERROR findings found Yes
degraded Fewer than 2 models succeeded (infra unavailable/timeout) Yes (OMN-15110)

Powered by omniintelligence.review_pairing.cli_review — node-based adversarial review via HandlerLlmCliSubprocess (OMN-8524)

@jonahgabriel

Copy link
Copy Markdown
Contributor Author

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant