Skip to content

fix: atomically interrupt+resume wedged managed sessions on human reply #SUPERLOG - #441

Open
superlog-app[bot] wants to merge 1 commit into
mainfrom
superlog/fix-wedged-turn-interrupt-resume
Open

fix: atomically interrupt+resume wedged managed sessions on human reply #SUPERLOG#441
superlog-app[bot] wants to merge 1 commit into
mainfrom
superlog/fix-wedged-turn-interrupt-resume

Conversation

@superlog-app

@superlog-app superlog-app Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

When an agent run in awaiting_human state tries to resume, the Anthropic managed session may have an open tool event waiting for a response (a "wedged" state). The current repair path calls interruptOpenTurn() (sends user.interrupt) and then attempt() (sends user.message) as two separate API calls. Since Anthropic may not have finished processing the interrupt before the retry message arrives, the retry also fails with the same 400 "waiting on responses to events" error, causing the run to permanently fail with resume_failed.

This is the same race that recoverManagedAgentRun explicitly avoids with its comment: "Batch the interrupt and retry so there is no accepted-interrupt / lost-continuation gap between calls."

Root cause

In deliverResumeRepairingWedgedTurn, the two-step repair path:

await opts.interruptOpenTurn();           // call 1
return { kind: "delivered", outcome: await opts.attempt(), repaired: true };  // call 2

…creates a window where the Anthropic session may still be processing the interrupt when the retry message arrives.

Fix

Add an optional repairAttempt parameter to deliverResumeRepairingWedgedTurn. When provided, it replaces the two-step interruptOpenTurn + attempt with a single atomic callback — the closed-source backend implements this by batching [user.interrupt, user.message] in one events.send call.

Add interruptAndResume?(sessionId, message): Promise<void> to AgentRunnerBackend as the optional method that backends provide to enable this atomic path.

The existing two-step interruptOpenTurn + attempt path is kept as a fallback for runtimes that don't implement interruptAndResume.


Incident: 4a97a9aa-e753-43fb-ba0c-0d1638911c04


Was this PR helpful? Leave feedback — goes straight to the Superlog team.


Summary by cubic

Make human-reply resumes atomic for wedged managed sessions to prevent 400 “waiting on responses to events” and avoid permanent resume_failed runs. Uses a single batched provider call when available and falls back to the two-step path otherwise.

  • Bug Fixes
    • Added optional interruptAndResume?(sessionId, message) to AgentRunnerBackend to batch user.interrupt + user.message.
    • Extended deliverResumeRepairingWedgedTurn with repairAttempt and prefer it over interruptOpenTurn + attempt.
    • Updated resumeAgentRunFromHumanInput to call the atomic path when the backend provides it; otherwise use the existing two-step repair.
    • Added tests covering success and failure of the atomic repair path.

Written for commit 357d89e. Summary will update on new commits.

Review in cubic

…ly #SUPERLOG

Delivery-Id: 955651bfad744b1a631065c1c79bece842f5f4149add0965e95d9c1cbb6ca034
Delivery-Base: main

@superlog-app superlog-app Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Observability review

  • 1 blocking
  • 1 warning

}),
classifyError: (err) => runner.classifyDeliveryError?.(err) ?? "unknown",
interruptOpenTurn: interrupt ? () => interrupt(sessionId) : null,
// Preferred over two-step interrupt + resume: atomically interrupt the

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logs · blocking — Log error when atomic repairAttempt fails before returning failed result

Add an error log with sessionId, errorKind, and the caught error when repairAttempt throws, so operators can distinguish atomic-repair failures from two-step-repair failures without correlating raw traces. Without it, a permanent resume_failed caused by the new code path is silent at the log level.

Useful? React with 👍 / 👎.

Comment on lines +340 to +344
? () =>
resumeDurableAgentRun({
sessionId,
inputs: resumeInputs,
runner: {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

traces · warning — Preserve full runner context in the atomic repairAttempt span

The inline { resume: ..., steer: ... } object passed to resumeDurableAgentRun drops all other runner methods, including any OTel context carriers or span-enrichment hooks the full runner provides; pass the full runner with only resume overridden so the atomic path appears correctly in traces.

Suggested change
? () =>
resumeDurableAgentRun({
sessionId,
inputs: resumeInputs,
runner: {
resumeDurableAgentRun({
sessionId,
inputs: resumeInputs,
runner: {
...runner,
resume: (sid, msg) => interruptAndResume(sid, msg),
},
transitionToRunning: () => agentRunLifecycle.resumeRunning(resumeRunningOpts),
markProcessed,
})

Useful? React with 👍 / 👎.

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