Skip to content

Commit 459e89a

Browse files
authored
fix(gateway): keep session tool mirrors under pressure
Reverts the diagnostic queue-pressure suppression of non-terminal session tool mirrors from PR 84846 while keeping PR 86503 recipient dedupe intact. Session-only Control UI subscribers keep receiving tool lifecycle mirrors; overlapping run and session subscribers still receive one canonical run-scoped frame. Verification: focused gateway and diagnostic tests, diff check, changed check, and autoreview all passed.
1 parent 0ab63e2 commit 459e89a

5 files changed

Lines changed: 2 additions & 171 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
1515

1616
### Fixes
1717

18+
- Gateway: keep session-only Control UI tool-start mirrors flowing during diagnostic queue pressure instead of silently dropping non-terminal tool updates.
1819
- Gateway: avoid sending duplicate tool-event frames to Control UI connections that are subscribed by both run and session.
1920
- Discord/OpenAI voice: accept longer leading wake-name mistranscripts such as "Open Club" for OpenClaw.
2021
- Discord/OpenAI voice: accept leading fuzzy wake-name transcripts such as "Monty" or "Moti" for a Molty agent while keeping ambient speech gated.

src/gateway/server-chat.agent-events.test.ts

Lines changed: 0 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ describe("agent event handler", () => {
7979
resolveSessionKeyForRun?: (runId: string) => string | undefined;
8080
lifecycleErrorRetryGraceMs?: number;
8181
isChatSendRunActive?: (runId: string) => boolean;
82-
shouldBackoffLowPrioritySessionToolEvents?: () => boolean;
8382
}) {
8483
const nowSpy =
8584
params?.now === undefined ? undefined : vi.spyOn(Date, "now").mockReturnValue(params.now);
@@ -107,7 +106,6 @@ describe("agent event handler", () => {
107106
loadGatewaySessionRowForSnapshot: loadGatewaySessionRow,
108107
lifecycleErrorRetryGraceMs: params?.lifecycleErrorRetryGraceMs,
109108
isChatSendRunActive: params?.isChatSendRunActive,
110-
shouldBackoffLowPrioritySessionToolEvents: params?.shouldBackoffLowPrioritySessionToolEvents,
111109
});
112110

113111
return {
@@ -1564,96 +1562,6 @@ describe("agent event handler", () => {
15641562
);
15651563
});
15661564

1567-
it("backs off session-scoped tool mirrors during queued gateway pressure", () => {
1568-
const { broadcastToConnIds, sessionEventSubscribers, toolEventRecipients, handler } =
1569-
createHarness({
1570-
resolveSessionKeyForRun: () => "session-pressure",
1571-
shouldBackoffLowPrioritySessionToolEvents: () => true,
1572-
});
1573-
1574-
registerAgentRunContext("run-pressure-tool", {
1575-
sessionKey: "session-pressure",
1576-
verboseLevel: "off",
1577-
});
1578-
toolEventRecipients.add("run-pressure-tool", "conn-run");
1579-
sessionEventSubscribers.subscribe("conn-session");
1580-
1581-
handler({
1582-
runId: "run-pressure-tool",
1583-
seq: 1,
1584-
stream: "tool",
1585-
ts: 1_234,
1586-
data: {
1587-
phase: "start",
1588-
name: "exec",
1589-
toolCallId: "tool-pressure-1",
1590-
args: { command: "echo hi" },
1591-
},
1592-
});
1593-
1594-
expect(broadcastToConnIds).toHaveBeenCalledTimes(1);
1595-
expect(requireMockArg(broadcastToConnIds, 0, 0, "run tool event")).toBe("agent");
1596-
expect(requireMockArg(broadcastToConnIds, 0, 2, "run tool recipients")).toEqual(
1597-
new Set(["conn-run"]),
1598-
);
1599-
});
1600-
1601-
it("keeps terminal session-scoped tool mirrors during queued gateway pressure", () => {
1602-
let backoffActive = false;
1603-
const { broadcastToConnIds, sessionEventSubscribers, handler } = createHarness({
1604-
resolveSessionKeyForRun: () => "session-pressure-terminal",
1605-
shouldBackoffLowPrioritySessionToolEvents: () => backoffActive,
1606-
});
1607-
1608-
registerAgentRunContext("run-pressure-terminal-tool", {
1609-
sessionKey: "session-pressure-terminal",
1610-
verboseLevel: "off",
1611-
});
1612-
sessionEventSubscribers.subscribe("conn-session");
1613-
1614-
handler({
1615-
runId: "run-pressure-terminal-tool",
1616-
seq: 1,
1617-
stream: "tool",
1618-
ts: 1_234,
1619-
data: {
1620-
phase: "start",
1621-
name: "exec",
1622-
toolCallId: "tool-pressure-terminal-1",
1623-
args: { command: "echo hi" },
1624-
},
1625-
});
1626-
1627-
backoffActive = true;
1628-
handler({
1629-
runId: "run-pressure-terminal-tool",
1630-
seq: 2,
1631-
stream: "tool",
1632-
ts: 1_235,
1633-
data: {
1634-
phase: "result",
1635-
name: "exec",
1636-
toolCallId: "tool-pressure-terminal-1",
1637-
result: { content: [{ type: "text", text: "done" }] },
1638-
},
1639-
});
1640-
1641-
expect(broadcastToConnIds).toHaveBeenCalledTimes(2);
1642-
expect(requireMockArg(broadcastToConnIds, 0, 0, "session tool start event")).toBe(
1643-
"session.tool",
1644-
);
1645-
expect(requireMockArg(broadcastToConnIds, 1, 0, "session tool result event")).toBe(
1646-
"session.tool",
1647-
);
1648-
const resultPayload = requireMockPayload(broadcastToConnIds, 1, 1, "session tool result");
1649-
expectRecordFields(requireRecord(resultPayload.data, "session tool result data"), {
1650-
phase: "result",
1651-
name: "exec",
1652-
toolCallId: "tool-pressure-terminal-1",
1653-
result: { content: [{ type: "text", text: "done" }] },
1654-
});
1655-
});
1656-
16571565
it("suppresses heartbeat tool events for Control UI and verbose node subscribers", () => {
16581566
const {
16591567
broadcastToConnIds,

src/gateway/server-chat.ts

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { getRuntimeConfig } from "../config/io.js";
55
import { type AgentEventPayload, getAgentRunContext } from "../infra/agent-events.js";
66
import { detectErrorKind, type ErrorKind } from "../infra/errors.js";
77
import { resolveHeartbeatVisibility } from "../infra/heartbeat-visibility.js";
8-
import { isDiagnosticQueuePressureBackoffActive } from "../logging/diagnostic.js";
98
import { isAcpSessionKey, isSubagentSessionKey } from "../sessions/session-key-utils.js";
109
import { setSafeTimeout } from "../utils/timer-delay.js";
1110
import {
@@ -177,12 +176,6 @@ function readChatErrorKind(value: unknown): ErrorKind | undefined {
177176
: undefined;
178177
}
179178

180-
const TERMINAL_TOOL_EVENT_PHASES = new Set(["end", "error", "result"]);
181-
182-
function isTerminalToolEventPhase(phase: string): boolean {
183-
return TERMINAL_TOOL_EVENT_PHASES.has(phase);
184-
}
185-
186179
function excludeConnIds(
187180
connIds: ReadonlySet<string>,
188181
excludedConnIds: ReadonlySet<string> | undefined,
@@ -238,7 +231,6 @@ export type AgentEventHandlerOptions = {
238231
loadGatewaySessionRowForSnapshot?: typeof loadGatewaySessionRow;
239232
lifecycleErrorRetryGraceMs?: number;
240233
isChatSendRunActive?: (runId: string) => boolean;
241-
shouldBackoffLowPrioritySessionToolEvents?: () => boolean;
242234
};
243235

244236
export function createAgentEventHandler({
@@ -255,7 +247,6 @@ export function createAgentEventHandler({
255247
loadGatewaySessionRowForSnapshot = loadGatewaySessionRow,
256248
lifecycleErrorRetryGraceMs = AGENT_LIFECYCLE_ERROR_RETRY_GRACE_MS,
257249
isChatSendRunActive = () => false,
258-
shouldBackoffLowPrioritySessionToolEvents = isDiagnosticQueuePressureBackoffActive,
259250
}: AgentEventHandlerOptions) {
260251
type TerminalLifecycleOptions = { skipChatErrorFinal?: boolean };
261252
type PendingTerminalLifecycleError = {
@@ -998,14 +989,7 @@ export function createAgentEventHandler({
998989
// not know the runId in advance, so they cannot register as run-scoped
999990
// tool recipients. Mirror tool lifecycle onto a session-scoped event so
1000991
// they can render live pending tool cards without polling history.
1001-
const shouldMirrorSessionToolEvent =
1002-
!shouldBackoffLowPrioritySessionToolEvents() || isTerminalToolEventPhase(toolPhase);
1003-
if (
1004-
isControlUiVisible &&
1005-
sessionKey &&
1006-
!suppressHeartbeatToolEvents &&
1007-
shouldMirrorSessionToolEvent
1008-
) {
992+
if (isControlUiVisible && sessionKey && !suppressHeartbeatToolEvents) {
1009993
const sessionSubscribers = excludeConnIds(
1010994
sessionEventSubscribers.getAll(),
1011995
runToolRecipients,

src/logging/diagnostic.test.ts

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import {
3434
logSessionStateChange,
3535
logMessageQueued,
3636
diagnosticLogger,
37-
isDiagnosticQueuePressureBackoffActive,
3837
markDiagnosticSessionProgress,
3938
resetDiagnosticStateForTest,
4039
resolveStuckSessionAbortMs,
@@ -1694,35 +1693,6 @@ describe("stuck session diagnostics threshold", () => {
16941693
);
16951694
});
16961695

1697-
it("marks queued gateway pressure for low-priority backoff", () => {
1698-
startDiagnosticHeartbeat(
1699-
{
1700-
diagnostics: {
1701-
enabled: true,
1702-
},
1703-
},
1704-
{
1705-
emitMemorySample: createEmitMemorySampleMock(),
1706-
sampleLiveness: () => ({
1707-
reasons: ["cpu"],
1708-
intervalMs: 30_000,
1709-
eventLoopUtilization: 0.99,
1710-
cpuTotalMs: 30_000,
1711-
cpuCoreRatio: 1,
1712-
}),
1713-
},
1714-
);
1715-
1716-
logSessionStateChange({ sessionId: "s1", sessionKey: "main", state: "processing" });
1717-
logMessageQueued({ sessionId: "s1", sessionKey: "main", source: "discord" });
1718-
vi.advanceTimersByTime(30_000);
1719-
1720-
expect(isDiagnosticQueuePressureBackoffActive(Date.now())).toBe(true);
1721-
stopDiagnosticHeartbeat();
1722-
expect(isDiagnosticQueuePressureBackoffActive(Date.now())).toBe(false);
1723-
expect(isDiagnosticQueuePressureBackoffActive(Date.now() + 60_001)).toBe(false);
1724-
});
1725-
17261696
it("does not let idle liveness samples suppress later active-work warnings", () => {
17271697
const warnSpy = vi.spyOn(diagnosticLogger, "warn").mockImplementation(() => undefined);
17281698

src/logging/diagnostic.ts

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ const DEFAULT_LIVENESS_EVENT_LOOP_DELAY_WARN_MS = 1_000;
8282
const DEFAULT_LIVENESS_EVENT_LOOP_UTILIZATION_WARN = 0.95;
8383
const DEFAULT_LIVENESS_CPU_CORE_RATIO_WARN = 0.9;
8484
const DEFAULT_LIVENESS_WARN_COOLDOWN_MS = 120_000;
85-
const DEFAULT_QUEUE_PRESSURE_BACKOFF_TTL_MS = 60_000;
8685
let commandPollBackoffRuntimePromise: Promise<
8786
typeof import("../agents/command-poll-backoff.runtime.js")
8887
> | null = null;
@@ -1138,32 +1137,6 @@ export function logActiveRuns() {
11381137
}
11391138

11401139
let heartbeatInterval: NodeJS.Timeout | null = null;
1141-
let diagnosticQueuePressureBackoffUntil = 0;
1142-
1143-
export function isDiagnosticQueuePressureBackoffActive(now = Date.now()): boolean {
1144-
if (!heartbeatInterval || !areDiagnosticsEnabledForProcess()) {
1145-
return false;
1146-
}
1147-
return diagnosticQueuePressureBackoffUntil > now;
1148-
}
1149-
1150-
export function resetDiagnosticQueuePressureBackoffForTest(): void {
1151-
diagnosticQueuePressureBackoffUntil = 0;
1152-
}
1153-
1154-
function updateDiagnosticQueuePressureBackoff(
1155-
now: number,
1156-
sample: DiagnosticLivenessSample,
1157-
work: DiagnosticWorkSnapshot,
1158-
): void {
1159-
if (work.queuedCount <= 0 || sample.reasons.length === 0) {
1160-
return;
1161-
}
1162-
diagnosticQueuePressureBackoffUntil = Math.max(
1163-
diagnosticQueuePressureBackoffUntil,
1164-
now + DEFAULT_QUEUE_PRESSURE_BACKOFF_TTL_MS,
1165-
);
1166-
}
11671140

11681141
export function startDiagnosticHeartbeat(
11691142
config?: OpenClawConfig,
@@ -1202,9 +1175,6 @@ export function startDiagnosticHeartbeat(
12021175
livenessSample !== null && shouldEmitDiagnosticLivenessEvent(now);
12031176
const shouldEmitLivenessWarning =
12041177
livenessSample !== null && shouldEmitDiagnosticLivenessWarning(now, work);
1205-
if (livenessSample) {
1206-
updateDiagnosticQueuePressureBackoff(now, livenessSample, work);
1207-
}
12081178
const shouldEmitLivenessReport = shouldEmitLivenessEvent || shouldEmitLivenessWarning;
12091179
const shouldRecordMemorySample =
12101180
shouldEmitLivenessReport || hasRecentDiagnosticActivity(now) || hasOpenDiagnosticWork(work);
@@ -1328,7 +1298,6 @@ export function stopDiagnosticHeartbeat() {
13281298
stopDiagnosticLivenessSampler();
13291299
stopDiagnosticStabilityRecorder();
13301300
uninstallDiagnosticStabilityFatalHook();
1331-
resetDiagnosticQueuePressureBackoffForTest();
13321301
}
13331302

13341303
export function getDiagnosticSessionStateCountForTest(): number {
@@ -1349,5 +1318,4 @@ export function resetDiagnosticStateForTest(): void {
13491318
resetDiagnosticPhasesForTest();
13501319
resetDiagnosticStabilityRecorderForTest();
13511320
resetDiagnosticStabilityBundleForTest();
1352-
resetDiagnosticQueuePressureBackoffForTest();
13531321
}

0 commit comments

Comments
 (0)