Guard final delivery session refresh (#83928)

* guard final delivery session refresh

* add changelog for final delivery refresh guard
This commit is contained in:
Josh Avant
2026-05-18 21:44:17 -05:00
committed by GitHub
parent 8bd24ad6d4
commit e996159738
4 changed files with 116 additions and 18 deletions

View File

@@ -98,6 +98,7 @@ Docs: https://docs.openclaw.ai
- Discord/subagents: route the initial reply from thread-bound delegated sessions into the bound Discord thread instead of the parent channel. Fixes #83170. (#83172) Thanks @100menotu001.
- Gateway/sessions: rotate failed agent sessions when their transcript file is missing instead of wedging per-channel lanes. Fixes #83488. (#83553) Thanks @LLagoon3.
- Agents: refresh final-delivery routing from fresh session state before declaring a no-send failure, keeping recovered runs on the normal durable delivery path. (#83835) Thanks @joshavant.
- Agents: guard final-delivery fresh session routing against mismatched logical sessions before reusing recovered delivery context. (#83928) Thanks @joshavant.
- Media: prevent image metadata probing from invoking external decoder delegates on unrecognized image bytes, and stop fallback chaining after real processing errors.
- Media: install Sharp with the root package and fall back to sips, Windows native imaging, ImageMagick, GraphicsMagick, or ffmpeg for image resizing/conversion when Sharp is unavailable. Fixes #83401. Thanks @scotthuang.
- Telegram: deliver generated media completions back into forum topics by preserving topic IDs across requester-agent handoff. (#83556) Thanks @fuller-stack-dev.

View File

@@ -1533,23 +1533,32 @@ async function agentCommandInternal(
clone: false,
});
const freshEntry = freshStore[sessionKey];
if (freshEntry) {
sessionStore[sessionKey] = freshEntry;
if (!freshEntry || freshEntry.sessionId !== sessionId) {
return undefined;
}
sessionStore[sessionKey] = freshEntry;
return freshEntry;
}
: undefined;
const deliveryResult = await deliverAgentCommandResult({
const deliveryParams = {
cfg,
deps: resolvedDeps,
runtime,
opts,
outboundSession,
sessionEntry,
resolveFreshSessionEntryForDelivery,
result,
payloads,
});
};
const deliveryResult = await deliverAgentCommandResult(
resolveFreshSessionEntryForDelivery
? {
...deliveryParams,
expectedSessionIdForFreshDelivery: sessionId,
resolveFreshSessionEntryForDelivery,
}
: deliveryParams,
);
// Phase 2: Clear pending delivery payload after successful delivery.
if (

View File

@@ -363,7 +363,7 @@ describe("normalizeAgentCommandReplyPayloads", () => {
opts: {
message: "go",
deliver: true,
channel: "slack",
bestEffortDeliver: true,
sessionKey: "agent:tester:main",
} as AgentCommandOpts,
outboundSession: {
@@ -374,6 +374,7 @@ describe("normalizeAgentCommandReplyPayloads", () => {
sessionId: "session-1",
updatedAt: 1,
},
expectedSessionIdForFreshDelivery: "session-1",
resolveFreshSessionEntryForDelivery,
payloads: [{ text: "final answer" }],
result: createResult(),
@@ -395,6 +396,59 @@ describe("normalizeAgentCommandReplyPayloads", () => {
});
});
it("does not refresh final delivery routing from a different logical session", async () => {
deliverOutboundPayloadsMock.mockResolvedValue([{ channel: "slack", messageId: "msg-1" }]);
const runtime = { log: vi.fn(), error: vi.fn() };
const resolveFreshSessionEntryForDelivery = vi.fn(async () => ({
sessionId: "session-2",
updatedAt: 2,
deliveryContext: {
channel: "slack",
to: "#fresh",
accountId: "workspace-1",
},
}));
const delivered = await deliverAgentCommandResult({
cfg: {
agents: {
list: [{ id: "tester", workspace: "/tmp/agent-workspace" }],
},
} as OpenClawConfig,
deps: {} as CliDeps,
runtime: runtime as never,
opts: {
message: "go",
deliver: true,
bestEffortDeliver: true,
sessionKey: "agent:tester:main",
} as AgentCommandOpts,
outboundSession: {
key: "agent:tester:main",
agentId: "tester",
} as never,
sessionEntry: {
sessionId: "session-1",
updatedAt: 1,
},
expectedSessionIdForFreshDelivery: "session-1",
resolveFreshSessionEntryForDelivery,
payloads: [{ text: "final answer" }],
result: createResult(),
});
expect(resolveFreshSessionEntryForDelivery).toHaveBeenCalledTimes(1);
expect(deliverOutboundPayloadsMock).not.toHaveBeenCalled();
expect(delivered.deliverySucceeded).toBe(false);
expectDeliveryStatusFields(delivered, {
requested: true,
attempted: false,
status: "failed",
succeeded: false,
reason: "channel_resolved_to_internal",
});
});
it("does not report success when best-effort delivery records an error", async () => {
deliverOutboundPayloadsMock.mockImplementationOnce(async (params: unknown) => {
(

View File

@@ -73,6 +73,42 @@ export type AgentCommandDeliveryResult = {
const NESTED_LOG_PREFIX = "[agent:nested]";
type FreshSessionEntryForDeliveryResolver = () => Promise<SessionEntry | undefined>;
type FreshSessionDeliveryRefreshParams =
| {
expectedSessionIdForFreshDelivery: string;
resolveFreshSessionEntryForDelivery: FreshSessionEntryForDeliveryResolver;
}
| {
expectedSessionIdForFreshDelivery?: string;
resolveFreshSessionEntryForDelivery?: undefined;
};
type DeliverAgentCommandResultParams = {
cfg: OpenClawConfig;
deps: CliDeps;
runtime: RuntimeEnv;
opts: AgentCommandOpts;
outboundSession: OutboundSessionContext | undefined;
sessionEntry: SessionEntry | undefined;
result: RunResult;
payloads: RunResult["payloads"];
} & FreshSessionDeliveryRefreshParams;
function normalizeDeliverySessionId(value: string | undefined): string | undefined {
const trimmed = value?.trim();
return trimmed ? trimmed : undefined;
}
function isFreshDeliverySessionMatch(
freshSessionEntry: SessionEntry,
expectedSessionId: string | undefined,
): boolean {
const normalizedExpected = normalizeDeliverySessionId(expectedSessionId);
return Boolean(normalizedExpected && freshSessionEntry.sessionId === normalizedExpected);
}
function formatNestedLogPrefix(opts: AgentCommandOpts, sessionKey?: string): string {
const parts = [NESTED_LOG_PREFIX];
const session = sessionKey ?? opts.sessionKey ?? opts.sessionId;
@@ -331,17 +367,9 @@ export function normalizeAgentCommandReplyPayloads(params: {
return normalizedPayloads;
}
export async function deliverAgentCommandResult(params: {
cfg: OpenClawConfig;
deps: CliDeps;
runtime: RuntimeEnv;
opts: AgentCommandOpts;
outboundSession: OutboundSessionContext | undefined;
sessionEntry: SessionEntry | undefined;
resolveFreshSessionEntryForDelivery?: () => Promise<SessionEntry | undefined>;
result: RunResult;
payloads: RunResult["payloads"];
}): Promise<AgentCommandDeliveryResult> {
export async function deliverAgentCommandResult(
params: DeliverAgentCommandResultParams,
): Promise<AgentCommandDeliveryResult> {
const { cfg, deps, runtime, opts, outboundSession, sessionEntry, payloads, result } = params;
const effectiveSessionKey = outboundSession?.key ?? opts.sessionKey;
const deliver = opts.deliver === true;
@@ -467,7 +495,13 @@ export async function deliverAgentCommandResult(params: {
let deliveryRouting = await resolveDeliveryRouting(sessionEntry);
if (isRetryableFreshSessionRoutingFailure(deliveryRouting)) {
const freshSessionEntry = await params.resolveFreshSessionEntryForDelivery?.();
if (freshSessionEntry && freshSessionEntry !== sessionEntry) {
const expectedFreshSessionId =
params.expectedSessionIdForFreshDelivery ?? sessionEntry?.sessionId;
if (
freshSessionEntry &&
freshSessionEntry !== sessionEntry &&
isFreshDeliverySessionMatch(freshSessionEntry, expectedFreshSessionId)
) {
const freshRouting = await resolveDeliveryRouting(freshSessionEntry);
if (!deliveryRoutingFailureReason(freshRouting)) {
if (!opts.json) {