Skip to content

LOU-U7, LOU-U8, LOU-U9: durable execution gaps (approval mid-batch, resume with new input, checkpoint after each LLM turn) - #68

Merged
LinuxDevil merged 12 commits into
mainfrom
lou-u7-u8-u9-durable-execution
Oct 1, 2026
Merged

LinuxDevil merged 12 commits into
mainfrom
lou-u7-u8-u9-durable-execution

Conversation

@LinuxDevil

Copy link
Copy Markdown
Owner

What

Three durable-execution correctness gaps, fixed together because they share one mechanism: on start, AgentExecutor now finds tool calls of the transcript's last model turn that have no result (splitPendingTurn(), new src/execution/transcript.ts) and runs exactly those through the normal LOU-V3 batch path before calling the model.

  • LOU-U7 - approval mid-batch. ExecutionSnapshot.remainingToolCalls records the calls after the paused one. resumeAfterApproval() records the paused call's result (approved) or a structured { error, note } rejection, then the executor runs the remaining calls. They can run, fail validation, or pause again with a new approval, so the chain keeps going. The resumed run now uses the same approvalStore by default. Snapshots saved before this change (no field) resume the way they did: later calls do not run. Each of them gets a "not run" error result so the transcript is valid.
  • LOU-U8 - resume with new input. Checkpoint.status: 'in-progress' | 'awaiting-approval' | 'finished' (plus approvalId):
    • (a) unfinished: resumes. New input is queued behind the pending tool results (never between a tool-call turn and its results). It survives another approval pause or crash because it rides at the end of the checkpoint/snapshot. Input that re-sends the run's starting message (the natural crash retry) is not appended again; input: [] just resumes.
    • (b) finished: the checkpoint is kept (it used to be deleted) and the session continues as a conversation. A caller that re-sends the full history is not duplicated. steps, usage and toolCalls restart at zero.
    • (c) awaiting approval: throws the new exported SessionAwaitingApprovalError (sessionId, approvalId). It names resumeAfterApproval.
  • LOU-U9 - checkpoint after each model turn. The assistant tool-call turn is checkpointed before any tool runs. A crash then resumes by running the missing calls without re-calling the model. Tools now get toolCallId in their execute options (non-sandbox path; the sandbox path and messages stay with LOU-U15) to use as an idempotency key.

Why queue instead of reject for (a)

Rejecting would break the most valuable case: interrupt-and-redirect, where the user aborts and sends a correction. Queueing is always provider-valid because the message is placed after the pending results. Treating the starting message as a retry keeps "call execute() again with the same request after a crash" working without duplicating the question.

Behaviour changes (tests updated, called out)

  • A finished run's checkpoint is kept as 'finished' instead of deleted. Tests that asserted deletion or "fresh input on a reused session" were changed: AgentExecutor.test.ts, resume.test.ts, kvCheckpointStore.test.ts, cloudflare.checkpoint.test.ts, cancellation.test.ts, and agent-forge runRegistry.test.ts. The known-failing agent-forge test stop() then run() resumes from the last checkpoint now passes.
  • Checkpoint save counts went up (one extra save per model turn, plus the finished save).
  • The test that asserted later batch calls are not run after resume (parallelToolCalls.test.ts) now asserts they run once, in call order.
  • Tools' execute options now include toolCallId. Three toHaveBeenCalledWith(args, {}) assertions were updated, one of them in agent-forge compileHooks.test.ts.

How verified

On the merged result (origin/main d2cdf24 merged at 5407ed2):

  • npx tsc --noEmit: exit 0
  • npm run lint: ✖ 433 problems (0 errors, 433 warnings) (same count as main; none in touched files)
  • npm run build && npm run build --workspace=packages/create-loushy-agent: exit 0
  • npm run test:coverage: Test Files 130 passed (130), Tests 1921 passed | 4 skipped (1925). Earlier runs hit load-related timing flakes in http.test.ts, guardrails.test.ts and sandbox.test.ts; they pass in isolation and on rerun.
  • npm run fallow: exit 0, ✓ No issues found, ✓ 0 above threshold
  • npm run test:types: 19 passed, no type errors
  • npm run docs:llms:check: exit 0
  • npm run docs:verify-snippets: all 80 snippet(s) type-check against src/; 6 also run cleanly
  • agent-forge: typecheck and typecheck:server exit 0; npx vitest run 108 passed; npm run test:server 105 passed

New tests are in src/execution/durableExecution.test.ts (mockModel + KVCheckpointStore + a JSON round-tripping approval store, with simulated crashes via PropagatingToolError and a failing store write). They cover:

  • one result per call, in call order;
  • no call executed twice (except the documented at-least-once re-run of a call that was running at the crash);
  • model.calls.length proving the model is not re-called;
  • approve, reject, chained approvals, validation failure and legacy snapshots;
  • U8 (a) queued, (a) abort-redirect, (b) continue, (b) full-history dedupe and (c) error then continue;
  • queued input surviving a second approval pause.

Store round-trips: checkpoint.test.ts, kvCheckpointStore.test.ts, ApprovalGate.test.ts. The SQLite store keeps opaque JSON and is already covered by its contract tests.

Notes for reviewer

  • AgentExecutor.ts changes stay focused: pending-call resume, runBatch split out of runToolCalls, checkpoint after the model turn, status-aware pause/finish. When merging LOU-V2 (agent.stream()), runStepOrAbort became generic over a step thunk, so resumed pending calls get the same step events.
  • main's README carried literal conflict markers from the LOU-V2 merge. They were removed in my merge commit (Streaming, Sessions and Project-instructions bullets all kept).
  • Docs: new docs/durable-execution.md, covering what is checkpointed when, resume semantics, the at-least-once caveat with a toolCallId idempotency example, compatibility and limits. Also short updates to docs/api-overview.md, docs/sessions.md and README.
  • Files touched outside src/execution: agent-forge runRegistry.ts/abortableProvider.ts (doc comments only) and two agent-forge tests.
  • Possible follow-up: resumeAfterApproval() without checkpointStore leaves the session marked awaiting-approval. This is documented, but could be detected.

🤖 Generated with Claude Code

LinuxDevil and others added 12 commits October 1, 2026 10:30
LOU-U7: an approval in the middle of a tool batch kept the transcript
invalid - calls after the paused one never ran and got no result. The
approval snapshot now records `remainingToolCalls`; resumeAfterApproval()
records the paused call's result (or rejection) and AgentExecutor runs the
remaining calls through the normal batch path, so they can run, fail
validation or pause again (the resumed run defaults to the same
approvalStore). Snapshots without the field resume as before (later calls
do not run) but each unanswered call gets a "not run" error result.

LOU-U8: execute() with a sessionId now honours new input. Checkpoints carry
a `status`: an unfinished run resumes and new input is queued behind its
pending tool results (a re-sent starting message is a retry, not appended);
a finished run is kept (no longer deleted) and continues as a conversation;
a run awaiting approval throws SessionAwaitingApprovalError.

LOU-U9: the model's tool-call turn is checkpointed before any tool runs,
and a resumed transcript whose last turn has unanswered calls runs exactly
those calls without calling the model. Tools receive `toolCallId` in their
execute options (non-sandbox path) as an idempotency key for the
at-least-once re-run.

Tests that encoded the old behaviour (checkpoint deleted on finish, input
ignored on resume, later batch calls not run, empty execute options,
checkpoint save counts) were updated. Docs: docs/durable-execution.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolves overlaps with LOU-V2 (agent.stream()): the resumed pending tool
calls are bracketed by the same step events, and pauseForApproval writes the
awaiting-approval checkpoint before emitting approvalRequested. Also removes
the conflict markers main's README carried.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-execution

Keeps both intents: checkpoints carry status/approvalId and LOU-V5 usage;
approval snapshots carry remainingToolCalls and usage; resumed pending and
remaining tool calls run through the same batch path, so they get the same
accounting, and a resumed run continues the stored totals (a new turn of a
finished session starts from zero). ToolRunContext gains toolCallId, so the
tool execute options carry toolCallId, abortSignal and onDelegatedUsage.
New tests assert usage continuity across crash-resume and approval resume.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
runRegistry's synthetic flow result and debugController.test's onLLMResponse
call predate LOU-V5's RunUsage / three-argument onLLMResponse (main's
agent-forge server typecheck failed on them); compileHooks.test now matches
the tool options with objectContaining since they also carry onDelegatedUsage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Takes main's agent-forge LOU-V5 fixes (emptyRunUsage, objectContaining) over
the equivalent ones on this branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ecution

Resolves conflicts between the durable-execution changes and main's
sub-agent inheritance work:

- AgentExecutor: approval snapshots store the base agent (main) and the
  queued input at the end (LOU-U8); savePause writes the approval record
  first, then the 'awaiting-approval' checkpoint, for tool and sub-agent
  pauses alike. The sub-agent suspension list now lives in runBatch,
  which the durable-execution branch split out of runStep.
- resume: the sub-agent resume path (LOU-Y1) is kept and the turn's
  remaining calls are still closed/left pending per LOU-U7; the approved
  tool gets toolCallId (LOU-U9) alongside its sub-agent scope.
- toolCallExecution: tools get both onDelegatedUsage and toolCallId.
- subagentRuntime: a run paused on a sub-agent keeps queued input in its
  snapshot, like a run paused on a tool.
- README/llms-full.txt: keep the durable-execution link and main's
  "Sub-agents and delegation" heading.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3s6hrJ2zhYHbSjvVCSEjZ
@LinuxDevil
LinuxDevil merged commit 320fd29 into main Oct 1, 2026
2 checks passed
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.

2 participants