fix(outbound): replay queued session context (#66025)

* fix(outbound): preserve replay session context

* fix(outbound): remove user work log

* changelog: note outbound session-context replay fix (#66025)

---------

Co-authored-by: Devin Robison <drobison@nvidia.com>
This commit is contained in:
Agustin Rivera
2026-04-13 12:30:42 -07:00
committed by GitHub
parent 7c09ba70ef
commit 48aae82bbc
6 changed files with 68 additions and 0 deletions

View File

@@ -26,6 +26,7 @@ Docs: https://docs.openclaw.ai
- Browser/CDP: let local attach-only `manual-cdp` profiles reuse the local loopback CDP control plane under strict default policy and remote-class probe timeouts, so tabs/snapshot stop falsely reporting a live local browser session as not running. (#65611, #66080) Thanks @mbelinky.
- Cron/scheduler: stop inventing short retries when cron next-run calculation returns no valid future slot, and keep a maintenance wake armed so enabled unscheduled jobs recover without entering a refire loop. (#66019, #66083) Thanks @mbelinky.
- Cron/scheduler: preserve the active error-backoff floor when maintenance repair recomputes a missing cron next-run, so recurring errored jobs do not resume early after a transient next-run resolution failure. (#66019, #66083, #66113) Thanks @mbelinky.
- Outbound/delivery-queue: persist the originating outbound `session` context on queued delivery entries and replay it during recovery, so write-ahead-queued sends keep their original outbound media policy context after restart instead of evaluating against a missing session. (#66025) Thanks @eleqtrizit.
## 2026.4.12

View File

@@ -507,6 +507,7 @@ export async function deliverOutboundPayloads(
forceDocument: params.forceDocument,
silent: params.silent,
mirror: params.mirror,
session: params.session,
gatewayClientScopes: params.gatewayClientScopes,
}).catch(() => null); // Best-effort — don't block delivery if queue write fails.

View File

@@ -104,6 +104,7 @@ function buildRecoveryDeliverParams(entry: QueuedDelivery, cfg: OpenClawConfig)
forceDocument: entry.forceDocument,
silent: entry.silent,
mirror: entry.mirror,
session: entry.session,
gatewayClientScopes: entry.gatewayClientScopes,
skipQueue: true, // Prevent re-enqueueing during recovery.
} satisfies Parameters<DeliverFn>[0];

View File

@@ -4,6 +4,7 @@ import type { ReplyPayload } from "../../auto-reply/types.js";
import { resolveStateDir } from "../../config/paths.js";
import { generateSecureUuid } from "../secure-random.js";
import type { OutboundMirror } from "./mirror.js";
import type { OutboundSessionContext } from "./session-context.js";
import type { OutboundChannel } from "./targets.js";
const QUEUE_DIRNAME = "delivery-queue";
@@ -26,6 +27,8 @@ export type QueuedDeliveryPayload = {
forceDocument?: boolean;
silent?: boolean;
mirror?: OutboundMirror;
/** Session context needed to preserve outbound media policy on recovery. */
session?: OutboundSessionContext;
/** Gateway caller scopes at enqueue time, preserved for recovery replay. */
gatewayClientScopes?: readonly string[];
};
@@ -144,6 +147,7 @@ export async function enqueueDelivery(
forceDocument: params.forceDocument,
silent: params.silent,
mirror: params.mirror,
session: params.session,
gatewayClientScopes: params.gatewayClientScopes,
retryCount: 0,
});

View File

@@ -165,6 +165,15 @@ describe("delivery-queue recovery", () => {
text: "a",
mediaUrls: ["https://example.com/a.png"],
},
session: {
key: "agent:main:main",
agentId: "agent-main",
requesterAccountId: "acct-1",
requesterSenderId: "sender-1",
requesterSenderName: "Sender One",
requesterSenderUsername: "sender.one",
requesterSenderE164: "+15551234567",
},
},
tmpDir(),
);
@@ -183,6 +192,15 @@ describe("delivery-queue recovery", () => {
text: "a",
mediaUrls: ["https://example.com/a.png"],
},
session: {
key: "agent:main:main",
agentId: "agent-main",
requesterAccountId: "acct-1",
requesterSenderId: "sender-1",
requesterSenderName: "Sender One",
requesterSenderUsername: "sender.one",
requesterSenderE164: "+15551234567",
},
}),
);
});

View File

@@ -33,6 +33,12 @@ describe("delivery-queue storage", () => {
text: "hello",
mediaUrls: ["https://example.com/file.png"],
},
session: {
key: "agent:main:main",
agentId: "agent-main",
requesterAccountId: "acct-1",
requesterSenderId: "sender-1",
},
},
tmpDir(),
);
@@ -53,6 +59,12 @@ describe("delivery-queue storage", () => {
text: "hello",
mediaUrls: ["https://example.com/file.png"],
},
session: {
key: "agent:main:main",
agentId: "agent-main",
requesterAccountId: "acct-1",
requesterSenderId: "sender-1",
},
retryCount: 0,
});
expect(entry.payloads).toEqual([{ text: "hello" }]);
@@ -169,6 +181,37 @@ describe("delivery-queue storage", () => {
expect(entry.gatewayClientScopes).toEqual(["operator.write"]);
});
it("persists session context for recovery replay", async () => {
const id = await enqueueTextDelivery(
{
channel: "telegram",
to: "2",
payloads: [{ text: "b" }],
session: {
key: "agent:main:main",
agentId: "agent-main",
requesterAccountId: "acct-1",
requesterSenderId: "sender-1",
requesterSenderName: "Sender One",
requesterSenderUsername: "sender.one",
requesterSenderE164: "+15551234567",
},
},
tmpDir(),
);
const entry = readQueuedEntry(tmpDir(), id);
expect(entry.session).toEqual({
key: "agent:main:main",
agentId: "agent-main",
requesterAccountId: "acct-1",
requesterSenderId: "sender-1",
requesterSenderName: "Sender One",
requesterSenderUsername: "sender.one",
requesterSenderE164: "+15551234567",
});
});
it("backfills lastAttemptAt for legacy retry entries during load", async () => {
const id = await enqueueTextDelivery({
channel: "whatsapp",