Skip to content

fix(OMN-20386): a deployed-lane reply timeout is a typed failure, not a usage error - #4509

Merged
jonahgabriel merged 4 commits into
devfrom
rlane/omn-20386-delegate-reply-timeout
Oct 3, 2026
Merged

jonahgabriel merged 4 commits into
devfrom
rlane/omn-20386-delegate-reply-timeout

Conversation

@jonahgabriel

@jonahgabriel jonahgabriel commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

OMN-20386: Delegate Reply Timeout Handling

Summary

Implements gate OMN-19432 AC-1 for ticket OMN-20386. Addresses the issue where a deployed-lane delegation whose reply never arrives previously exited with code 2, printed the Usage banner, and left the run folder with only workflow_result.json.

Cause

The function _delegate_receipt_evidence_error (the receipt validator in cli_delegate.py) caught only DelegatePrePublishFailureError. The resolver's DelegateTerminalUnresolvedError is a ValueError, which escaped run_receipt_mode. Consequently, delegate_command mapped ValueError to click.UsageError. The receipt writer was never reached. The brief's "error string at line 443" corresponds to the resolver sentence "delegate receipt carries no resolvable delegation terminal".

Change

  1. delegate_terminal_resolver.py gains DelegateReplyTimeoutError (subclass of DelegateTerminalUnresolvedError) plus is_reply_timeout and reply_timeout_error. _delegation_result raises it when workflow_result is timeout, a wire_correlation_id is recorded, and no terminal resolves. The message names "cause timeout", the run id, and the wire correlation id.
  2. The validator returns the unresolved-terminal message instead of raising, resulting in exit 1 and no banner.
  3. receipt_mode keeps workflow_result as timeout (not error) when a validator refuses a timed-out run that has a wire correlation id.
  4. The unterminalized receipt from PR 4502 records wire_correlation_id.

Verification

  • AC1: Exit is 1 (not 0 or 2), stderr has no "Usage:", and names "cause timeout" and the run id.
  • AC2: runs/<run_id>/receipt.json has terminal_class timeout, run_id, correlation_id, and wire_correlation_id.
  • Tests: New tests/unit/cli/test_delegate_reply_timeout_omn20386.py contains 3 tests, all failed before (exit 2, no timeout on stderr, only an in_flight receipt) and pass after. One assertion extended in tests/integration/cli/test_delegate_pre_publish_failure_omn19131.py.
  • Coverage: 524 tests selected by -k "delegate or receipt" under tests/unit/cli and tests/integration/cli pass.
  • Quality: ruff and mypy clean on the touched source. pre-commit passed on commit.
  • Methodology: The tests drive the real delegate_command and real run_receipt_mode; only the runtime is replaced by a stand-in that records result timeout and a wire_correlation_id.

Stack and lab

  • Stack: This branch carries PR 4502 (OMN-17427, commit e4f9e54daf, rebased onto dev from 1c387309ea) as its first commit, because the receipt writer lives there. The OMN-20386 commit is the second, 1853041cb3. When 4502 lands, rebase and the first commit drops out. PR 4503 (tenant stamp) is not touched.
  • Lab: The deployed-lane reproduction against the dev lane was NOT exercised; the stand-in runtime reproduces the receipt the live runtime records for that case.

Evidence-Ticket: OMN-20386
Evidence-Source: OCC#12561

@jonahgabriel
jonahgabriel enabled auto-merge October 3, 2026 09:18
@jonahgabriel

Copy link
Copy Markdown
Collaborator Author

Lab: host=h201 lane=next3-OMN-20386-2a21 head=1853041cb33ac3db7e9191ee300c1fd6d927f9fd command=uv run pytest tests/unit/cli tests/integration/cli -k "delegate or receipt" -n auto observed=524 passed; the 3 new tests in tests/unit/cli/test_delegate_reply_timeout_omn20386.py failed before the change (exit 2, no timeout on stderr, only an in_flight receipt) and pass after. The live dev-lane deployed-lane reproduction was not exercised; a stand-in runtime records the timeout receipt.

jonahgabriel pushed a commit to OmniNode-ai/onex_change_control that referenced this pull request Oct 3, 2026
…12561)

* evidence(OMN-20386): author OCC companion for OmniNode-ai/omnibase_infra#4509

OCC companion by node_pr_lifecycle_fix_effect (OMN-13317 F1 / OMN-13990 / OMN-14285). Product PR head 1853041cb33ac3db7e9191ee300c1fd6d927f9fd.

* evidence(OMN-20386): self-bind OCC#12561 + rebind contract_sha256

---------

Co-authored-by: omnimarket-bot <bot@omninode.ai>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hostile Reviewer — adversarial findings (OMN-17492)

Models succeeded: qwen3-review, gpt-oss-review
Models failed: none
New finding threads: 8
Deduped (already posted on this PR): 0
Below quorum (one model only, reported not threaded): 9
Nit-level findings suppressed: 0

The model is the FINDER, never the gate: merge is gated only by the
deterministic Hostile Review Thread Gate, which blocks while
hostile-reviewer threads are unresolved. Resolve each thread after
addressing (or rejecting, with a reply) its finding.

Below quorum: 9 finding(s) raised by one model only (OMN-18479)

These are reported and NOT dropped, but they get no thread and do not block: a single model's finding no other model reproduced is not evidence enough to stop a merge. Read them; act on them if they are right.

  • [MAJOR] src/omnibase_infra/cli/cli_delegate.py, src/omnibase_infra/cli/receipt_mode.py (qwen3-review) — DelegateCallerInterruptedError is a BaseException, not an Exception | DelegateCallerInterruptedError subclasses BaseException. The except clause in run_delegate catches it explicitly. However, any cod
  • [MAJOR] src/omnibase_infra/cli/receipt_mode.py (qwen3-review) — workflow_result comparison in receipt_mode uses .value on an enum that may not be TIMEOUT | In _run_receipt_mode, the new code checks workflow_result is EnumWorkflowResult.TIMEOUT and workflow_data.ge
  • [MAJOR] tests/unit/cli/test_delegate_incomplete_receipt_omn17427.py (qwen3-review) — No test for the SIGKILL path leaving an in-flight receipt | The test suite includes a test that observes the in-flight receipt mid-wait (test_a_receipt_is_on_disk_before_the_wait_so_a_sigkill_leaves_o
  • [MAJOR] src/omnibase_infra/cli/cli_delegate.py (gpt-oss-review) — Potential race condition between terminal writer and in‑flight receipt settlement | The function _settle_in_flight_receipt reads the provisional receipt file and, if it still contains the IN_FLIGHT cl
  • [MINOR] src/omnibase_infra/cli/cli_delegate.py (qwen3-review) — Signal handler restoration may restore a None handler to SIG_DFL | In _caller_interrupt_guard, the finally block restores signal handlers with signal.signal(signum, handler if handler is not None else
  • [MINOR] src/omnibase_infra/cli/delegate_terminal_resolver.py (qwen3-review) — is_reply_timeout checks wire_correlation_id truthiness, which may be too strict | is_reply_timeout returns True only when workflow_result is "timeout" and wire_correlation_id is truthy. If the runtime
  • [MINOR] src/omnibase_infra/cli/cli_delegate.py (qwen3-review) — exit_code variable in run_delegate may be unbound if an exception is raised | In run_delegate, exit_code = run_receipt_mode(...) is assigned inside the with block. If run_receipt_mode raises an except
  • [MINOR] src/omnibase_infra/cli/receipt_mode.py (qwen3-review) — run_id parameter added to run_receipt_mode changes its public API | run_receipt_mode is a public function (it is in all or at least imported by other modules). Adding a run_id parameter is backwar
  • [MINOR] src/omnibase_infra/cli/receipt_mode.py (gpt-oss-review) — run_receipt_mode signature change breaks external callers | run_receipt_mode now accepts an optional run_id parameter, but external code that imports the function without updating call sites will rece

Findings demoted from threads (anchor rejected)

  • [MAJOR] hostile-reviewer (qwen3-review)

    In-flight receipt settlement reads a file that may not exist | _settle_in_flight_receipt reads state_root / runs / run_id / receipt.json. If the in-flight receipt was never written (the OSError path in _file_in_flight_receipt swallows the failure and continues), the file does not exist. The read_text call raises FileNotFoundError, which is a subclass of OSError, so it is caught by the except (OSError, ValueError) clause and the function returns silently. This is correct behavior, but the function's docstrin

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks while hostile-reviewer threads are unresolved (OMN-17492).

  • [MAJOR] hostile-reviewer (qwen3-review)

    Race between in-flight receipt write and terminal receipt write | _file_in_flight_receipt writes receipt.json with terminal_class=in_flight. The terminal receipt writer (inside run_receipt_mode) also writes receipt.json. If the terminal writer completes and writes a terminal receipt, and then _settle_in_flight_receipt runs, it reads the terminal receipt, sees terminal_class is not in_flight, and returns without modifying it. This is correct. However, if the terminal writer is in the middle of writing receip

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks while hostile-reviewer threads are unresolved (OMN-17492).

  • [MAJOR] hostile-reviewer (gpt-oss-review)

    Unterminated receipt JSON lacks required workflow_result field | _write_incomplete_run_files creates receipt.json for incomplete runs but does not include the "workflow_result" key that receipt validation expects. Subsequent validation in _run_receipt_mode may raise a validation error, breaking downstream consumers. | Evidence: Function _write_incomplete_run_files (lines 770‑845) builds the receipt JSON without a "workflow_result" entry, while _run_receipt_mode (lines 1470‑1485) validates receipt.workflow_r

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks while hostile-reviewer threads are unresolved (OMN-17492).

  • [MAJOR] hostile-reviewer (gpt-oss-review)

    Missing error handling when in‑flight receipt cannot be created | _file_in_flight_receipt catches OSError, logs a message, and continues execution. Subsequent code assumes the provisional receipt exists (e.g., _settle_in_flight_receipt), leading to silent failures or missing receipt files. | Evidence: Exception handling in _file_in_flight_receipt (lines 1025‑1035) only echoes the error without aborting or flagging the condition. | Fix: Propagate the OSError (or raise a custom exception) so that the caller c

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks while hostile-reviewer threads are unresolved (OMN-17492).

  • [MINOR] hostile-reviewer (qwen3-review)

    In-flight receipt write adds I/O before every delegation wait | _file_in_flight_receipt writes three files (result.txt, receipt.json, run.json) before the terminal wait begins. This adds three file writes and directory creation to the critical path of every delegation. For a system that dispatches many delegations, this is a measurable overhead. The writes are atomic, so they are safe, but the cost is non-trivial. The docstring acknowledges that a failure to write is reported and does not stop the delegatio

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks while hostile-reviewer threads are unresolved (OMN-17492).

  • [MINOR] hostile-reviewer (qwen3-review)

    Test for signal handler restoration uses SIGTERM only | test_signal_handlers_are_restored_after_the_run checks that SIGTERM's handler is restored. The _caller_interrupt_guard also installs handlers for SIGHUP and SIGINT. The test does not verify that SIGHUP and SIGINT handlers are also restored. If a regression breaks the restoration for SIGHUP or SIGINT, this test would not catch it. | Evidence: def test_signal_handlers_are_restored_after_the_run(self, tmp_path, monkeypatch): ... before = signal.getsignal(

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks while hostile-reviewer threads are unresolved (OMN-17492).

  • [MINOR] hostile-reviewer (gpt-oss-review)

    Inefficient JSON serialization for large receipt writes | All receipt writes use json.dumps with sort_keys=True and default=str, which incurs unnecessary CPU overhead for large payloads and may affect latency in high‑throughput scenarios. | Evidence: Calls to json.dumps in _write_incomplete_run_files (lines 795‑822) and _write_unterminalized_run_files (lines 860‑879). | Fix: Remove sort_keys=True for production writes or replace with a faster serializer (or pre‑sorted dict) when ordering is not required.

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks while hostile-reviewer threads are unresolved (OMN-17492).

  • [MINOR] hostile-reviewer (gpt-oss-review)

    EnumDelegateIncompleteClass.IN_FLIGHT path lacks unit test verification of settlement logic | The new IN_FLIGHT enum value is exercised only for receipt creation; there is no test confirming that _settle_in_flight_receipt correctly transitions the receipt to FAILED when the terminal writer does not replace it. | Evidence: Test suite includes TestCallerTimeoutLeavesAReceipt but does not assert the final receipt after settlement for the IN_FLIGHT case. | Fix: Add a unit test that simulates a run where the ter

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks while hostile-reviewer threads are unresolved (OMN-17492).

@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

✅ Hostile Reviewer — REVIEWED

Critical findings: 1
Major findings: 5
Total findings: 15
Models succeeded: qwen3-review,gpt-oss-review
Models unavailable: none

Action required: findings were posted as review threads. Address or reject each one, then resolve its thread — the Hostile Review Thread Gate fails while hostile-reviewer threads are unresolved (OMN-17492).


Semantics (OMN-17492 — the model finds, thread resolution gates)

Surface Meaning Blocks merge?
Review threads Per-finding, posted by the reviewer No (informational)
Hostile Review Thread Gate Deterministic: unresolved hostile-reviewer threads exist Fails until resolved (not yet a required context)
degraded verdict Fewer than 2 models succeeded (infra) Fails this job with a named reason

Powered by omniintelligence.review_pairing.cli_review — multi-model adversarial review: qwen3-review (Qwen3.8-27B), gpt-oss-review (gpt-oss-120b) (OMN-8468/OMN-8524/OMN-17492)

@jonahgabriel
jonahgabriel force-pushed the rlane/omn-20386-delegate-reply-timeout branch from 1853041 to 86f2f59 Compare October 3, 2026 10:54
jonahgabriel and others added 4 commits October 3, 2026 09:13
A caller that ends `onex delegate` at its own timeout (300 s, SIGKILL, on
the landing lanes) left no receipt.json, so the run had a
delegation_events row and nothing on disk (4 cases on h201). Two h201
runs (52e9d386, 87fabfec) failed with a receipt that held no terminal
payload and also left nothing, because the resolver raised before the
writer ran.

- An in-flight receipt (status pending, terminal_class in_flight, run id,
  correlation id, declared timeout, command topic) is filed before the
  terminal wait. The terminal receipt replaces it; a run that ends with it
  still in place is rewritten as failed.
- The CLI's hard timeout writes a timeout receipt; SIGTERM, SIGHUP and
  SIGINT write an interrupted receipt and exit 128 + the signal number.
- A receipt with no resolvable terminal, including a run refused before
  publish, writes a failed or timeout receipt with the runtime's error and
  type, then raises as before.
- run_receipt_mode takes an optional run_id, so the id the CLI prints and
  files under is the id the final receipt lands in.

Two tests that pinned "nothing is written" now assert the receipt and the
unchanged raise.
… a usage error

Before: onex delegate with a deployed-lane run whose reply never arrives exited 2, printed the Usage banner, and left a run folder with only workflow_result.json.
Cause: the receipt validator _delegate_receipt_evidence_error in cli_delegate.py caught only DelegatePrePublishFailureError; the resolver's DelegateTerminalUnresolvedError (a ValueError) escaped run_receipt_mode and delegate_command maps ValueError to click.UsageError. The receipt writer never ran.
Change 1: delegate_terminal_resolver.py gains DelegateReplyTimeoutError (subclass of DelegateTerminalUnresolvedError), raised by _delegation_result when the runtime recorded workflow_result timeout plus a wire_correlation_id and no terminal resolves. Its message names "cause timeout", the run id and the wire correlation id.
Change 2: the validator returns the unresolved-terminal message instead of raising, so the exit is 1, with no Usage banner.
Change 3: receipt_mode keeps workflow_result timeout (instead of rewriting it as error) when a validator refuses a timed-out run that has a wire correlation id.
Change 4: the unterminalized receipt (introduced by PR 4502) now records wire_correlation_id, so receipt.json for this case holds terminal_class timeout, run_id, correlation_id and wire_correlation_id.
Tests: new tests/unit/cli/test_delegate_reply_timeout_omn20386.py (3 tests, red before, green after); one existing assertion in test_delegate_pre_publish_failure_omn19131.py extended. 524 delegate and receipt tests pass.
Stacked on PR 4502 (OMN-17427), which supplies the receipt writer.
Update test_an_unresolvable_terminal_refuses_and_names_the_field to assert receipt.json exists with terminal_class "failed", terminal_recorded False, and "attempts" in failure_reason. This corrects the assertion that previously checked the runs directory did not exist, which failed after commit e4f9e54 (OMN-17427) changed behavior to write receipts on all exit paths. The test still asserts DelegateTerminalUnresolvedError is raised with "attempts" in the message. test_another_nodes_run_is_still_silent is unchanged. Verified 7 tests pass in the file, 118 pass across the six touched test files, and ruff/mypy are clean.
Add test case for the new except DelegateCallerInterruptedError
branch in cli_delegate.py introduced by OMN-17427 (omnibase_infra#4502).
This branch handles SIGTERM/SIGHUP/SIGINT by writing an interrupted
receipt and exiting with 128 + signal number.

The new "caller-interrupted" case in BRANCH_CASES asserts a non-zero
exit, one run directory, and a failed receipt.json with failure_reason
carrying "ended by SIGTERM".

46 tests pass; ruff and mypy clean.
@jonahgabriel
jonahgabriel force-pushed the rlane/omn-20386-delegate-reply-timeout branch from 86f2f59 to 71a66b8 Compare October 3, 2026 13:14

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hostile Reviewer — adversarial findings (OMN-17492)

Models succeeded: qwen3-review, gpt-oss-review
Models failed: none
New finding threads: 2
Deduped (already posted on this PR): 0
Below quorum (one model only, reported not threaded): 11
Nit-level findings suppressed: 2

The model is the FINDER, never the gate: merge is gated only by the
deterministic Hostile Review Thread Gate, which blocks while
hostile-reviewer threads are unresolved. Resolve each thread after
addressing (or rejecting, with a reply) its finding.

Below quorum: 11 finding(s) raised by one model only (OMN-18479)

These are reported and NOT dropped, but they get no thread and do not block: a single model's finding no other model reproduced is not evidence enough to stop a merge. Read them; act on them if they are right.

  • [CRITICAL] src/omnibase_infra/cli/cli_delegate.py (gpt-oss-review) — is_reply_timeout called with wrong argument type | In delegation_result the function is_reply_timeout is invoked with the variable 'result', which holds a ModelDelegateTerminal or None, but is_reply
  • [MAJOR] src/omnibase_infra/cli/cli_delegate.py (qwen3-review) — Race condition in _settle_in_flight_receipt allows overwriting terminal receipts | The _settle_in_flight_receipt function performs a read-then-write on receipt.json without any file locking or atomic
  • [MAJOR] src/omnibase_infra/cli/cli_delegate.py (qwen3-review) — DelegateCallerInterruptedError raised inside signal handler may not propagate correctly | The _on_signal handler in _caller_interrupt_guard raises DelegateCallerInterruptedError directly from the sign
  • [MAJOR] src/omnibase_infra/cli/delegate_terminal_resolver.py (qwen3-review) — is_reply_timeout checks summary.get('wire_correlation_id') but envelope structure may not match | The is_reply_timeout function expects a summary dict with keys workflow_result and `wire_correlati
  • [MAJOR] src/omnibase_infra/cli/cli_delegate.py (gpt-oss-review) — Potential race condition when settling in‑flight receipt | The _settle_in_flight_receipt function reads the receipt file without any file‑system lock while another process (or a signal handler) could
  • [MAJOR] plan (gpt-oss-review) — Missing test for reply‑timeout detection path | New reply‑timeout logic introduced via is_reply_timeout and reply_timeout_error is not exercised by the test suite; the existing tests only cover pre‑pu
  • [MINOR] src/omnibase_infra/cli/cli_delegate.py (qwen3-review) — Unsanitized envelope data written to receipt.json | The _write_incomplete_run_files function writes the entire envelope dict to receipt.json via "receipt": envelope. The envelope is a dict[str, ob
  • [MINOR] tests/unit/cli/test_delegate_incomplete_receipt_omn17427.py (qwen3-review) — Missing test for SIGKILL leaving in-flight receipt with concurrent terminal writer | The test suite covers SIGKILL leaving an in-flight receipt, but does not test the race condition where a terminal w
  • [MINOR] src/omnibase_infra/cli/cli_delegate.py (qwen3-review) — _unterminalized_receipt_fields may produce misleading terminal_class for pre-publish failures | The _unterminalized_receipt_fields function classifies a receipt as TIMEOUT if workflow_result is 'timeo
  • [MINOR] tests/unit/cli/test_delegate_incomplete_receipt_omn17427.py (qwen3-review) — Test for signal handler restoration uses identity comparison that may be fragile | The test test_signal_handlers_are_restored_after_the_run asserts signal.getsignal(signal.SIGTERM) is before. This
  • [MINOR] src/omnibase_infra/cli/receipt_mode.py (qwen3-review) — run_id parameter added to run_receipt_mode changes the function's identity semantics | The run_receipt_mode function previously always minted a fresh run_id via uuid.uuid4(). The new run_id parameter

Findings demoted from threads (anchor rejected)

  • [MINOR] hostile-reviewer (qwen3-review)

    Synchronous file I/O in signal handler path may delay signal delivery | When DelegateCallerInterruptedError is caught, _write_incomplete_run_files performs multiple synchronous file writes (result.txt, receipt.json, run.json) before returning. If the caller sent SIGTERM because it has a hard deadline, these file writes may consume significant time, potentially causing the caller to escalate to SIGKILL before the process exits. The docstring acknowledges this risk ('a second wait here is how a SIGTERM become

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks while hostile-reviewer threads are unresolved (OMN-17492).

  • [MINOR] hostile-reviewer (gpt-oss-review)

    Increased I/O due to provisional in‑flight receipt writes | Every delegation now writes an additional provisional receipt before waiting for the terminal, and may later rewrite it. This doubles the number of file writes for successful runs, potentially impacting performance on high‑throughput workloads. | Evidence: Calls to _file_in_flight_receipt before the wait and later _settle_in_flight_receipt after the wait (src/omnibase_infra/cli/cli_delegate.py lines ~560 and ~720). | Fix: Make the provisional recei

    Resolve this thread when addressed — the Hostile Review Thread Gate blocks while hostile-reviewer threads are unresolved (OMN-17492).

@jonahgabriel
jonahgabriel added this pull request to the merge queue Oct 3, 2026
Merged via the queue into dev with commit 1aa2a37 Oct 3, 2026
159 checks passed
@jonahgabriel
jonahgabriel deleted the rlane/omn-20386-delegate-reply-timeout branch October 3, 2026 14:18
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