Add crash-safe tool execution journaling and interrupted-turn recovery - #4
Open
Charlie-Wang-03 wants to merge 11 commits into
Open
Add crash-safe tool execution journaling and interrupted-turn recovery#4Charlie-Wang-03 wants to merge 11 commits into
Charlie-Wang-03 wants to merge 11 commits into
Conversation
Charlie-Wang-03
marked this pull request as ready for review
September 9, 2026 05:41
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Moonshine can resume a session's message history, but it currently cannot distinguish a cleanly completed tool-bearing turn from a process interruption inside tool execution.
That creates a side-effect safety gap: a tool may have completed externally while its result or the enclosing turn was never durably completed. A resumed model can then request additional tools without knowing whether replay would duplicate prior external effects.
This PR adds a generic tool-execution journal with a deliberately conservative recovery contract:
ok/errormarker before dispatching the next tool.turn_completed.handle_function_callsresult contract.This does not claim exactly-once execution. The contract is instead:
Problem
Crash window A: interruption inside a multi-tool batch
Previously, the batch could leave the normal result-persistence path without a generic durable execution-state contract.
The runtime could not reliably distinguish:
Crash window B: handler finishes before the turn commits
A completed handler does not prove that the enclosing agent turn was durably completed.
If the resumed provider context does not reconstruct that exact interrupted tool trajectory, automatically allowing another tool request can duplicate an external side effect.
Design contract
The recovery policy is intentionally conservative:
If Moonshine cannot prove that this lifecycle completed cleanly, later tool dispatch for the same durable session is blocked.
The runtime therefore prefers a visible fail-closed state over silent replay of an operation that may already have produced an external side effect.
This PR deliberately does not introduce:
Those would require a separate, stronger execution contract.
Implementation
agent_runtime/execution_journal.pyAdds a dedicated
ToolExecutionJournalruntime component with four lifecycle events:tool_execution_startedtool_execution_finishedtool_execution_ambiguoustool_execution_blockedThe journal uses the existing session conversation-event store rather than introducing another persistence backend.
Large tool arguments and outputs are not duplicated into the journal. Bounded previews and SHA-256 fingerprints are stored instead.
Durable dispatch boundary
For each executable call:
A normal tool exception remains a terminal error and does not poison later dispatch.
For process-level interruption such as
KeyboardInterrupt, Moonshine records the execution as ambiguous on a best-effort basis and re-raises the original interruption.A hard process kill may prevent the explicit ambiguous event itself from being written, but the unmatched durable
tool_execution_startedrecord is sufficient for recovery detection after restart.Interrupted-turn detection
Execution completion alone is not sufficient because the process may fail after the handler finishes but before the enclosing turn commits.
The journal therefore also derives a monotonic turn sequence from the existing:
turn_startedturn_completedevents.
Completed resumed turns are paired LIFO with starts. This prevents a later successfully completed turn from accidentally hiding an older interrupted tool-bearing turn and avoids relying on wall-clock timestamp precision.
Fail-closed recovery
If Moonshine detects either:
future tool dispatch in that session is blocked.
The provider receives a structured
blocked_interrupted_executionerror instead of executing the handler.A resumed turn can still answer without tools; only potentially unsafe additional dispatch is prevented.
Compatibility
handle_function_callsretains its existing result keys:If no durable
session_store/session_idis available, dispatch retains its legacy behavior.The change is therefore scoped to durable-session recovery without changing the non-durable call contract.
Deterministic regression coverage
tests/test_interrupted_tool_recovery.pyadds five offline tests requiring no real LLM or API:tool_execution_startedblocks dispatch afterSessionStorerestart.RuntimeErroris terminal and does not poison later tool dispatch.Validation on current upstream
PR #4 is refreshed onto current upstream
main:The refresh used a normal merge commit. No rebase, history rewrite, or force push was used.
Validation was run on GitHub Actions with Ubuntu 24.04 / Python 3.11.16.
Moonshine validation run:
https://github.com/Charlie-Wang-03/Moonshine/actions/runs/34313956509
Focused checks
This verifies that the execution journal coexists with the provider/tool-call protocol changes already merged through PR #1.
Current-main vs PR-branch full-suite A/B
Current
main(bb0e4aa):This PR (
af181ed):Therefore the branch adds exactly five passing deterministic recovery tests while preserving the same existing full-suite failure/error counts as current
main.No additional full-suite failure or error is introduced by this PR.
The existing 43 failure/error baseline is current upstream test/code drift being addressed separately in PR #3.
Downstream compatibility: Creative-Intelligence runner v2
The PR head was also validated against current
DeepMathLLM/Creative-Intelligencemain:The validation used Creative-Intelligence's deterministic real-runtime Moonshine integration suite.
Only the integration workflow was temporarily pinned to the PR #4 head; no Creative-Intelligence production logic was changed and no downstream upstream PR was created.
Downstream validation run:
https://github.com/Charlie-Wang-03/Creative-Intelligence/actions/runs/34313995187
Result:
The suite exercises the real Moonshine runtime while faking only provider responses and covers existing downstream contracts around:
No downstream compatibility bug was found.
Relationship to existing PRs
PR #1 — merged
PR #1 fixes provider-protocol validity around orphaned
tool_callswhen the tool-round limit is reached.Its regression suite passes together with this PR:
There is no direct file overlap with this PR.
PR #2 — merged
PR #2 closes SQLite connections after store operations and is already part of the current upstream base.
No additional work from PR #2 is duplicated here.
PR #3 — still open
PR #3 addresses broader upstream test/code drift across research workflow, retrieval, tool exposure, prompts, session infrastructure, and related behavior.
Its current diff has no direct file overlap with this PR.
There is one complementary semantic area: PR #3 writes ordinary
tool_resultconversation events, while this PR records execution-lifecycle state:These serve different contracts:
tool_result: result/trajectory traceabilityNo PR #3 code is pulled into this PR.
Diff scope
The effective PR diff remains limited to:
agent_runtime/execution_journal.pymodel_tools.pytests/test_interrupted_tool_recovery.pyNo CI workflow, downstream production change, retry framework, or unrelated cleanup is included.
Downstream motivation
Creative-Intelligence already contains workload-specific recovery logic around persisted verifier results and archive publication.
The underlying question is generic:
That contract applies to any Moonshine workflow, so recovery belongs in the Moonshine runtime rather than in downstream-specific workarounds.
Limitations
This PR deliberately does not attempt to provide exactly-once semantics or automatic recovery of arbitrary side-effecting tools.
Safe automatic replay would require an additional contract such as:
The present change establishes the prerequisite runtime invariant: