mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
Guard final delivery session refresh (#83928)
* guard final delivery session refresh * add changelog for final delivery refresh guard
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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) => {
|
||||
(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user