Skip to content

fix(eval): keep leftover processes through framework timeout - #3191

Open
1625567290 wants to merge 9 commits into
apache:mainfrom
1625567290:fix/eval-framework-timeout-background-policy
Open

fix(eval): keep leftover processes through framework timeout#3191
1625567290 wants to merge 9 commits into
apache:mainfrom
1625567290:fix/eval-framework-timeout-background-policy

Conversation

@1625567290

Copy link
Copy Markdown
Contributor

Summary

The verifier still scores a trial that hits the framework timeout (subject_failed plus the verifier reward). The relay used the same cancel path as host abort, so it signalled the recorded process group and could delete the environment — leftover services survived a clean subject exit and disappeared on timeout.

  • Framework timeout stops only the subject leader so environment.exec can return. It does not signal the process group.
  • If the leader never acknowledges TERM/KILL, the relay fails closed instead of publishing a scoreable timeout frame over an environment the subject may still be mutating.
  • Host abort still settles or destroys, because that trial is abandoned.
  • The Maka hosted-execution client uses abortPolicy: preserve_environment on this path, so a timeout abort does not cancel Host-owned work the verifier is about to score.

Fixes #3150

Verification

  • python3 harbor/test_relay_lifecycle.py — 14 pass, 3 skipped
  • python3 harbor/test_relay_contract.py — 20/20

Checklist

  • Tests cover the change and fail without it
  • Focused lint/typecheck and the affected suites pass locally
  • Full workspace lint/format/typecheck

Does this PR entail a change in behavior?

  • Yes — a framework timeout no longer tears down leftover subject processes or deletes the environment when the subject stops; an unconfirmed timeout is no longer reported as a scoreable framework_timeout

@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4a59392d-83ff-4ff4-93c7-e8726776e851

📥 Commits

Reviewing files that changed from the base of the PR and between cdcf818 and eed0e21.

📒 Files selected for processing (1)
  • packages/eval/harbor/test_relay_lifecycle.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/eval/harbor/test_relay_lifecycle.py

Included review availability: Your plan includes up to 3 reviews per rolling hour; 1 remains after this review.


📝 Walkthrough

What this PR solves

Framework timeouts now stop only the subject leader. The process group and environment remain available for verifier scoring.

If the leader does not exit within the TERM/KILL deadlines, the relay fails closed. It destroys the environment and does not publish a scoreable timeout frame.

Host abort behavior remains unchanged. It still quiesces and may destroy the trial.

The hosted-execution client uses abortPolicy: 'preserve_environment' for framework timeouts. Queued requests that were not admitted settle as cancelled.

Source of truth

This PR extends the existing relay and hosted-execution paths. It does not create a parallel timeout path.

The relay remains responsible for process signaling and timeout-frame safety. The hosted-execution client provides the explicit environment-preserving abort contract.

Scope and simplicity

The solution separates framework timeout behavior from host abort behavior:

  • Stop only the leader on framework timeout.
  • Preserve the process group and environment for verifier execution.
  • Fail closed when leader exit is unconfirmed.
  • Keep host-abort teardown unchanged.
  • Use the remaining stop deadline for leader-stop commands.
  • Prevent host settlement after detachment.
  • Preserve standard cancellation for requests that were not admitted.

The detached state and abort-policy branches are necessary to prevent environment destruction during an admitted, environment-preserving abort. No deletion or simplification is identified that would preserve the same behavior and regression coverage. Optional cleanup findings remain non-blocking.

Validation

  • 14 lifecycle tests pass.
  • 3 lifecycle tests are skipped.
  • 20/20 relay contract tests pass.
  • Tests cover leader-only signaling, ignored termination, vanished leaders, preserved background services, host teardown, and preserve_environment behavior before and after Host admission.
  • Integration coverage confirms that a background TCP service remains reachable after framework-timeout cleanup.
  • Required-check status remains unverified without direct check results.
  • Real-framework and Docker end-to-end behavior remains unverified.
  • The failed Windows lane may be infrastructure-related and requires rerun or explicit recording.

Complexity delta

  • Authorities: Adds an explicit abortPolicy authority while retaining relay ownership of timeout cleanup.
  • States: Adds admitted and detached execution states.
  • Branches: Adds leader-only termination, deadline handling, fail-closed cleanup, and environment-preserving cancellation branches.
  • Configuration: Adds optional RunHostedExecutionInput.abortPolicy values: cancel and preserve_environment.
  • Public surface: Adds the optional abortPolicy property.
  • Test burden: Adds lifecycle, relay, and hosted-execution coverage for the new behavior.

The PR increases local maintenance complexity. The added complexity is necessary to separate framework timeout from host abort and to protect scoring safety. Total maintenance complexity stays justified.

Review-relevant risks

The PR changes user-visible evaluation behavior. Framework timeouts can preserve background processes and allow verifier scoring.

The PR changes the public RunHostedExecutionInput contract.

The PR changes process signaling, environment lifetime, cancellation, and trial teardown behavior. Errors could affect process isolation, scoring, resource cleanup, or cancellation results.

Material changes in user-visible behavior, public contracts, process isolation, scoring, or teardown require independent human review under repository policy.

The person performing the merge must review the final diff. A maintainer makes the final determination.

Walkthrough

Framework timeouts preserve the subject environment for verification. The relay stops only the subject leader and confirms exit. Host aborts still destroy the environment. Hosted execution supports environment-preserving detachment with indeterminate status.

Changes

Timeout environment preservation

Layer / File(s) Summary
Hosted execution detachment
packages/runtime-host/src/client/hosted-execution.ts, packages/runtime-host/src/__tests__/hosted-execution-client.test.ts
Hosted execution accepts abortPolicy: 'preserve_environment'. Preservation abort releases the host, closes the connection, skips host settlement, and returns an indeterminate projection.
Relay timeout teardown
packages/eval/harbor/relay_agent.py, packages/eval/README.md
Framework timeout uses leader-only signaling and preserves the environment. Full process-group signaling remains for quiescence and host teardown. Unconfirmed subject exit causes destruction and an error.
Lifecycle validation
packages/eval/harbor/test_relay_lifecycle.py
Tests cover leader-only timeout stopping, host destruction, unconfirmed exit, and preservation of a live background TCP service.
Adapter policy wiring
packages/eval/src/harbor-maka-subject.ts, packages/eval/src/__tests__/lifecycle-boundaries.test.ts
The Maka subject passes preserve_environment to hosted execution. Lifecycle-boundary tests require and document the policy.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🔵 Low · up to eed0e

The change preserves leftover subject processes and the environment after framework timeouts so timed-out trials can still be scored, while host aborts continue teardown. The remaining merge-readiness concern is bounded: a lifecycle test relies on scheduler timing and may not reliably cover repeated cancellation during timeout cleanup, so merge is reasonable with explicit owner follow-up.

Sequence Diagram(s)

sequenceDiagram
  participant EvalRelay
  participant HostedExecution
  participant RuntimeHost
  participant SubjectLeader
  participant VerificationEnvironment
  EvalRelay->>HostedExecution: start execution with preserve_environment
  EvalRelay->>SubjectLeader: stop leader on framework timeout
  HostedExecution->>RuntimeHost: release host and close connection
  SubjectLeader-->>EvalRelay: confirm subject exit
  EvalRelay->>VerificationEnvironment: run verification in preserved environment
  EvalRelay->>VerificationEnvironment: destroy environment if exit is unconfirmed
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the problem, implementation, issue link, verification, behavior change, and checklist, but omits the required AI use section. Add the AI use section, select exactly one option, and name the tools and scope when generative tooling made a substantive contribution.
Ai Use Disclosure ⚠️ Warning The authored PR description selects neither AI-use declaration. If generative tooling made a substantive contribution, the disclosure is missing; all six introduced commits have no Generated-by tra... Select exactly one declaration and, if AI authored material contribution content, name the tool and scope and add consistent trailers to affected commits so they survive squash or amend. See CONTRIBUTING.md: Human ownership and AI attrib...
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: preserving leftover processes through framework timeouts.
Linked Issues check ✅ Passed The changes define consistent timeout and abort behavior, preserve background processes, fail closed when termination is unconfirmed, and add focused lifecycle coverage for issue #3150.
Out of Scope Changes check ✅ Passed The relay, hosted-execution client, documentation, and tests all support the linked issue objectives and contain no unrelated changes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Preserve evaluation environments across framework timeouts

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Preserve leftover subject processes when framework timeouts remain eligible for verifier scoring.
• Detach Maka’s hosted client instead of cancelling Host-owned execution during timeout aborts.
• Fail closed when the relay cannot confirm the timed-out subject leader exited.
Diagram

graph TD
  T["Framework Timeout"] --> C["Hosted Client"] --> H["Runtime Host"] --> P["Preserved Environment"] --> V["Verifier"]
  T --> R["Relay Agent"] --> D{"Leader Stopped?"}
  D -->|Yes| P
  D -->|No| X["Fail Closed"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Separate execution control groups
  • ➕ Provides explicit ownership boundaries between the subject leader and retained services.
  • ➕ Avoids relying on the recorded leader PID for selective signalling.
  • ➖ Requires broader launcher, task-contract, and environment changes.
  • ➖ Adds platform-specific process-management complexity for a focused lifecycle fix.

Recommendation: Keep the PR’s leader-only timeout signalling and explicit hosted-client detach policy. It directly aligns cleanup with scoring semantics while preserving host-abort teardown; dedicated control groups may offer stronger long-term ownership isolation but are disproportionate for this fix.

Files changed (7) +399 / -31

Bug fix (3) +156 / -12
relay_agent.pySeparate framework timeout from host teardown +104/-8

Separate framework timeout from host teardown

• Adds a framework-timeout path that signals only the subject leader and preserves descendants after confirmed exit. Retains process-group settlement for host aborts and fails closed by deleting the environment when leader exit cannot be confirmed.

packages/eval/harbor/relay_agent.py

harbor-maka-subject.tsPreserve the evaluation environment on Maka abort +1/-0

Preserve the evaluation environment on Maka abort

• Configures Maka hosted execution to detach on abort rather than cancel Host-owned work that the verifier is about to inspect.

packages/eval/src/harbor-maka-subject.ts

hosted-execution.tsAdd environment-preserving hosted abort policy +51/-4

Add environment-preserving hosted abort policy

• Introduces an optional abort policy that releases Host ownership and closes the connection instead of sending cancellation. Detached executions bypass settlement and return an indeterminate projection when no environment-preserving result is available.

packages/runtime-host/src/client/hosted-execution.ts

Tests (3) +236 / -14
test_relay_lifecycle.pyCover timeout preservation and teardown boundaries +178/-7

Cover timeout preservation and teardown boundaries

• Adds lifecycle tests proving that framework timeout preserves background services, avoids process-group signals, and rejects unconfirmed exits as scoreable. Also verifies that explicit host teardown still destroys a running subject environment.

packages/eval/harbor/test_relay_lifecycle.py

lifecycle-boundaries.test.tsRequire Maka’s environment-preserving abort policy +9/-7

Require Maka’s environment-preserving abort policy

• Updates the shim contract test to assert that hosted execution receives 'abortPolicy: preserve_environment'. Refines comments to describe exit codes as projections of result-frame status.

packages/eval/src/tests/lifecycle-boundaries.test.ts

hosted-execution-client.test.tsTest environment-preserving hosted execution detach +49/-0

Test environment-preserving hosted execution detach

• Verifies that a preserving abort releases the Host to the environment, closes the client connection, and avoids cancellation or settlement. Confirms the detached execution returns an indeterminate verification-oriented projection.

packages/runtime-host/src/tests/hosted-execution-client.test.ts

Documentation (1) +7 / -5
README.mdDocument score-preserving framework timeout behavior +7/-5

Document score-preserving framework timeout behavior

• Clarifies that framework-timeout trials are still verified, so leftover processes and the environment remain intact after the subject leader stops. Distinguishes this path from host abort, where the trial is abandoned and teardown remains appropriate.

packages/eval/README.md

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/eval/harbor/test_relay_lifecycle.py (1)

545-580: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The comment does not describe what this fixture exercises.

FrameworkTimeoutEnvironment inherits the pgid= branch from SimultaneousEnvironment, which returns return_code=3. _signal_leader therefore returns False, and the relay takes the vanished-leader branch. No signal is ever delivered, so the subject cannot "never acknowledge" one. Correct the comment, or make the fixture return 0 for the leader-stop command so the test covers a subject that ignores both TERM and KILL. The second option is the one that adds coverage.

🧹 Nitpick comments (1)
packages/eval/harbor/relay_agent.py (1)

537-551: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The destroy-and-cancel block duplicates _settle_or_destroy.

Lines 540-550 repeat the destroy, cancel, and bounded-await sequence at lines 570-580 verbatim. Two copies of the same teardown authority drift apart the moment one deadline rule changes. Extract one helper, for example _destroy_environment(environment, execution, deadline, loop), and call it from both places.

As per path instructions: "Choose remedies in this order: delete an unnecessary path, consolidate duplicated authority, reuse the closest existing seam…".

Source: Path instructions


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ea312524-cc98-492f-8579-47c13d3c358a

📥 Commits

Reviewing files that changed from the base of the PR and between 62f550c and 517b077.

📒 Files selected for processing (7)
  • packages/eval/README.md
  • packages/eval/harbor/relay_agent.py
  • packages/eval/harbor/test_relay_lifecycle.py
  • packages/eval/src/__tests__/lifecycle-boundaries.test.ts
  • packages/eval/src/harbor-maka-subject.ts
  • packages/runtime-host/src/__tests__/hosted-execution-client.test.ts
  • packages/runtime-host/src/client/hosted-execution.ts

Included review availability: Your plan includes up to 3 reviews per rolling hour; 2 remain after this review.

Comment thread packages/eval/harbor/relay_agent.py Outdated
Comment thread packages/eval/harbor/test_relay_lifecycle.py Outdated
Comment thread packages/eval/README.md Outdated
@qodo-code-review

qodo-code-review Bot commented Aug 18, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Timeout fallback deletes environment it must preserve ✗ Dismissed 🐞 Bug ≡ Correctness
Description
_stop_subject_for_timeout's stated purpose is to avoid deleting the environment on framework
timeout ("do not hunt descendants or delete the environment"), but its unconfirmed-exit fallback
calls environment.stop(delete=True) and only then raises RuntimeError. If TERM/KILL never produce
positive proof of leader exit within the teardown budget, the environment is destroyed anyway,
directly contradicting the PR's headline behavior change ('a framework timeout no longer... deletes
the environment when the subject stops') for the case where the leader simply never confirms exit
within budget.
Code

packages/eval/harbor/relay_agent.py[R537-543]

+    # A verifier cannot measure a stable environment while the subject may
+    # still be mutating it. Fail closed instead of publishing a scoreable
+    # framework_timeout frame without positive leader-exit evidence.
+    try:
+        remaining = max(0.001, deadline - loop.time())
+        await asyncio.wait_for(environment.stop(delete=True), timeout=remaining)
+    except Exception:
Relevance

●●● Strong

Deleting on unconfirmed exit directly contradicts this PR’s stated environment-preservation
behavior.

PR-#2674

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The function's own comment at lines 489-491 states framework timeout must not delete the
environment, but the fallback path at 540-543 explicitly requests delete=True. The new test suite
only covers two branches (successful leader-stop and confirmed unconfirmed-exit) but never exercises
this deletion within _stop_subject_for_timeout under a still-running subject, so the contradiction
is untested.

packages/eval/harbor/relay_agent.py[489-543]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`_stop_subject_for_timeout` documents that a framework timeout must never delete the environment, but its fallback path (when leader exit cannot be confirmed) calls `environment.stop(delete=True)`.

## Issue Context
This fallback runs specifically on the framework-timeout code path (not host abort, which correctly uses `_settle_or_destroy` with delete). Deleting the environment here reintroduces the exact bug this PR set out to fix: leftover services disappearing on timeout.

## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[537-551]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Leader stop exceeds deadline ✓ Resolved 🐞 Bug ☼ Reliability
Description
Fix-now: _stop_subject_for_timeout gives each leader-signal environment.exec a fixed five-second
timeout outside its own deadline accounting, then waits using the stale pre-signal remaining
value. With a valid one-second teardown budget, a slow signal command can therefore delay
framework-timeout handling for more than five seconds instead of failing closed within the
configured budget.
Code

packages/eval/harbor/relay_agent.py[R605-609]

+        result = await environment.exec(
+            command,
+            cwd=cwd,
+            timeout_sec=5,
+        )
Relevance

●●● Strong

Fixed signal timeout can exceed the configured teardown deadline; prompt cancellation and bounded
cleanup are accepted reliability priorities.

PR-#3169
PR-#2674

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The helper establishes an overall deadline and computes remaining before signaling, but
_signal_leader independently allows five seconds and lines 521-525 reuse the old value afterward.
The relay accepts any positive millisecond budget, and existing lifecycle tests instantiate a 1,000
ms budget, so the fixed five-second operation can exceed a supported configuration.

packages/eval/harbor/relay_agent.py[81-83]
packages/eval/harbor/relay_agent.py[496-525]
packages/eval/harbor/test_relay_lifecycle.py[593-596]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Leader signaling can outlive the overall teardown deadline and the subsequent execution wait uses a stale remaining duration.

## Issue Context
Reuse the helper's existing monotonic deadline: cap each signal command and post-signal wait by a freshly recomputed remaining duration. No new timeout state or configuration is needed.

## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[496-532]
- packages/eval/harbor/relay_agent.py[598-612]
- packages/eval/harbor/test_relay_lifecycle.py[582-618]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Detach precedes execution admission ✓ Resolved 🐞 Bug ≡ Correctness
Description
Fix-now: a preserving abort can release the owned Host and close its connection before
hosted.execution.start is dispatched, after which the catch path falsely reports that execution
continues even though the request may be rejected as not_dispatched. This is reproducible when the
signal aborts after connection/setup but before the start request, leaving no verifier work running
on the preserved environment.
Code

packages/runtime-host/src/client/hosted-execution.ts[R149-151]

+    if (abortPolicy === 'preserve_environment') {
+      detach();
+      return;
Relevance

●●● Strong

Abort-before-dispatch can falsely claim continuation; admission and cancellation boundary fixes are
established team priorities.

PR-#2674

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The preserve handler immediately releases ownership and starts closing the connection, but the start
request is issued afterward and detached failures are unconditionally translated to “continues.” The
real connection rejects requests made after terminal close as not_dispatched, proving this race
can report continuation for work that never reached the Host.

packages/runtime-host/src/client/hosted-execution.ts[142-180]
packages/runtime-host/src/client/connection.ts[487-497]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A preserve-policy abort can detach and close the connection before hosted execution has been admitted, yet returns that execution continues.

## Issue Context
Reuse the existing cancellation/settlement path when abort happens before start admission; only release ownership after the start request has actually been dispatched. This consolidates ownership decisions around the existing request seam and adds no new public state.

## Fix Focus Areas
- packages/runtime-host/src/client/hosted-execution.ts[142-184]
- packages/runtime-host/src/__tests__/hosted-execution-client.test.ts[94-133]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Signal exception conflated with vanished leader 🐞 Bug ☼ Reliability
Description
_signal_leader returns False for any exception raised by environment.exec, not only when the
leader process is actually gone. _stop_subject_for_timeout treats every False as "leader
vanished," waits only 100ms, and then breaks without ever attempting KILL — so a transient
environment.exec failure while sending TERM (e.g., a network blip) skips KILL entirely and can
fall through to the environment.stop(delete=True) fallback even though a live subject was never
actually signaled.
Code

packages/eval/harbor/relay_agent.py[R505-520]

+        signalled = await _signal_leader(environment, cwd, scope_path, signal)
+        if not signalled:
+            # A vanished leader can race the environment.exec completion by a
+            # few scheduling turns. Admit that terminal result, but do not
+            # spend the rest of the timeout pretending an unissued signal is
+            # evidence that a live subject stopped.
+            try:
+                return await asyncio.wait_for(
+                    asyncio.shield(execution), timeout=min(0.1, remaining)
+                )
+            except asyncio.CancelledError:
+                if execution.cancelled():
+                    raise RuntimeError("Maka Eval subject execution was cancelled") from None
+                raise
+            except (TimeoutError, asyncio.TimeoutError):
+                break
Relevance

●●● Strong

Exception handling conflates transport failure with vanished leader, risking skipped KILL
escalation.

PR-#2674

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
_signal_leader's except Exception: return False (relay_agent.py:611-612) does not distinguish a
genuinely vanished pgid file/process from a transient exec/transport error. The caller's `if not
signalled:` branch (505-520) assumes the former and gives up the loop on any timeout, bypassing the
KILL escalation that would otherwise run for a still-live process.

packages/eval/harbor/relay_agent.py[598-613]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`_signal_leader` maps any exception (transient exec/transport failure included) to the same `False` result used for "process already gone", causing the caller to skip the KILL escalation and risk premature environment deletion.

## Issue Context
The intent (per code comment) is only to short-circuit when the leader has legitimately already exited; a transient failure to deliver TERM should instead retry or proceed to KILL, not be treated as proof of exit.

## Fix Focus Areas
- packages/eval/harbor/relay_agent.py[598-613]
- packages/eval/harbor/relay_agent.py[505-520]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. abortPolicy unconditionally preserves environment on all aborts 🐞 Bug ≡ Correctness
Description
harbor-maka-subject.ts unconditionally passes abortPolicy: 'preserve_environment' for every
abort of its local AbortController, which is triggered only by that child process's own
SIGINT/SIGTERM handlers — the same signal delivery path used for both a genuine abandoned-trial
abort and any other termination reaching this shim. Consequently executeHostedExecution's
detach() releases the Host and closes only the connection without ever calling
connected.host.settle(...), so scenarios the PR says should still "settle or destroy" (host abort)
instead leave the Host process undetermined/unsettled whenever this Node-level SIGINT/SIGTERM path
is exercised.
Code

packages/eval/src/harbor-maka-subject.ts[57]

+    abortPolicy: 'preserve_environment',
Relevance

●● Moderate

The policy is intentional for framework timeout, but broader SIGTERM/SIGINT semantics are not
established by available history.

PR-#2674

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
abort.abort() is wired only to process.once('SIGINT'/'SIGTERM', stop)
(harbor-maka-subject.ts:34-39), and this same abort.signal is now always sent with `abortPolicy:
'preserve_environment' (line 57). In hosted-execution.ts, preserve_environment` abort routes to
detach() (hosted-execution.ts:142-152), which releases the host and closes the connection but
never calls host.settle(...), and the outer function returns early via `if (detached) return
projection; (line 122) before the settle-or-release branch that otherwise runs for abortPolicy:
'cancel'`. Whether this shim's SIGINT/SIGTERM is only ever used for
framework-timeout-abandoned-in-place trials versus also being reused for other abort reasons could
not be fully confirmed from the diff alone, but the code applies the policy with no conditional
logic distinguishing reasons.

packages/eval/src/harbor-maka-subject.ts[34-39]
packages/runtime-host/src/client/hosted-execution.ts[118-152]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`harbor-maka-subject.ts` always sets `abortPolicy: 'preserve_environment'`, so every abort of its local AbortController — regardless of the reason the shim receives SIGINT/SIGTERM — skips Host settlement.

## Issue Context
The PR description states host abort should still settle/destroy since that trial is abandoned, while only framework timeout should preserve the environment. If this shim's signal ever fires for reasons other than a framework-timeout-driven leader stop (e.g. any other externally delivered SIGTERM), the intended distinction is lost at the Node/runtime-host layer.

## Fix Focus Areas
- packages/eval/src/harbor-maka-subject.ts[18-39]
- packages/eval/src/harbor-maka-subject.ts[50-59]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

6. Detach never awaits Host settlement on error ✓ Resolved 🐞 Bug ◔ Observability
Description
In executeHostedExecution, once detach() runs, the function returns without ever invoking
connected.host.settle(...), relying solely on releaseToEnvironment() (child.unref()) to hand
off the runtime-host child process; there is no code path that waits for or verifies the detached
Host process's outcome.
Code

packages/runtime-host/src/client/hosted-execution.ts[R140-147]

+  let detached = false;
+  let closeForDetach: Promise<void> | undefined;
+  const detach = () => {
+    if (abortPolicy !== 'preserve_environment' || detached) return;
+    detached = true;
+    host.releaseToEnvironment();
+    closeForDetach = connection.close().catch(() => undefined);
+  };
Relevance

●● Moderate

No settlement is explicitly intentional for preservation, making this an observability concern
rather than a clearly accepted defect.

PR-#2674

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
detach() (140-147) calls host.releaseToEnvironment() and closes only the connection;
runHostedExecutionWithDependencies short-circuits with if (detached) return projection; (line
122) before it would otherwise reach connected.host.settle(...) at line 127.
releaseToEnvironment in launcher.ts is implemented as child.unref() with no wait/verify step.

packages/runtime-host/src/client/hosted-execution.ts[140-147]


Grey Divider

Context
Review mode: 🧠 Deep: This is a behaviorally significant, cross-package timeout/teardown change spanning relay state transitions, process signaling, environment destruction, and hosted-execution abort semantics, with many independent edge cases that merit redundant review.

Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Finding overflow, which tucks the rest behind 'View more'

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread packages/runtime-host/src/client/hosted-execution.ts
Comment thread packages/eval/harbor/relay_agent.py
Comment thread packages/eval/harbor/relay_agent.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 471f8fe3-f0db-4b0c-8db9-364148cd9f23

📥 Commits

Reviewing files that changed from the base of the PR and between 3aca1fa and ecffaff.

📒 Files selected for processing (4)
  • packages/eval/harbor/relay_agent.py
  • packages/eval/harbor/test_relay_lifecycle.py
  • packages/runtime-host/src/__tests__/hosted-execution-client.test.ts
  • packages/runtime-host/src/client/hosted-execution.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/runtime-host/src/tests/hosted-execution-client.test.ts
  • packages/eval/harbor/test_relay_lifecycle.py

Included review availability: Your plan includes up to 3 reviews per rolling hour; 0 remain after this review.

Comment thread packages/eval/harbor/relay_agent.py
@Astro-Han

Copy link
Copy Markdown
Contributor

Thanks for the fix — the inconsistency is real (on timeout the old run() cancelled the recorded process group and could delete the environment, so leftover services vanished on timeout but survived clean exit), and the solution is sound: _host_teardown_requested distinguishes host abort (still settle/destroy) from framework timeout (stop only the leader), the budget math in _stop_subject_for_timeout is correct (TERM ≤20s / KILL ≤10s slices bounded by remaining, destroy reserve min(20, 0.2*timeout) — 30s budget math checks out), _signal_leader's kill -- "$pgid" is correct bash semantics (pgid file = setsid'd sh PID = group leader, exec'd into the subject), and the fail-closed paths (leader ignoring TERM/KILL, vanished leader with unconfirmed exec) destroy the environment and stop publishing scoreable frames — all under test (test_unconfirmed_…, test_ignored_…). The architecture boundary holds: only the Runtime Host client (abortPolicy option, default 'cancel' unchanged) and the eval relay changed; @maka/eval doesn't construct Runtime or touch host implementation. I ran the relay tests on the PR head (20/20 + harbor lifecycle 20, 3 skipped) and verified red→green by swapping main's relay_agent.py back in — 4 new timeout tests fail on old code, so the coverage is genuine. The live-process test (test_framework_timeout_preserves_a_live_background_service) really keeps a connection alive through leader TERM.

Conclusion: PASS with two P2s (Windows lane infra-flake + end-to-end validation gap) — no P0/P1.

P2-1 — the other side of this contract lives outside the repo: end-to-end behavior is unverified against the real framework. The fix keeps leftovers alive only if the framework's own timeout mechanism doesn't independently cancel environment.exec's recorded process group or destroy the environment. The framework is a pip dependency installed by run_trial.py:98-100 and isn't in the diff, so this can't be confirmed or refuted from the repo. The live test uses LocalEnvironment (local process exec), not real docker exec cancel semantics. Please either run a real Harbor cell (start background service → trigger framework timeout → verify the service reachable in the validate phase) or explicitly state this premise and the verification plan in the PR description.

P2-2 — the failed Windows lane is an environment flake, not this PR. The failure ("Validate installed CLI Windows x64 / Node 24" → "checking the interactive TUI setup path" → INTERNAL_STARTUP_FAILURE) matches byte-for-byte the same step on an unrelated dependabot PR (32105259238), the PR's own earlier commits passed the same lane, and the final commit only touches eval relay Python (not executed by the Windows smoke) and the hosted-execution client (not in the TUI startup path). Please re-run that lane (or record it as a known infra issue) rather than attributing it to this PR.

P3 (optional): the elif not execution_reported and not _host_teardown_requested: branch in relay_agent.py:201-209 is now dead code (_stop_subject_for_timeout can't return None — it either returns or raises), so the result-frame-missing fallback frame can no longer be produced on the timeout path; await closeForDetach at hosted-execution.ts:184 has no timeout (a hung close means the relay budget exhausts → destroy + fail-closed, safe but unbounded); _signal_leader treats any exec exception (including transient) as "leader vanished" — if the subject then doesn't finish within remaining, the environment is destroyed and the trial unscoreable (documented fail-closed, low probability); README has two paragraphs squeezed onto one line, and _stop_subject_for_timeout is annotated -> Any | None but never returns None.


AI-assisted review disclosure: this review was produced with AI assistance (pi review subagent on opencode-go/deepseek-v4-flash). The subagent ran the relay tests on both the PR head and the old implementation (red→green proof), traced the budget math, and pulled and compared the Windows failure log against the unrelated dependabot run; P2-1 is an unverifiable-from-repo gap the PR itself implies, not an observed failure. Please weigh these findings with your own judgment.

中文摘要(AI 辅助审查)

结论:PASS(2 个 P2:Windows lane 环境 flake + 端到端验证缺口;无 P0/P1)。问题真实(timeout 时旧 run() 无条件 cancel recorded process group 且可删环境→leftover 在 timeout 时消失、clean exit 时存活),方案正确:_host_teardown_requested 区分 host abort(仍 settle/destroy)与 framework timeout(只停 leader);_stop_subject_for_timeout 预算 math 正确(TERM≤20s/KILL≤10s 各 slice 被 remaining 约束、destroy 保留 min(20,0.2*timeout),30s 预算推算成立);_signal_leader 的 kill -- "$pgid" 在 bash 语义下正确(pgid 文件=setsid'd sh PID=组 leader,exec 后即 subject);fail-closed 路径(leader 无视 TERM/KILL、vanished leader 且 exec 未确认)销毁环境并停止发布可计分 frame,均有测试。架构边界保持:只改 Runtime Host client(abortPolicy 新选项,默认 'cancel' 不变)与 eval relay;@maka/eval 未构造 Runtime、未碰 host 实现。实跑 relay 测试(20/20 + harbor lifecycle 20(3 skipped))并把 main 的 relay_agent.py 换回后 4 个新 timeout 测试全失败——red→green 证明测试真实;实况测试用真实进程验证 background service 在 leader TERM 后仍可 TCP 连接。P2-1:修复的另一侧在仓库外——framework 自身 timeout 机制若独立 cancel recorded process group 或销毁环境,则 end-to-end 不成立;框架是 pip 依赖(run_trial.py:98-100 安装)不在 diff 中,仓库内无法证实/证伪;实况测试用 LocalEnvironment 不覆盖真实 docker exec 的 cancel 语义。建议用真实 Harbor cell 验证(启动 background service→触发 framework timeout→验证阶段服务可达)或至少在描述中声明前提与验证方式。P2-2:Windows lane 失败是环境 flake 非本 PR 引入——同一错误签名在无关 dependabot PR(32105259238)同步骤逐字复现、本 PR 更早 commits 同 lane 通过、最后 commit 只改 eval relay Python(Windows smoke 不执行)与 hosted-execution client(不在 TUI 启动路径)。建议重跑该 lane 或按已知 infra 问题延后。P3(可选):relay_agent.py:201-209 的 elif 分支已成死代码(_stop_subject_for_timeout 不可能返回 None,结果帧缺失兜底 frame 在 timeout 路径已无法产生);hosted-execution.ts:184 closeForDetach 无超时(挂起时 relay 预算耗尽→destroy+fail-closed,安全但无界);_signal_leader 对待瞬态 exec 异常一律当"leader 已消失",若 subject 未在 remaining 内完成则环境被毁、trial 不可计分(文档化 fail-closed,概率低);README 两段挤一行、_stop_subject_for_timeout 标注 -> Any|None 但从不返回 None。

@Astro-Han Astro-Han 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.

Separating framework timeout from host abort is the correct ownership model: the verifier may need subject leftovers, while an abandoned trial still requires destructive cleanup. The current implementation loses that distinction at two race boundaries—before Hosted Execution admission is known, and when timeout cleanup itself is cancelled or fails.

The first-principles solution is an explicit server-owned admission token plus one shielded, bounded cleanup state machine. Preserve/detach is allowed only after admission is acknowledged; a second cancellation or execution error transitions that same cleanup state machine to environment destruction before cancellation is re-raised. This is simpler than inferring ownership from a client-side boolean and nested exception handlers.

Review performed with two Codex reviewer agents and DeepSeek V4 Flash as advisory tools; I verified both failure paths against the latest head and current main.

中文评论

区分 framework timeout 与 host abort 是正确的 ownership 模型:verifier 可能需要 subject leftovers,而被放弃的 trial 仍必须执行破坏性清理。当前实现会在两个 race 边界丢失这一区分:Hosted Execution admission 尚未确认时,以及 timeout cleanup 自身再次被取消或失败时。

更符合第一性原理的方案是显式的 server-owned admission token,加上一套 shielded、bounded cleanup state machine。只有 admission ack 后才允许 preserve/detach;第二次 cancellation 或 execution error 必须让同一 cleanup state machine 转入 environment destruction,完成后再重新抛出 cancellation。这比依赖客户端 boolean 和嵌套 exception handler 推断 ownership 更简单可靠。

本次审查使用了两位 Codex reviewer agents 与 DeepSeek V4 Flash 作为辅助工具;我已依据最新 head 和当前 main 复核两个失败路径。

Comment thread packages/runtime-host/src/client/hosted-execution.ts Outdated
Comment thread packages/eval/harbor/relay_agent.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: dd32ad82-cf2a-4924-a458-5bff0ddc543b

📥 Commits

Reviewing files that changed from the base of the PR and between ecffaff and cdcf818.

📒 Files selected for processing (4)
  • packages/eval/harbor/relay_agent.py
  • packages/eval/harbor/test_relay_lifecycle.py
  • packages/runtime-host/src/__tests__/hosted-execution-client.test.ts
  • packages/runtime-host/src/client/hosted-execution.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/eval/harbor/relay_agent.py

Included review availability: Your plan includes up to 3 reviews per rolling hour; 2 remain after this review.

Comment thread packages/eval/harbor/test_relay_lifecycle.py

@Astro-Han Astro-Han 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.

The latest commits improve the timeout boundary: leader shutdown now uses the remaining deadline, unconfirmed shutdown fails closed, and the second-cancel test now enters the intended cleanup window. Two authority gaps remain.

The simplest correct model is a server-owned admission token followed by one shielded, deadline-bounded cleanup state machine. Transport dispatch is not admission, and every exception raised while finalizing cancellation must converge on settle-or-destroy before being rethrown. This removes the current sibling-exception special cases.

Reviewed with Codex using two independent review passes and DeepSeek V4 Flash as an external adversarial pass; I verified the cited state transitions against this exact head and current main.

中文

最新提交改进了 timeout 边界:leader stop 使用剩余 deadline,无法确认退出时 fail-closed,二次 cancel 测试也进入了正确窗口。但仍有两个权威缺口。

最小正确模型是:由 server 返回 admission token;随后所有取消收尾都进入一个 shielded、带 deadline 的状态机。transport dispatch 不能代表 admission,收尾中的任何异常都必须先 settle-or-destroy 再重抛。

本次由 Codex 两轮独立审查,并使用 DeepSeek V4 Flash 做外部对抗审查;我核对了当前 head 与最新 main

Comment thread packages/runtime-host/src/client/hosted-execution.ts Outdated
Comment thread packages/eval/harbor/relay_agent.py
The verifier still scores a timed-out trial, so the relay must not
quiesce the process group or delete the environment. Stop only the
subject leader so exec can return. Host abort still settles or
destroys, because that trial is abandoned.

Fixes apache#3150
Harbor exec completion routinely exceeds 100 ms, so a vanished-leader
grace of 0.1 s turned a finished subject into an infrastructure
failure. Wait for the reserved stop deadline instead. Test predicates
now match the group-signal form the relay actually emits.
A preserve abort before hosted.execution.start must not claim the
execution continues. Leader-stop execs now take the remaining stop
deadline instead of a fixed five seconds.
A preserve abort now inspects whether hosted.execution.start was
dispatched. A queued, not-yet-admitted request settles as cancelled.
Cancel cleanup uncancels the task so a second cancel still destroys
the environment. Leader-stop execs use the remaining stop deadline.
The recancel fixture now sets an event when the leader-stop exec
starts. The test waits on that event so the second cancel lands
during cleanup, not before it.
Transport in-flight is not Host admission. preserve_environment now
calls hosted.execution.admit and detaches only after that token
returns. A frame-written interrupt without the token settles the
Host instead of claiming execution continues.

Cancelled relay cleanup funnels every finalize exception through
settle-or-destroy before rethrowing, including execution and
persist failures during host abort.

Fixes apache#3150
@1625567290
1625567290 force-pushed the fix/eval-framework-timeout-background-policy branch from eed0e21 to 9281bc9 Compare August 18, 2026 16:38
Astro-Han
Astro-Han previously approved these changes Aug 18, 2026

@Astro-Han Astro-Han 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.

The current head closes the two earlier authority gaps. Framework timeout and Host abort now enter distinct relay paths; cancellation cleanup converges on bounded settle-or-destroy; and preserve/detach is permitted only after a server-owned admission acknowledgement. If that acknowledgement is lost, the client deliberately fails closed and settles the owned Host rather than claiming preservation it cannot prove.

The solution keeps Runtime Host as the execution authority and uses one cleanup state machine instead of inferring ownership from transport dispatch. I found no blocking correctness issue. The inline P3 removes an unreachable fallback left behind by the stronger fail-closed contract.

CI note: all completed checks are green; test_workspaces is still in progress, so merge readiness still depends on that final check.

AI-assisted review disclosure: Codex performed the final review using two independent reviewer passes and OpenCode Go DeepSeek V4 Flash (high effort) as an adversarial advisory pass. I verified admission/abort state transitions, deadline handling, exact head 9281bc99b, and live CI; no local tests were run.

中文评论

当前 head 已关闭此前两个 authority 缺口:framework timeout 与 Host abort 进入不同 relay 路径;取消清理统一收敛到有界的 settle-or-destroy;只有收到 server-owned admission ack 后才允许 preserve/detach。如果 ack 丢失,client 会刻意 fail closed 并 settle owned Host,而不会声称一个无法证明的 preservation。

方案保持 Runtime Host 为 execution authority,并使用单一 cleanup state machine,不再从 transport dispatch 推断 ownership。未发现阻塞性正确性问题。行内 P3 只删除强化 fail-closed 契约后留下的不可达 fallback。

CI 说明:所有已完成 checks 均为绿色;test_workspaces 仍在运行,因此是否可合并还取决于这个最后检查。

AI 辅助审查说明:Codex 使用两轮独立 reviewer 审查,并以 OpenCode Go DeepSeek V4 Flash(high effort)进行对抗性辅助审查;我已核对 admission/abort 状态转换、deadline、精确 head 9281bc99b 与实时 CI。本轮未运行本地测试。

Comment thread packages/eval/harbor/relay_agent.py Outdated
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.

eval: define a consistent framework-timeout policy for background processes across arms

2 participants