mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:20:43 +00:00
fix(gateway): surface chat.send lifecycle errors to clients (#69747)
Merged via squash.
Prepared head SHA: 75b403b2de
Co-authored-by: wangshu94 <53429538+wangshu94@users.noreply.github.com>
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
Reviewed-by: @hxy91819
This commit is contained in:
@@ -75,6 +75,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Providers/OpenAI: honor the private-network SSRF opt-in for OpenAI-compatible image generation endpoints, so trusted LocalAI/LAN `image_generate` routes work without disabling SSRF checks globally. Fixes #62879. Thanks @seitzbg.
|
||||
- Providers/OpenAI: stop advertising the removed `gpt-5.3-codex-spark` Codex model through fallback catalogs, and suppress stale rows with a GPT-5.5 recovery hint.
|
||||
- Plugins/QR: replace legacy `qrcode-terminal` QR rendering with bounded `qrcode-tui` helpers for plugin login/setup flows. (#65969) Thanks @vincentkoc.
|
||||
- Gateway/chat: register chat.send runs in the chat run registry so lifecycle error events reach the client instead of being silently dropped, fixing stuck 'waiting' state and /abort reporting no active run. (#69747) Thanks @wangshu94.
|
||||
- Voice-call/realtime: wait for OpenAI session configuration before greeting or forwarding buffered audio, and reject non-allowlisted Twilio callers before stream setup. (#43501) Thanks @forrestblount.
|
||||
- ACPX/Codex: stop materializing `auth.json` bridge files for Codex ACP, Codex app-server, and Codex CLI runs; Codex-owned runtimes now use their normal `CODEX_HOME`/`~/.codex` auth path directly.
|
||||
- Auto-reply/system events: route async exec-event completion replies through the persisted session delivery context, so long-running command results return to the originating channel instead of being dropped when live origin metadata is missing. (#70258) Thanks @wzfukui.
|
||||
|
||||
@@ -1326,6 +1326,44 @@ describe("agent event handler", () => {
|
||||
expect(agentRunSeq.has("run-chat-send")).toBe(false);
|
||||
});
|
||||
|
||||
it("emits lifecycle chat errors for active chat.send runs with a chat run link", () => {
|
||||
vi.useFakeTimers();
|
||||
const { broadcast, chatRunState, clearAgentRunContext, agentRunSeq, handler } = createHarness({
|
||||
resolveSessionKeyForRun: () => "session-chat-send",
|
||||
lifecycleErrorRetryGraceMs: 100,
|
||||
isChatSendRunActive: (runId) => runId === "run-chat-send",
|
||||
});
|
||||
chatRunState.registry.add("run-chat-send", {
|
||||
sessionKey: "session-chat-send",
|
||||
clientRunId: "run-chat-send",
|
||||
});
|
||||
registerAgentRunContext("run-chat-send", { sessionKey: "session-chat-send" });
|
||||
|
||||
handler({
|
||||
runId: "run-chat-send",
|
||||
seq: 1,
|
||||
stream: "lifecycle",
|
||||
ts: Date.now(),
|
||||
data: { phase: "error", error: "chat.send failed" },
|
||||
});
|
||||
|
||||
vi.advanceTimersByTime(100);
|
||||
|
||||
const chatErrors = chatBroadcastCalls(broadcast).filter(
|
||||
([, payload]) => (payload as { state?: string }).state === "error",
|
||||
);
|
||||
expect(chatErrors).toHaveLength(1);
|
||||
expect(chatErrors[0]?.[1]).toMatchObject({
|
||||
runId: "run-chat-send",
|
||||
sessionKey: "session-chat-send",
|
||||
state: "error",
|
||||
errorMessage: "chat.send failed",
|
||||
});
|
||||
expect(chatRunState.registry.peek("run-chat-send")).toBeUndefined();
|
||||
expect(clearAgentRunContext).toHaveBeenCalledWith("run-chat-send");
|
||||
expect(agentRunSeq.has("run-chat-send")).toBe(false);
|
||||
});
|
||||
|
||||
it("suppresses chat and node session events for non-control-UI-visible runs", () => {
|
||||
const { broadcast, nodeSendToSession, handler } = createHarness({
|
||||
resolveSessionKeyForRun: () => "session-hidden",
|
||||
|
||||
@@ -307,6 +307,7 @@ function createChatContext(): Pick<
|
||||
| "chatDeltaSentAt"
|
||||
| "chatDeltaLastBroadcastLen"
|
||||
| "chatAbortedRuns"
|
||||
| "addChatRun"
|
||||
| "removeChatRun"
|
||||
| "dedupe"
|
||||
| "loadGatewayModelCatalog"
|
||||
@@ -322,6 +323,7 @@ function createChatContext(): Pick<
|
||||
chatDeltaSentAt: new Map(),
|
||||
chatDeltaLastBroadcastLen: new Map(),
|
||||
chatAbortedRuns: new Map(),
|
||||
addChatRun: vi.fn(),
|
||||
removeChatRun: vi.fn(),
|
||||
dedupe: new Map(),
|
||||
loadGatewayModelCatalog: async () =>
|
||||
|
||||
@@ -2248,6 +2248,10 @@ export const chatHandlers: GatewayRequestHandlers = {
|
||||
ownerConnId: normalizeOptionalText(client?.connId),
|
||||
ownerDeviceId: normalizeOptionalText(client?.connect?.device?.id),
|
||||
});
|
||||
context.addChatRun(clientRunId, {
|
||||
sessionKey,
|
||||
clientRunId,
|
||||
});
|
||||
const ackPayload = {
|
||||
runId: clientRunId,
|
||||
status: "started" as const,
|
||||
@@ -2740,8 +2744,11 @@ export const chatHandlers: GatewayRequestHandlers = {
|
||||
})
|
||||
.finally(() => {
|
||||
context.chatAbortControllers.delete(clientRunId);
|
||||
context.removeChatRun(clientRunId, clientRunId, sessionKey);
|
||||
});
|
||||
} catch (err) {
|
||||
context.chatAbortControllers.delete(clientRunId);
|
||||
context.removeChatRun(clientRunId, clientRunId, sessionKey);
|
||||
const error = errorShape(ErrorCodes.UNAVAILABLE, String(err));
|
||||
const payload = {
|
||||
runId: clientRunId,
|
||||
|
||||
Reference in New Issue
Block a user