mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 08:32:43 +00:00
fix: contain block reply media failures
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createSubscribedSessionHarness,
|
||||
emitAssistantTextDelta,
|
||||
emitAssistantTextEnd,
|
||||
emitMessageStartAndEndForAssistantText,
|
||||
} from "./pi-embedded-subscribe.e2e-harness.js";
|
||||
|
||||
const waitForAsyncCallbacks = async () => {
|
||||
await Promise.resolve();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
};
|
||||
|
||||
describe("subscribeEmbeddedPiSession block reply rejections", () => {
|
||||
const unhandledRejections: unknown[] = [];
|
||||
const onUnhandledRejection = (reason: unknown) => {
|
||||
unhandledRejections.push(reason);
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
process.off("unhandledRejection", onUnhandledRejection);
|
||||
unhandledRejections.length = 0;
|
||||
});
|
||||
|
||||
it("contains rejected async text_end block replies", async () => {
|
||||
process.on("unhandledRejection", onUnhandledRejection);
|
||||
const onBlockReply = vi.fn().mockRejectedValue(new Error("boom"));
|
||||
const { emit } = createSubscribedSessionHarness({
|
||||
runId: "run",
|
||||
onBlockReply,
|
||||
blockReplyBreak: "text_end",
|
||||
});
|
||||
|
||||
emitAssistantTextDelta({ emit, delta: "Hello block" });
|
||||
emitAssistantTextEnd({ emit });
|
||||
await waitForAsyncCallbacks();
|
||||
|
||||
expect(onBlockReply).toHaveBeenCalledTimes(1);
|
||||
expect(unhandledRejections).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("contains rejected async message_end block replies", async () => {
|
||||
process.on("unhandledRejection", onUnhandledRejection);
|
||||
const onBlockReply = vi.fn().mockRejectedValue(new Error("boom"));
|
||||
const { emit } = createSubscribedSessionHarness({
|
||||
runId: "run",
|
||||
onBlockReply,
|
||||
blockReplyBreak: "message_end",
|
||||
});
|
||||
|
||||
emitMessageStartAndEndForAssistantText({ emit, text: "Hello block" });
|
||||
await waitForAsyncCallbacks();
|
||||
|
||||
expect(onBlockReply).toHaveBeenCalledTimes(1);
|
||||
expect(unhandledRejections).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -326,6 +326,16 @@ export function handleMessageEnd(
|
||||
ctx.finalizeAssistantTexts({ text, addedDuringMessage, chunkerHasBuffered });
|
||||
|
||||
const onBlockReply = ctx.params.onBlockReply;
|
||||
const emitBlockReplySafely = (payload: Parameters<NonNullable<typeof onBlockReply>>[0]) => {
|
||||
if (!onBlockReply) {
|
||||
return;
|
||||
}
|
||||
void Promise.resolve()
|
||||
.then(() => onBlockReply(payload))
|
||||
.catch((err) => {
|
||||
ctx.log.warn(`block reply callback failed: ${String(err)}`);
|
||||
});
|
||||
};
|
||||
const shouldEmitReasoning = Boolean(
|
||||
ctx.state.includeReasoning &&
|
||||
formattedReasoning &&
|
||||
@@ -339,7 +349,7 @@ export function handleMessageEnd(
|
||||
return;
|
||||
}
|
||||
ctx.state.lastReasoningSent = formattedReasoning;
|
||||
void onBlockReply?.({ text: formattedReasoning, isReasoning: true });
|
||||
emitBlockReplySafely({ text: formattedReasoning, isReasoning: true });
|
||||
};
|
||||
|
||||
if (shouldEmitReasoningBeforeAnswer) {
|
||||
@@ -362,7 +372,7 @@ export function handleMessageEnd(
|
||||
} = splitResult;
|
||||
// Emit if there's content OR audioAsVoice flag (to propagate the flag).
|
||||
if (cleanedText || (mediaUrls && mediaUrls.length > 0) || audioAsVoice) {
|
||||
void onBlockReply({
|
||||
emitBlockReplySafely({
|
||||
text: cleanedText,
|
||||
mediaUrls: mediaUrls?.length ? mediaUrls : undefined,
|
||||
audioAsVoice,
|
||||
|
||||
@@ -100,6 +100,18 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar
|
||||
const pendingMessagingTargets = state.pendingMessagingTargets;
|
||||
const replyDirectiveAccumulator = createStreamingDirectiveAccumulator();
|
||||
const partialReplyDirectiveAccumulator = createStreamingDirectiveAccumulator();
|
||||
const emitBlockReplySafely = (
|
||||
payload: Parameters<NonNullable<SubscribeEmbeddedPiSessionParams["onBlockReply"]>>[0],
|
||||
) => {
|
||||
if (!params.onBlockReply) {
|
||||
return;
|
||||
}
|
||||
void Promise.resolve()
|
||||
.then(() => params.onBlockReply?.(payload))
|
||||
.catch((err) => {
|
||||
log.warn(`block reply callback failed: ${String(err)}`);
|
||||
});
|
||||
};
|
||||
|
||||
const resetAssistantMessageState = (nextAssistantTextBaseline: number) => {
|
||||
state.deltaBuffer = "";
|
||||
@@ -510,7 +522,7 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar
|
||||
if (!cleanedText && (!mediaUrls || mediaUrls.length === 0) && !audioAsVoice) {
|
||||
return;
|
||||
}
|
||||
void params.onBlockReply({
|
||||
emitBlockReplySafely({
|
||||
text: cleanedText,
|
||||
mediaUrls: mediaUrls?.length ? mediaUrls : undefined,
|
||||
audioAsVoice,
|
||||
|
||||
Reference in New Issue
Block a user