Skip to content

fix: prevent interrupt mode from replaying previous assistant reply#50147

Closed
nano-jason wants to merge 1 commit into
openclaw:mainfrom
nano-jason:fix/interrupt-abort-replay-old-reply
Closed

fix: prevent interrupt mode from replaying previous assistant reply#50147
nano-jason wants to merge 1 commit into
openclaw:mainfrom
nano-jason:fix/interrupt-abort-replay-old-reply

Conversation

@nano-jason

Copy link
Copy Markdown

Fixes #50145

Problem

When messages.queue.mode is set to "interrupt", incoming messages that abort an active run cause the previous turn's assistant reply to be re-sent to the user before the new (correct) reply is delivered.

Root Cause

buildEmbeddedRunPayloads falls back to extractAssistantText(lastAssistant) when assistantTexts is empty. After an abort, assistantTexts is empty (model never generated new text), but lastAssistant still references the previous turn's message from messagesSnapshot (full session history). This stale text gets packaged as the current run's output and delivered to the user.

Fix

Two changes (defense in depth):

1. Guard lastAssistant fallback (run.ts)

When the run was aborted and produced no new assistant text, pass undefined instead of the stale lastAssistant:

lastAssistant:
  aborted && attempt.assistantTexts.length === 0 ? undefined : attempt.lastAssistant,

2. Early exit for aborted runs (agent-runner.ts)

  • Skip blockReplyPipeline.flush() when aborted (buffer may contain stale content)
  • Return early before payload delivery when wasAborted is true
const wasAborted = runResult.meta?.aborted === true;

if (blockReplyPipeline) {
  if (!wasAborted) {
    await blockReplyPipeline.flush({ force: true });
  }
  blockReplyPipeline.stop();
}

// ...

if (wasAborted) {
  return finalizeWithFollowup(undefined, queueKey, runFollowupTurn);
}

Testing

  1. Set messages.queue.mode: "interrupt"
  2. Send message A, then quickly send message B before A completes
  3. Verify: only one reply is delivered (combining context from both A and B), no stale replay of previous conversation

When messages.queue.mode is 'interrupt', aborting an active run causes
the previous turn's assistant reply to be re-sent to the user.

Root cause: buildEmbeddedRunPayloads falls back to
extractAssistantText(lastAssistant) when assistantTexts is empty.
After an abort, assistantTexts is empty but lastAssistant still
references the previous turn's message from session history.

Fix:
1. Guard lastAssistant fallback: pass undefined when aborted and no
   new assistant text was generated (run.ts)
2. Skip blockReplyPipeline flush and payload delivery entirely when
   the run was aborted (agent-runner.ts)

Fixes openclaw#50145
@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: XS labels Mar 19, 2026
@greptile-apps

greptile-apps Bot commented Mar 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a bug in interrupt queue mode (messages.queue.mode: "interrupt") where the previous turn's assistant reply was incorrectly replayed to the user before the new reply was delivered. The root cause was that buildEmbeddedRunPayloads fell back to extractAssistantText(lastAssistant) when assistantTexts was empty — a valid fallback for normal runs, but after an abort, lastAssistant referenced a stale message from the session history snapshot.

The fix applies two complementary guards:

  • run.ts: Sets lastAssistant to undefined when the run was aborted and produced no new assistant text, preventing the stale value from being used as a fallback in buildEmbeddedRunPayloads.
  • agent-runner.ts: Skips blockReplyPipeline.flush() for aborted runs (avoiding delivery of buffered stale content) and returns early via finalizeWithFollowup(undefined, ...) before payload delivery.

Notably, persistRunSessionUsage is still called before the wasAborted early return, correctly preserving token usage records for the aborted attempt. The finally block (typing.markRunComplete() / typing.markDispatchIdle()) executes regardless, so the typing indicator lifecycle is properly managed even when the early return fires. pendingToolTasks are also awaited before the abort check.

The only side effect worth noting is that incrementRunCompactionCount and diagnostics events are skipped for aborted runs, which is a reasonable tradeoff for a run that was intentionally interrupted.

Confidence Score: 5/5

  • This PR is safe to merge — the changes are narrowly scoped to the abort path in interrupt queue mode and introduce no new risk to the happy path.
  • Both changes are well-targeted: the run.ts fix is a single conditional guard on an existing argument, and the agent-runner.ts changes are positioned correctly after session-usage persistence and before payload delivery. The finally block ensures typing indicator cleanup is unconditional regardless of the early return. No regressions are expected on normal (non-aborted) runs since wasAborted is false there, keeping all existing code paths intact.
  • No files require special attention.

Last reviewed commit: "fix: prevent interru..."

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a1f0cdba66

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +489 to +490
if (wasAborted) {
return finalizeWithFollowup(undefined, queueKey, runFollowupTurn);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Don't drop timeout payloads on aborted runs

This early return regresses the timeout path. In src/agents/pi-embedded-runner/run/attempt.ts:2231-2238, a timeout sets aborted = true, and src/agents/pi-embedded-runner/run.ts:1624-1649 deliberately synthesizes a user-facing timeout error payload when no reply was generated. Because this branch exits before payload delivery, any auto-reply turn that hits timeoutMs now becomes a silent/no-reply turn again instead of surfacing the timeout notice.

Useful? React with 👍 / 👎.

Comment on lines +489 to +490
if (wasAborted) {
return finalizeWithFollowup(undefined, queueKey, runFollowupTurn);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep post-compaction bookkeeping when aborting a turn

Returning here skips the autoCompactionCount handling later in this function. src/auto-reply/reply/agent-runner-execution.ts:475-483 still records completed compactions even if the run is later aborted, so an interrupt that lands after compaction will now bypass both incrementRunCompactionCount() and the readPostCompactionContext() injection at src/auto-reply/reply/agent-runner.ts:679-707. The next queued turn can therefore resume from a compacted session without the AGENTS refresh/startup reminder that normally protects post-compaction correctness.

Useful? React with 👍 / 👎.

@clawsweeper

clawsweeper Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge.

Summary
The branch suppresses lastAssistant fallback for aborted empty embedded attempts and skips auto-reply flush plus payload delivery whenever runResult.meta.aborted is true.

Reproducibility: yes. Source inspection on current main shows interrupt mode aborts active work, an aborted empty attempt can retain an older lastAssistant, and payload delivery can still turn that fallback into a user-visible reply; I did not run a live channel repro in this read-only review.

Real behavior proof
Needs real behavior proof before merge: The PR body lists manual steps but provides no after-fix screenshot, recording, terminal output, logs, live output, or linked artifact showing the interrupt-mode behavior. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, ask a maintainer to comment @clawsweeper re-review.

Next step before merge
Manual follow-up is needed because the external PR lacks real behavior proof and the current patch has two correctness blockers rather than a narrow automation-only repair.

Security
Cleared: The diff only changes TypeScript reply-control flow and does not touch dependencies, workflows, package scripts, secrets, permissions, or downloaded code.

Review findings

  • [P1] Preserve timeout payload delivery — src/auto-reply/reply/agent-runner.ts:483-489
  • [P2] Keep compaction bookkeeping before abort exit — src/auto-reply/reply/agent-runner.ts:483-489
Review details

Best possible solution:

Keep the narrow stale lastAssistant suppression, but make any silent aborted-run exit specific to superseded interrupt runs with no new output while preserving timeout/error payload delivery, compaction accounting, and focused regression coverage.

Do we have a high-confidence way to reproduce the issue?

Yes. Source inspection on current main shows interrupt mode aborts active work, an aborted empty attempt can retain an older lastAssistant, and payload delivery can still turn that fallback into a user-visible reply; I did not run a live channel repro in this read-only review.

Is this the best way to solve the issue?

No. The lastAssistant guard is the right direction, but treating every meta.aborted result as undeliverable is too broad because timeout and compaction paths intentionally carry useful outcomes on aborted metadata.

Full review comments:

  • [P1] Preserve timeout payload delivery — src/auto-reply/reply/agent-runner.ts:483-489
    Timeout aborts intentionally return a user-facing timeout payload with meta.aborted: true. This early return exits before reply payload delivery, so an auto-reply turn that hits timeoutMs would become silent instead of sending the timeout notice.
    Confidence: 0.92
  • [P2] Keep compaction bookkeeping before abort exit — src/auto-reply/reply/agent-runner.ts:483-489
    This return runs before autoCompactionCount persistence and post-compaction context refresh. An aborted run that already completed compaction would skip incrementRunCompactionCount, queued-session refresh, and next-turn context injection.
    Confidence: 0.88

Overall correctness: patch is incorrect
Overall confidence: 0.9

Acceptance criteria:

  • pnpm test src/agents/pi-embedded-runner/run/payloads.test.ts src/agents/pi-embedded-runner/run.overflow-compaction.loop.test.ts src/auto-reply/reply/agent-runner-execution.test.ts src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts src/auto-reply/reply/get-reply-run-queue.test.ts src/auto-reply/reply/abort.test.ts
  • pnpm check:changed

What I checked:

Likely related people:

  • steipete: Current-line blame and recent file history place the active embedded payload, reply delivery, timeout, and compaction code paths under Peter Steinberger’s recent work. (role: recent area contributor; confidence: high; commits: 8fd0f9965e24, 83a108080de7; files: src/agents/pi-embedded-runner/run.ts, src/agents/pi-embedded-runner/run/payloads.ts, src/auto-reply/reply/agent-runner.ts)
  • Sanjays2402: Commit 1f06dbd04ced added final-answer recovery in buildEmbeddedRunPayloads, the fallback that can surface stale lastAssistant text when current streamed text is empty. (role: introduced relevant fallback behavior; confidence: medium; commits: 1f06dbd04ced; files: src/agents/pi-embedded-runner/run/payloads.ts)
  • doedewaldt: Commit 18f4a2d98723 recently added explicit LLM idle timeout error surfacing, which the proposed blanket aborted-run return would bypass before delivery. (role: recent timeout-path contributor; confidence: medium; commits: 18f4a2d98723; files: src/agents/pi-embedded-runner/run.ts)
  • vyctorbrzezowski: Commit d5c67d712ebb recently changed the post-compaction context surface that the proposed early return would skip. (role: recent post-compaction context contributor; confidence: medium; commits: d5c67d712ebb; files: src/auto-reply/reply/agent-runner.ts)

Remaining risk / open question:

  • The external PR has no after-fix real behavior proof from an interrupt-mode setup.
  • This read-only review did not run a live multi-channel interrupt reproduction; the reproduction confidence comes from source and test-path inspection.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 6a589017cab2.

@clawsweeper clawsweeper Bot added P2 Normal backlog priority with limited blast radius. impact:message-loss Channel message delivery can be lost, duplicated, or misrouted. impact:session-state Session, memory, transcript, context, or agent state can drift or corrupt. labels May 17, 2026
@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 17, 2026
@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. and removed impact:session-state Session, memory, transcript, context, or agent state can drift or corrupt. impact:message-loss Channel message delivery can be lost, duplicated, or misrouted. labels May 18, 2026
@openclaw-barnacle

Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Jun 2, 2026
@openclaw-barnacle

Copy link
Copy Markdown

Closing due to inactivity.
If you believe this PR should be revived, post in #clawtributors on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. P2 Normal backlog priority with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: XS stale Marked as stale due to inactivity status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

interrupt queue mode replays previous assistant reply after abort

1 participant