diff --git a/CHANGELOG.md b/CHANGELOG.md index 66a11bce406..b677bfb514d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Gateway/OpenAI-compatible: send the assistant role SSE chunk as soon as streaming chat-completion headers are accepted, so cold agent setup cannot leave `/v1/chat/completions` clients with a bodyless 200 response until their idle timeout fires. +- Agents/media: avoid direct generated-media completion fallback while the announce-agent run is still pending, so async video and music completions do not duplicate raw media messages. (#77754) - TUI/sessions: bound the session picker to recent rows and use exact lookup-style refreshes for the active session, so dusty stores no longer make TUI hydrate weeks-old transcripts before becoming responsive. Thanks @vincentkoc. - Doctor/gateway: report recent supervisor restart handoffs in `openclaw doctor --deep`, using the installed service environment when available so service-managed clean exits are visible in guided diagnostics. Thanks @shakkernerd. - Gateway/status: show recent supervisor restart handoffs in `openclaw gateway status --deep`, including JSON details, so clean service-managed restarts are reported as restart handoffs instead of opaque stopped-service diagnostics. Thanks @shakkernerd. diff --git a/src/agents/subagent-announce-delivery.ts b/src/agents/subagent-announce-delivery.ts index f44fe6b5668..a4ea8f7d721 100644 --- a/src/agents/subagent-announce-delivery.ts +++ b/src/agents/subagent-announce-delivery.ts @@ -6,6 +6,7 @@ import { normalizeAccountId } from "../routing/session-key.js"; import { defaultRuntime } from "../runtime.js"; import { deriveSessionChatTypeFromKey } from "../sessions/session-chat-type-shared.js"; import { isCronSessionKey } from "../sessions/session-key-utils.js"; +import { isNonTerminalAgentRunStatus } from "../shared/agent-run-status.js"; import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; import { mergeDeliveryContext, @@ -694,7 +695,7 @@ function isGatewayAgentRunPending(response: unknown): boolean { return false; } const status = (response as { status?: unknown }).status; - return status === "accepted" || status === "in_flight" || status === "started"; + return isNonTerminalAgentRunStatus(status); } function inferCompletionChatType(params: { diff --git a/src/gateway/server-methods/agent-wait-dedupe.ts b/src/gateway/server-methods/agent-wait-dedupe.ts index 83ae4f842b9..1f7fe959132 100644 --- a/src/gateway/server-methods/agent-wait-dedupe.ts +++ b/src/gateway/server-methods/agent-wait-dedupe.ts @@ -1,3 +1,4 @@ +import { isNonTerminalAgentRunStatus } from "../../shared/agent-run-status.js"; import { setSafeTimeout } from "../../utils/timer-delay.js"; import type { DedupeEntry } from "../server-shared.js"; @@ -91,7 +92,7 @@ function readTerminalSnapshotFromDedupeEntry(entry: DedupeEntry): AgentWaitTermi } | undefined; const status = typeof payload?.status === "string" ? payload.status : undefined; - if (status === "accepted" || status === "started" || status === "in_flight") { + if (isNonTerminalAgentRunStatus(status)) { return null; } diff --git a/src/shared/agent-run-status.test.ts b/src/shared/agent-run-status.test.ts new file mode 100644 index 00000000000..b4d70c63366 --- /dev/null +++ b/src/shared/agent-run-status.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; +import { isNonTerminalAgentRunStatus } from "./agent-run-status.js"; + +describe("isNonTerminalAgentRunStatus", () => { + it.each(["accepted", "started", "in_flight"])("recognizes %s as non-terminal", (status) => { + expect(isNonTerminalAgentRunStatus(status)).toBe(true); + }); + + it.each(["ok", "error", "timeout", "queued", "", null, undefined, 1, {}, []])( + "does not recognize %s as non-terminal", + (status) => { + expect(isNonTerminalAgentRunStatus(status)).toBe(false); + }, + ); +}); diff --git a/src/shared/agent-run-status.ts b/src/shared/agent-run-status.ts new file mode 100644 index 00000000000..d43a4646b41 --- /dev/null +++ b/src/shared/agent-run-status.ts @@ -0,0 +1,5 @@ +const NON_TERMINAL_AGENT_RUN_STATUSES = new Set(["accepted", "started", "in_flight"]); + +export function isNonTerminalAgentRunStatus(status: unknown): boolean { + return typeof status === "string" && NON_TERMINAL_AGENT_RUN_STATUSES.has(status); +}