openclaw - 💡(How to fix) Fix Restart recovery should optionally deliver resumed final replies for channel sessions

Official PRs (…)
ON THIS PAGE

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

When OpenClaw resumes an interrupted main session after a gateway restart, the recovery path currently calls the agent with deliver: false. For channel-originated sessions that still have a valid delivery context, the final resumed reply is written to the session/logs but is not delivered back through the original channel.

This caused a recovered reply in a bncr direct session to appear in the gateway/session output without any corresponding bncr outbound/outbox records.

Error Message

After a gateway restart:

Root Cause

Because deliver is false, the resumed final output does not enter the normal channel delivery path and does not get handed back to the plugin's outbound queue.

Code Example

[bncr] outbound ...
[bncr] outbox push ...
[bncr] outbox ack ok ...

---

await callGateway({
  method: "agent",
  params: {
    message: buildResumeMessage(sanitizedPendingText),
    sessionKey: params.sessionKey,
    idempotencyKey: crypto.randomUUID(),
    deliver: false,
    lane: "main"
  },
  timeoutMs: 1e4
});

---

const shouldDeliverRecoveredFinal = hasRecoverableDeliveryContext(entry);

await callGateway({
  method: "agent",
  params: {
    message: buildResumeMessage(sanitizedPendingText),
    sessionKey: params.sessionKey,
    idempotencyKey: crypto.randomUUID(),
    deliver: shouldDeliverRecoveredFinal,
    lane: "main"
  },
  timeoutMs: 1e4
});

---

restartRecovery.deliverResumedFinal = true | false
RAW_BUFFERClick to expand / collapse

Summary

When OpenClaw resumes an interrupted main session after a gateway restart, the recovery path currently calls the agent with deliver: false. For channel-originated sessions that still have a valid delivery context, the final resumed reply is written to the session/logs but is not delivered back through the original channel.

This caused a recovered reply in a bncr direct session to appear in the gateway/session output without any corresponding bncr outbound/outbox records.

Observed behavior

After a gateway restart:

  1. The interrupted main session was detected from stale transcript locks.
  2. Restart recovery resumed the session.
  3. The assistant produced the final response.
  4. The response appeared in gateway/session output.
  5. The original bncr channel did not receive it.

For normal bncr delivery we see records like:

[bncr] outbound ...
[bncr] outbox push ...
[bncr] outbox ack ok ...

For the resumed response, none of those records were present. Subsequent bncr messages delivered normally, so the bncr client/outbox/target were healthy.

Current code path

In the built runtime, resumeMainSession(...) hard-codes delivery off:

await callGateway({
  method: "agent",
  params: {
    message: buildResumeMessage(sanitizedPendingText),
    sessionKey: params.sessionKey,
    idempotencyKey: crypto.randomUUID(),
    deliver: false,
    lane: "main"
  },
  timeoutMs: 1e4
});

Because deliver is false, the resumed final output does not enter the normal channel delivery path and does not get handed back to the plugin's outbound queue.

Why this matters for channel plugins

For channel-originated sessions, the plugin/channel may already have the correct delivery semantics:

  • outbound queue
  • ack handling
  • timeout/retry/dead-letter
  • recipient validation
  • best-effort behavior

For example, bncr has its own outbox and ack lifecycle. If restart recovery restores a turn that originated from bncr and the session still has a valid deliveryContext / lastChannel / lastTo, letting the resumed final reply go through normal channel delivery would allow bncr to handle delivery exactly like any other assistant reply.

Suggestion

Avoid globally hard-coding deliver: false for all restart recovery resumes.

Possible approaches:

Option A: conditionally deliver when a recoverable channel target exists

Use deliver: true when the session has a valid delivery target, for example via deliveryContext or lastChannel/lastTo; otherwise keep deliver: false.

Pseudo-shape:

const shouldDeliverRecoveredFinal = hasRecoverableDeliveryContext(entry);

await callGateway({
  method: "agent",
  params: {
    message: buildResumeMessage(sanitizedPendingText),
    sessionKey: params.sessionKey,
    idempotencyKey: crypto.randomUUID(),
    deliver: shouldDeliverRecoveredFinal,
    lane: "main"
  },
  timeoutMs: 1e4
});

Option B: expose this as channel/session configuration

Allow channels or session delivery contexts to define restart recovery behavior, e.g. a capability/config such as:

restartRecovery.deliverResumedFinal = true | false

Then recovery can respect the channel's preference. Channels with robust outbound queues can opt in, while internal/web-only sessions can remain non-delivering.

Expected behavior

For a resumed interrupted turn from a channel session with a valid delivery context:

  1. Recovery resumes the agent turn.
  2. The final reply enters normal channel delivery.
  3. The plugin receives the outbound send request.
  4. The plugin's existing queue/ack/retry logic handles delivery.

For sessions without a valid delivery target, recovery can continue using deliver: false.

Notes

A local temporary test can change only this line from deliver: false to deliver: true to verify that resumed final replies re-enter the bncr outbound path, but a proper upstream fix should be scoped by channel/session delivery context rather than globally forcing delivery for every recovery path.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

For a resumed interrupted turn from a channel session with a valid delivery context:

  1. Recovery resumes the agent turn.
  2. The final reply enters normal channel delivery.
  3. The plugin receives the outbound send request.
  4. The plugin's existing queue/ack/retry logic handles delivery.

For sessions without a valid delivery target, recovery can continue using deliver: false.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix Restart recovery should optionally deliver resumed final replies for channel sessions