Skip to content

fix: salvage REPL state on mid-block exceptions instead of discarding it#172

Open
mehul79 wants to merge 1 commit into
alexzhang13:mainfrom
mehul79:main
Open

fix: salvage REPL state on mid-block exceptions instead of discarding it#172
mehul79 wants to merge 1 commit into
alexzhang13:mainfrom
mehul79:main

Conversation

@mehul79

@mehul79 mehul79 commented Jul 4, 2026

Copy link
Copy Markdown

What this fixes

When code running in a REPL block throws an exception partway through, every environment (local, docker, daytona, e2b, modal, prime) discards all variables that block had already assigned — even ones from expensive work like a loop of llm_query/rlm_query sub-calls. The model has no way to know earlier sub-calls succeeded, so it usually redoes the whole loop next turn, re-paying for calls it already made.

The fix

On an exception, save the variables that were assigned before the crash into the persistent namespace (the same copy the success path already does), and tell the model in stderr that this happened. Now a failed loop can resume where it left off instead of starting over.

ipython_repl.py didn't need this — it runs cells through a real IPython kernel, which already keeps its namespace after a failed cell.

Testing

  • ruff check / ruff format --check: clean.
  • Full test suite: passes, no new failures.
  • Added 3 tests to tests/repl/test_local_repl.py: prior variables survive a raise, scaffold names still restore after a failure, and a loop that fails on call 8 of 10 resumes from call 8 instead of redoing 1-7.

Every REPL-style environment (local, docker, daytona, e2b, modal, prime)
executes model code as `exec(code, combined, combined)` and, on success,
copies newly-assigned variables from `combined` into the persistent
`_locals`/`self.locals` namespace. On an exception mid-block, none of them
did this copy — any variables the code had already assigned (e.g. partial
results from a loop of `llm_query`/`rlm_query` sub-calls) were silently
discarded, forcing the model to redo already-completed, already-paid-for
sub-calls on its next turn.

This salvages `combined` into the persistent namespace in the except branch
too (same logic as the success path), restores scaffold names, and appends
a note to stderr so the model knows prior work survived and it can resume
a partially-completed loop instead of re-running it from scratch.

ipython_repl.py is unaffected — it executes cells through an actual IPython
kernel, which already retains its namespace across a failed cell.

Adds 3 regression tests to tests/repl/test_local_repl.py covering: prior
assignments surviving a mid-block raise, scaffold names still restored
after a failure, and an integration-style case where a loop that fails on
its 8th of 10 sub-calls resumes from item 8 on the next block instead of
re-paying for items 1-7.
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