Skip to content

Hardening: reconcile seam is unreachable for stalled sessions — a finished-but-lost-Stop session cannot be recovered as completed #61

Description

@pbean

Description

Follow-up surfaced while validating #53 (fixes #48). With Stop/window-death as the only authoritative completion signals, one recovery scenario is left with no automated path:

A dev/review session that genuinely finished its work (terminal artifact written, frontmatter done) but whose final Stop event was lost, and which then fails to answer any stall nudge, now ends status="stalled" with result_json=None (_final(accept_result=False), adapters/generic.py). That verdict is the right call at the adapter layer — an artifact under a live window that ignores nudges is untrustworthy by construction (#48). The gap is downstream:

  • _reconcile_generic_terminal_status (engine.py ~1576) is called from exactly one site, inside if result.status == "completed": in _dev_phase (~1271). It is structurally unreachable for stalled/crashed/timeout.
  • decide_dev (escalation.py ~71) treats every non-completed status identically: RETRY while budget remains, else DEFER/PAUSE. On RETRY the verify outcome is always None (the whole verify block is completed-gated), so the engine goes straight to _rollback_or_pause.

Net effect: finished-but-unvouched work is never inspected before disposal. Current mitigations are real but passive — scm.rollback_on_failure defaults OFF (pause for manual recovery, tree untouched), and when it is ON, _preserve_attempt_commits/_preserve_attempt_worktree park the work under attempt-preserve/* refs. But nothing ever re-reads the on-disk state to notice the session actually succeeded.

Likelihood

Low: it requires a lost/undelivered Stop and total nudge-unresponsiveness (default 2 nudges × dev_stall_grace_s). A responsive agent answers a nudge, ends its turn, and the fresh Stop completes normally. But hook-delivery failure modes (hook misconfiguration, events.jsonl write failure, hard-wedged CLI after finishing) make it a plausible long-tail on flaky environments — precisely where the native-Windows backend will live.

Proposed direction

A guarded post-kill reconcile for the stalled verdict — trust is re-established by window death rather than prose:

  1. Engine kills the session as today (run()'s finally).
  2. For a stalled result only, after the kill (window now provably dead), re-run the artifact scan (find_result_artifact + synthesize_result) against the session's launch floor.
  3. If the artifact parses terminal-done AND the frontmatter agrees (status_consistent), treat as completed: run the normal completed-path verify/reconcile/sync instead of RETRY-rollback. Any disagreement → keep the stall verdict.

This preserves #48/#53's invariant — nothing completes while the window is alive — while restoring recovery for the lost-Stop tail. Alternative (cheaper, weaker): keep the stall verdict but skip the destructive _rollback_or_pause when a consistent terminal artifact is detected post-kill, deferring the story with the tree intact for the next reconcile-capable pass.

Which area is this for?

Engine / escalation

Refs #48, #53.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions