Skip to content

fix(planner): attributable step failures, safe narration, headless exit prompt - #212

Open
lfnothias wants to merge 5 commits into
mimosa_v2from
propose/planner-step-attribution
Open

fix(planner): attributable step failures, safe narration, headless exit prompt#212
lfnothias wants to merge 5 commits into
mimosa_v2from
propose/planner-step-attribution

Conversation

@lfnothias

Copy link
Copy Markdown
Collaborator

Slice 4 of 4 from #197. Five commits cherry-picked unchanged from mimosa_v2_lfx. Each fix names the run it was reproduced from.

Problem

  • Phantom timeouts. run_cached treated completed=False as a timeout, but the two lines recording the caught exception were commented out. Every agent-level crash was reported as a five-hour timeout two minutes into the run, with the real exception discarded.
  • Exit code. A CSV run whose every row failed exited 0, so a harness scoring by exit code counted it as a pass.
  • Stale imports under pytest. pyproject force-includes main.py and config.py as top-level modules, so an installed copy in site-packages won every import during tests.
  • Unattributable step failures. The handler reported Critical error in step execution: unhashable type: 'slice' with no type, file or traceback; the planner had no logger at all.
  • Narration failing the step. The end-of-step TTS summary sliced each answer, but answers are dicts, so a run that had scored 0.760 and exported its capsule died on the summary and was reported as 0%.
  • Blocking prompt on the batch path. request_user_exit called input() with stdin redirected and died with EOF when reading a line. The question should not have been asked: _verify_expected_outputs compared a declared directory against a scan that yields files only, so the step was permanently "missing outputs".

Solution

  • Worker-thread liveness decides timeout versus crash; the real exception is re-raised.
  • papers_mode counts errored rows and exits 1; conftest.py puts the tree ahead of site-packages.
  • Failing steps log the traceback and name the exception type, keeping from e.
  • Answers are coerced before narration, and narration runs inside its own guard.
  • request_user_exit raises UserInterventionRequired when stdin is not a TTY, so the CSV harness still counts the row and prints its summary. A declared directory is satisfied by any file inside it.

Testing

Fresh uv sync --group dev --python 3.11 on this branch:

17 failed, 273 passed, 4 skipped

The 17 failures are the pre-existing set from #205. This slice adds 44 passing tests: agent_failure_reporting_test, papers_mode_exit_code_test, planner_step_error_test, planner_narration_test, headless_planner_test.

Backwards compatibility

Interactive runs still get the prompt. #206 defines the same UserInterventionRequired class at the same position, so the two merge cleanly in either order (checked with git merge-tree); the guards cover different functions (request_user_exit here, plan approval there).

🤖 Generated with Claude Code

lfnothias and others added 5 commits September 2, 2026 12:20
run_cached runs the agent on a worker thread, joins with a deadline, and treats
completed=False as a timeout. The retry loop caught every exception but the two
lines recording it were commented out:

    #result['exception'] = e
    #result['completed'] = True

So an agent whose three attempts all raised left completed=False and
exception=None. The thread exited, join() returned immediately, and the caller
raised

    TimeoutError: Agent 'reconstructor' execution timed out after 18000 seconds

about two minutes into the run, with the real exception discarded — visible
only as a bare print() inside the worker. Every agent-level crash was
misreported as a five-hour timeout.

The discriminator is whether the worker is still alive after the join: alive
past the deadline is a genuine timeout; finished without completing means every
retry raised, so re-raise the last one. A successful attempt clears any earlier
retry's exception, since the caller re-raises whatever is left in the slot.

Observed against stealth/ox-alpha running ASB capsules, where the underlying
cause was the temperature-400 fixed in 6499f03 — invisible behind the timeout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 3cfeaf2)
…nstall

Exit code. run_single_thread_eval_loop catches per-row exceptions and continues
— right for a batch — recording success_level: "Error" in execution_history.
Nothing read it, so a run whose every row failed exited 0. Observed: a task
logging "Error in csv row 1: Planner: Execution failed" twice, exit 0. Any
harness scoring by exit code counts that as a pass. papers_mode now reports the
count and exits 1.

Test isolation. Writing the test above surfaced a worse problem. pyproject
force-includes main.py and config.py as top-level modules, so installing the
project drops copies into site-packages, and under pytest those copies won
every import:

    import config  ->  .venv/lib/python3.11/site-packages/config.py  (Aug 14)

sources/ is not shipped that way and correctly resolved to the tree, which is
why only main/config were affected — and why it went unnoticed. The effect is
that config_roundtrip_test.py, whose whole purpose is to check which fields
survive dump/load, was checking an install-time snapshot: fields added in the
working tree were invisible to it and it passed regardless.

conftest.py now puts the repo root at the front of sys.path. config_roundtrip
consequently exercises real code for the first time in a while, and still
passes, including over the perspicacite_* fields added in 6499f03.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 07f7c1b)
The step-execution handler wrapped the cause as

    raise Exception(f"❌ Critical error in step execution: {str(e)}") from e

`from e` preserves the chain for a Python caller, but the operator only ever
sees the formatted string. A real run ended with

    ❌ Critical error in step execution: unhashable type: 'slice'

— no file, no line, no exception type, and nothing in the log to grep for. The
run had already produced its 13.9 kB deliverable and written its ASTRA capsule,
so the bare message made a late failure look like a total loss.

Now logs the traceback and names the exception type, keeping `from e`.

Planner had no logger at all — `self.logger` appeared nowhere in the file — so
logging the traceback needed one added to __init__ first. Without that the fix
would have raised AttributeError from inside an except block, which is a worse
failure than the one it was meant to explain.

The underlying TypeError is not fixed here: it is a dict indexed with a slice
somewhere under run_attempts, and it is not locatable from the message alone.
That is the point — this change is what makes the next occurrence diagnosable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit ec41bbc)
The p_iimn run of 2026-08-22 (20260822_183340_88cdd1fe) scored 0.760,
wrote reproduction_iimn.md and exported its ASTRA capsule, then died with

    Critical error in step execution: unhashable type: 'slice'

and was reported as a 0% success rate with a non-zero exit.

The cause was the TTS summary at the end of run_attempts, which sliced
each element of final_answers. Agents do not answer with strings: every
entry of that run's state_result.json is a dict, so x[:128] indexed a
dict with a slice. Reproduced from the persisted answers on the project
interpreter. On 3.12+, where slices became hashable, the same line
degrades to a KeyError instead — the fix covers both.

Every other consumer already coerced first (planner.py ~397,
evolution_engine.py ~245); this one did not, and the annotations said
list[str] while the data said list[dict]. Corrected both.

The second defect is that the narration sat in the step's success path
at all. A line that is spoken aloud must never decide whether the work
counts, so it moves to _narrate_step_completion and is caught there —
logged with its traceback, not swallowed.

Also anchors the exception-chaining test on the raise rather than on the
message text, which now appears in prose earlier in the module.

(cherry picked from commit 501742f)
The p_iimn re-run of 2026-08-22 reached step 4 of 6 and died with

    ❌ Planner: Execution failed: EOF when reading a line

Two defects, one behind the other.

request_user_exit called input() on the benchmark path. In a batch run
stdout is redirected and stdin is not a TTY, so the prompt printed into a
log and the read raised EOFError — a message naming neither the question
nor the step. It now raises UserInterventionRequired carrying the
question. Raising rather than exit(1) is deliberate: the CSV harness
counts the row and still prints its summary, which SystemExit from inside
the planner would skip. pricing.py and csv_mode._prompt_with_default were
already headless-safe; this was the last blocking prompt.

It should never have been asked. data_acquisition declared
/workspace/data/ as an output and wrote nine files into it, but
_verify_expected_outputs compares against a scan that yields files only,
so a directory could never match. The step was permanently "missing
outputs" and blocked every step downstream. A declared directory is now
satisfied by any file inside it, matched on the trailing directory name
because plans declare workspace-absolute paths while the scan returns
relative ones.

15 tests, including that an interactive yes still continues and an
interactive no still exits. Suite: 346 passed, the same 16 pre-existing
failures.

(cherry picked from commit 67b4251)
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