Skip to content

Commit 68adbf5

Browse files
committed
fix: recover tool-result delivery chain after errors
1 parent 80148cd commit 68adbf5

File tree

3 files changed

+43
-14
lines changed

3 files changed

+43
-14
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai
2323
- Discord/Gateway: handle close code 4014 (missing privileged gateway intents) without crashing the gateway. Thanks @thewilloftheshadow.
2424
- Security/Net: strip sensitive headers (`Authorization`, `Proxy-Authorization`, `Cookie`, `Cookie2`) on cross-origin redirects in `fetchWithSsrFGuard` to prevent credential forwarding across origin boundaries. (#20313) Thanks @afurm.
2525
- Auto-reply/Runner: emit `onAgentRunStart` only after agent lifecycle or tool activity begins (and only once per run), so fallback preflight errors no longer mark runs as started. (#21165) Thanks @shakkernerd.
26+
- Auto-reply/Tool results: serialize tool-result delivery and keep the delivery chain progressing after individual failures so concurrent tool outputs preserve user-visible ordering. (#21231) thanks @ahdernasr.
2627
- Auto-reply/Prompt caching: restore prefix-cache stability by keeping inbound system metadata session-stable and moving per-message IDs (`message_id`, `message_id_full`, `reply_to_id`, `sender_id`) into untrusted conversation context. (#20597) Thanks @anisoptera.
2728
- CLI/Onboarding: fix Anthropic-compatible custom provider verification by normalizing base URLs to avoid duplicate `/v1` paths during setup checks. (#21336) Thanks @17jmumford.
2829
- Security/Dependencies: bump transitive `hono` usage to `4.11.10` to incorporate timing-safe authentication comparison hardening for `basicAuth`/`bearerAuth` (`GHSA-gq3j-xvxp-8hrf`). Thanks @vincentkoc.

src/auto-reply/reply/agent-runner-execution.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -386,23 +386,25 @@ export async function runAgentTurnWithFallback(params: {
386386
// See: https://github.com/openclaw/openclaw/issues/11044
387387
let toolResultChain: Promise<void> = Promise.resolve();
388388
return (payload: ReplyPayload) => {
389-
const task = (toolResultChain = toolResultChain.then(async () => {
390-
const { text, skip } = normalizeStreamingText(payload);
391-
if (skip) {
392-
return;
393-
}
394-
await params.typingSignals.signalTextDelta(text);
395-
await onToolResult({
396-
text,
397-
mediaUrls: payload.mediaUrls,
398-
});
399-
}))
389+
toolResultChain = toolResultChain
390+
.then(async () => {
391+
const { text, skip } = normalizeStreamingText(payload);
392+
if (skip) {
393+
return;
394+
}
395+
await params.typingSignals.signalTextDelta(text);
396+
await onToolResult({
397+
text,
398+
mediaUrls: payload.mediaUrls,
399+
});
400+
})
400401
.catch((err) => {
402+
// Keep chain healthy after an error so later tool results still deliver.
401403
logVerbose(`tool result delivery failed: ${String(err)}`);
402-
})
403-
.finally(() => {
404-
params.pendingToolTasks.delete(task);
405404
});
405+
const task = toolResultChain.finally(() => {
406+
params.pendingToolTasks.delete(task);
407+
});
406408
params.pendingToolTasks.add(task);
407409
};
408410
})()

src/auto-reply/reply/agent-runner.runreplyagent.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,32 @@ describe("runReplyAgent typing (heartbeat)", () => {
562562
expect(deliveryOrder).toEqual(["first", "second"]);
563563
});
564564

565+
it("continues delivering later tool results after an earlier tool result fails", async () => {
566+
const delivered: string[] = [];
567+
const onToolResult = vi.fn(async (payload: { text?: string }) => {
568+
if (payload.text === "first") {
569+
throw new Error("simulated delivery failure");
570+
}
571+
delivered.push(payload.text ?? "");
572+
});
573+
574+
state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => {
575+
void params.onToolResult?.({ text: "first", mediaUrls: [] });
576+
void params.onToolResult?.({ text: "second", mediaUrls: [] });
577+
await new Promise((r) => setTimeout(r, 50));
578+
return { payloads: [{ text: "final" }], meta: {} };
579+
});
580+
581+
const { run } = createMinimalRun({
582+
typingMode: "message",
583+
opts: { onToolResult },
584+
});
585+
await run();
586+
587+
expect(onToolResult).toHaveBeenCalledTimes(2);
588+
expect(delivered).toEqual(["second"]);
589+
});
590+
565591
it("announces auto-compaction in verbose mode and tracks count", async () => {
566592
await withTempStateDir(async (stateDir) => {
567593
const storePath = path.join(stateDir, "sessions", "sessions.json");

0 commit comments

Comments
 (0)