Skip to content

Commit 1f55896

Browse files
committed
fix(tui): dismiss watchdog notice when response actually arrives
The streaming watchdog renders 'This response is taking longer than expected. Send another message to continue.' after 30s without a chat delta. If a delta or final then arrives — common for runs that are slow but not stuck — the notice stays in the log alongside the recovered response and contradicts what the user sees. Track the notice by runId in the chat log via a new `addPendingSystem` + `dismissPendingSystem` pair (mirroring the existing pendingUsers pattern) and dismiss it from `handleChatEvent` whenever any further chat event for that run is processed. The watchdog's internal cleanup (`activeChatRunId` reset, status idle, history reload) is unchanged. Refs #67052, #69081 (closed). Prior attempt #69026 raised the threshold and suppressed the notice entirely; this is the narrower fix that keeps the warning useful for genuinely stuck runs.
1 parent e2c92be commit 1f55896

5 files changed

Lines changed: 126 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ Docs: https://docs.openclaw.ai
6565
- Exec: keep configured `tools.exec.pathPrepend` entries ahead of user shell startup PATH changes on POSIX gateway runs. (#81403) Thanks @medns.
6666
- Gateway/sessions: allow shared-secret bearer callers to read and stream session history without an explicit scope header. (#81815) Thanks @medns.
6767
- Agents/embedded runner: classify HTML auth provider responses as `auth_html` and return a re-authentication hint instead of the CDN-blocked copy that `upstream_html` returns. Cloudflare Access login pages, nginx basic-auth challenges, and gateway login walls all produce HTML auth bodies that were previously misdiagnosed as transient CDN blocks. (#79900) Thanks @martingarramon.
68+
- TUI/streaming watchdog: dismiss the `This response is taking longer than expected` notice as soon as a chat event for the same run arrives, so the message no longer sits next to the recovered response when the run was only briefly silent. Refs #67052, #69081 (closed), prior attempt #69026. Thanks @jpruit20 and @romneyda.
6869
- Agents/Pi: tolerate OpenClaw-owned transcript writes while embedded prompts are released for model I/O, keeping long-running Feishu, Slack, Telegram, and cron turns from failing with false session-takeover errors. Fixes #84059. (#84250) Thanks @tianxiaochannel-oss88.
6970

7071
## 2026.5.20

src/tui/components/chat-log.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,33 @@ describe("ChatLog", () => {
150150
expect(chatLog.countPendingUsers()).toBe(0);
151151
});
152152

153+
it("dismisses a pending system notice by runId", () => {
154+
const chatLog = new ChatLog(40);
155+
156+
chatLog.addPendingSystem("run-1", "taking longer than expected");
157+
let rendered = chatLog.render(120).join("\n");
158+
expect(rendered).toContain("taking longer than expected");
159+
160+
const dismissed = chatLog.dismissPendingSystem("run-1");
161+
expect(dismissed).toBe(true);
162+
163+
rendered = chatLog.render(120).join("\n");
164+
expect(rendered).not.toContain("taking longer than expected");
165+
expect(chatLog.dismissPendingSystem("run-1")).toBe(false);
166+
});
167+
168+
it("replaces an existing pending system notice for the same runId", () => {
169+
const chatLog = new ChatLog(40);
170+
171+
chatLog.addPendingSystem("run-1", "first notice");
172+
chatLog.addPendingSystem("run-1", "second notice");
173+
174+
const rendered = chatLog.render(120).join("\n");
175+
expect(rendered).not.toContain("first notice");
176+
expect(rendered).toContain("second notice");
177+
expect(chatLog.children.length).toBe(1);
178+
});
179+
153180
it("does not hide a new repeated prompt when only older history matches", () => {
154181
const chatLog = new ChatLog(40);
155182

src/tui/components/chat-log.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export class ChatLog extends Container {
2020
createdAt: number;
2121
}
2222
>();
23+
private pendingSystemNotices = new Map<string, Container>();
2324
private btwMessage: BtwInlineMessage | null = null;
2425
private toolsExpanded = false;
2526

@@ -44,6 +45,11 @@ export class ChatLog extends Container {
4445
this.pendingUsers.delete(runId);
4546
}
4647
}
48+
for (const [runId, entry] of this.pendingSystemNotices.entries()) {
49+
if (entry === component) {
50+
this.pendingSystemNotices.delete(runId);
51+
}
52+
}
4753
if (this.btwMessage === component) {
4854
this.btwMessage = null;
4955
}
@@ -69,6 +75,7 @@ export class ChatLog extends Container {
6975
this.clear();
7076
this.toolById.clear();
7177
this.streamingRuns.clear();
78+
this.pendingSystemNotices.clear();
7279
this.btwMessage = null;
7380
if (!opts?.preservePendingUsers) {
7481
this.pendingUsers.clear();
@@ -102,6 +109,26 @@ export class ChatLog extends Container {
102109
this.append(this.createSystemMessage(text));
103110
}
104111

112+
addPendingSystem(runId: string, text: string) {
113+
const existing = this.pendingSystemNotices.get(runId);
114+
if (existing) {
115+
this.removeChild(existing);
116+
}
117+
const entry = this.createSystemMessage(text);
118+
this.pendingSystemNotices.set(runId, entry);
119+
this.append(entry);
120+
}
121+
122+
dismissPendingSystem(runId: string) {
123+
const existing = this.pendingSystemNotices.get(runId);
124+
if (!existing) {
125+
return false;
126+
}
127+
this.removeChild(existing);
128+
this.pendingSystemNotices.delete(runId);
129+
return true;
130+
}
131+
105132
addUser(text: string) {
106133
this.append(new UserMessageComponent(text));
107134
}

src/tui/tui-event-handlers.test.ts

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ type HandlerChatLog = {
88
startTool: (...args: unknown[]) => void;
99
updateToolResult: (...args: unknown[]) => void;
1010
addSystem: (...args: unknown[]) => void;
11+
addPendingSystem: (...args: unknown[]) => void;
12+
dismissPendingSystem: (...args: unknown[]) => void;
1113
updateAssistant: (...args: unknown[]) => void;
1214
finalizeAssistant: (...args: unknown[]) => void;
1315
dropAssistant: (...args: unknown[]) => void;
@@ -21,6 +23,8 @@ type MockChatLog = {
2123
startTool: MockFn;
2224
updateToolResult: MockFn;
2325
addSystem: MockFn;
26+
addPendingSystem: MockFn;
27+
dismissPendingSystem: MockFn;
2428
updateAssistant: MockFn;
2529
finalizeAssistant: MockFn;
2630
dropAssistant: MockFn;
@@ -36,6 +40,8 @@ function createMockChatLog(): MockChatLog & HandlerChatLog {
3640
startTool: vi.fn(),
3741
updateToolResult: vi.fn(),
3842
addSystem: vi.fn(),
43+
addPendingSystem: vi.fn(),
44+
dismissPendingSystem: vi.fn(),
3945
updateAssistant: vi.fn(),
4046
finalizeAssistant: vi.fn(),
4147
dropAssistant: vi.fn(),
@@ -1078,7 +1084,7 @@ describe("tui-event-handlers: streaming watchdog", () => {
10781084

10791085
expect(setActivityStatus).toHaveBeenLastCalledWith("idle");
10801086
expect(state.activeChatRunId).toBeNull();
1081-
expect(chatLog.addSystem).toHaveBeenCalledWith(expectedTimeoutMessage);
1087+
expect(chatLog.addPendingSystem).toHaveBeenCalledWith("run-stuck", expectedTimeoutMessage);
10821088

10831089
handlers.dispose?.();
10841090
});
@@ -1284,7 +1290,7 @@ describe("tui-event-handlers: streaming watchdog", () => {
12841290
expect(setActivityStatus).toHaveBeenLastCalledWith("idle");
12851291
expect(state.activeChatRunId).toBeNull();
12861292
expect(loadHistory).toHaveBeenCalledTimes(1);
1287-
expect(chatLog.addSystem).not.toHaveBeenCalledWith(expectedTimeoutMessage);
1293+
expect(chatLog.addPendingSystem).not.toHaveBeenCalled();
12881294

12891295
handlers.dispose?.();
12901296
});
@@ -1310,8 +1316,8 @@ describe("tui-event-handlers: streaming watchdog", () => {
13101316
vi.advanceTimersByTime(10_000);
13111317

13121318
const statusCalls = setActivityStatus.mock.calls.map((c) => c[0]);
1313-
expect(statusCalls.reduce((count, s) => count + (s === "idle" ? 1 : 0), 0)).toBe(1);
1314-
expect(chatLog.addSystem).not.toHaveBeenCalledWith(expectedTimeoutMessage);
1319+
expect(statusCalls.filter((s) => s === "idle").length).toBe(1);
1320+
expect(chatLog.addPendingSystem).not.toHaveBeenCalled();
13151321
expect(state.activeChatRunId).toBeNull();
13161322

13171323
handlers.dispose?.();
@@ -1332,7 +1338,7 @@ describe("tui-event-handlers: streaming watchdog", () => {
13321338
vi.advanceTimersByTime(60_000);
13331339

13341340
expect(setActivityStatus).not.toHaveBeenCalledWith("idle");
1335-
expect(chatLog.addSystem).not.toHaveBeenCalled();
1341+
expect(chatLog.addPendingSystem).not.toHaveBeenCalled();
13361342
expect(state.activeChatRunId).toBe("run-no-watchdog");
13371343

13381344
handlers.dispose?.();
@@ -1374,7 +1380,7 @@ describe("tui-event-handlers: streaming watchdog", () => {
13741380

13751381
expect(setActivityStatus).toHaveBeenLastCalledWith("idle");
13761382
expect(state.activeChatRunId).toBeNull();
1377-
expect(chatLog.addSystem).toHaveBeenCalledTimes(2);
1383+
expect(chatLog.addPendingSystem).toHaveBeenCalledTimes(2);
13781384

13791385
handlers.dispose?.();
13801386
});
@@ -1395,6 +1401,60 @@ describe("tui-event-handlers: streaming watchdog", () => {
13951401
vi.advanceTimersByTime(10_000);
13961402

13971403
expect(setActivityStatus).not.toHaveBeenCalledWith("idle");
1398-
expect(chatLog.addSystem).not.toHaveBeenCalled();
1404+
expect(chatLog.addPendingSystem).not.toHaveBeenCalled();
1405+
});
1406+
1407+
it("dismisses the watchdog notice when a delta arrives after the watchdog fires", () => {
1408+
const { state, chatLog, handlers } = createHarness({
1409+
streamingWatchdogMs: 5_000,
1410+
});
1411+
1412+
handlers.handleChatEvent({
1413+
runId: "run-late",
1414+
sessionKey: state.currentSessionKey,
1415+
state: "delta",
1416+
message: { content: "starting" },
1417+
} satisfies ChatEvent);
1418+
1419+
vi.advanceTimersByTime(5_001);
1420+
expect(chatLog.addPendingSystem).toHaveBeenCalledWith("run-late", expectedTimeoutMessage);
1421+
1422+
handlers.handleChatEvent({
1423+
runId: "run-late",
1424+
sessionKey: state.currentSessionKey,
1425+
state: "delta",
1426+
message: { content: "actually here" },
1427+
} satisfies ChatEvent);
1428+
1429+
expect(chatLog.dismissPendingSystem).toHaveBeenCalledWith("run-late");
1430+
1431+
handlers.dispose?.();
1432+
});
1433+
1434+
it("dismisses the watchdog notice when the final arrives after the watchdog fires", () => {
1435+
const { state, chatLog, handlers } = createHarness({
1436+
streamingWatchdogMs: 5_000,
1437+
});
1438+
1439+
handlers.handleChatEvent({
1440+
runId: "run-final-late",
1441+
sessionKey: state.currentSessionKey,
1442+
state: "delta",
1443+
message: { content: "starting" },
1444+
} satisfies ChatEvent);
1445+
1446+
vi.advanceTimersByTime(5_001);
1447+
expect(chatLog.addPendingSystem).toHaveBeenCalledWith("run-final-late", expectedTimeoutMessage);
1448+
1449+
handlers.handleChatEvent({
1450+
runId: "run-final-late",
1451+
sessionKey: state.currentSessionKey,
1452+
state: "final",
1453+
message: { content: [{ type: "text", text: "done" }], stopReason: "stop" },
1454+
} satisfies ChatEvent);
1455+
1456+
expect(chatLog.dismissPendingSystem).toHaveBeenCalledWith("run-final-late");
1457+
1458+
handlers.dispose?.();
13991459
});
14001460
});

src/tui/tui-event-handlers.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ type EventHandlerChatLog = {
1414
options?: { partial?: boolean; isError?: boolean },
1515
) => void;
1616
addSystem: (text: string) => void;
17+
addPendingSystem: (runId: string, text: string) => void;
18+
dismissPendingSystem: (runId: string) => void;
1719
updateAssistant: (text: string, runId: string) => void;
1820
finalizeAssistant: (text: string, runId: string) => void;
1921
dropAssistant: (runId: string) => void;
@@ -131,7 +133,7 @@ export function createEventHandlers(context: EventHandlerContext) {
131133
return;
132134
}
133135
flushPendingHistoryRefreshIfIdle();
134-
chatLog.addSystem(STREAMING_WATCHDOG_USER_MESSAGE);
136+
chatLog.addPendingSystem(runId, STREAMING_WATCHDOG_USER_MESSAGE);
135137
tui.requestRender();
136138
}, streamingWatchdogMs);
137139
const maybeUnref = (streamingWatchdogTimer as { unref?: () => void }).unref;
@@ -391,6 +393,7 @@ export function createEventHandlers(context: EventHandlerContext) {
391393
if (reconnectPendingRunId === evt.runId) {
392394
reconnectPendingRunId = null;
393395
}
396+
chatLog.dismissPendingSystem(evt.runId);
394397
noteSessionRun(evt.runId);
395398
if (!state.activeChatRunId && !isLocalBtwRunId?.(evt.runId)) {
396399
state.activeChatRunId = evt.runId;

0 commit comments

Comments
 (0)