Skip to content

harness: dependency timeout during failure finalization can leave turn running after reconnect #1108

Description

@McHersheys

Current verification status — 2026-09-06

The original incident report below is retained as historical evidence. Subsequent work now separates two branches:

  1. Failed-turn finalization: 7/7 controlled unchanged-release dependency fault/control cases reproduced the swallowed read/write-failure path and distinguished a committed write with a lost acknowledgement from an absent write.
  2. Later stranded admission: a separate real Engine/Queue handoff plus one deliberately lost reply reproduced a failed predecessor, new Running step0 successor and two in-flight deliveries. The diagnostic recovered at its shortened2.5s queue deadline. The initiating live outage and an indefinite wedge are not proven by that result.

Executed fixtures, full branch matrix and limitations.

Current main c86b4ddbd414eb43683cc70c6a3245b7e56c0cc3 already propagates failure-finalization errors and holds the session lock. A separate narrow follow-up patch confirms a timed-out terminal write by one exact whole-record readback without repeating work. It has a genuine failing regression baseline,4/4 passing regressions,455/455 passing library tests, formatting and Clippy checks. Tested patch and precise scope.

That patch is not deployed or accepted upstream; PR submission awaits the required matching MOT ticket. The later queue branch and original live initiating fault remain separate. No uncertain tool effects were replayed; final deployment and autonomous acceptance remain blocked.


Original incident report — preserved

Observed failure

An unmodified released Harness worker left a turn durably running after logging that it was finalising that turn as failed. Its SDK subsequently reconnected to the Engine, but the turn did not reach a terminal state or continue autonomously. harness::stop acknowledged stopping: true; readback showed the abort request but the same running step.

This is an observed integration fault plus a source-level hypothesis, not yet a minimal deterministic reproducer or a verified fix.

Version and scope

  • Harness release harness-v1.8.13, release source deb6f98dbd3c7cbc226234844761430a47b911cf.
  • Unmodified worker binary SHA-256: 563f96d9b59fa1a9b93c3b9b1e16aeda4aee5f601a247a622c93bbc5f28a0b03.
  • Linux amd64, bounded gVisor sandbox, private local Engine connection.
  • The relevant fail_turn error-handling shape is also present in the inspected 1.8.15 source and current main (00417f34dcee7465d5d4602f4ab497db106f3e55). This does not assert the complete live reproduction was repeated on those versions.

Sanitized timeline (2026-09-06 UTC)

08:54:48.725894  turn started
08:55:05.722388  ERROR turn step failed; finalising turn as failed
                       error=harness/dependency: session::messages: invocation timed out
08:55:13.556042  WARN expiry sweep skipped: binding store unreadable
                       error=harness/state: harness::state::list harness_binding: invocation timed out
08:55:53.558216  WARN expiry sweep skipped: binding store unreadable (same timeout)
08:55:55.722347  WARN no frames from engine within idle window; forcing reconnect
                       idle_timeout=60s
08:56:03.540050  INFO iii connected

After reconnect the same turn remained at running step 1. Readback found no pending function calls or child turns. The only observed work-tool action was read-only function discovery. An ordinary provider request had completed successfully; this is not evidence of a model-generation hang.

A subsequent explicit stop was acknowledged and recorded, but the turn remained running. A single explicit reconciliation wake also failed to terminalize it. Only a later operator-controlled cold restart cancelled/drained the old turn. No uncertain external operation was automatically repeated. That recovery is not a reliable product fix.

Source-level hypothesis

In harness/src/turn_loop.rs, fail_turn converts a state-read error to absence:

let record = crate::state::get_turn(...).await.ok().flatten();

It then converts a finalization failure to skipped(session_id):

finalize_failed(...).await.unwrap_or_else(|_| skipped(session_id))

skipped reports Running, next_step: None, skipped: true. Thus a dependency failure while trying to finalize can become indistinguishable to the caller from a benign no-op, without an established future terminalization step. The observed outage overlaps this path, but fault injection is still needed to prove the exact failure cut and the Engine/SDK cause of the original outage.

Expected behavior / useful regression coverage

  • Dependency unavailability during failure finalization should remain an explicit, recoverable failure to terminalize, not a silent running/no-next-step result.
  • Test disconnect/read timeout at get_turn, session finalization, and durable turn-write cuts; restore transport and verify an eventual truthful terminal observation through the owning lifecycle contract.
  • Test stop while failure finalization is unavailable, plus reconnect/re-registration and cold restart.
  • Preserve exact turn identity and cancellation ordering. Never replay an outcome-unknown external tool action merely to complete a turn.
  • Distinguish failed state persistence from a stale/terminal turn legitimately skipped.

No downstream Harness/Engine fork or restart watchdog has been introduced. This remains a reliability blocker for autonomous work through the affected runtime.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions