Skip to content

Commit b741ddb

Browse files
RomneyDasteipete
andauthored
fix(tui): dismiss watchdog notice when response actually arrives (#77375)
* 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. * fix(tui): adapt pending notice to repeatable system entries --------- Co-authored-by: Peter Steinberger <[email protected]>
1 parent d756e1c commit b741ddb

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
@@ -184,6 +184,7 @@ Docs: https://docs.openclaw.ai
184184
- Exec: keep configured `tools.exec.pathPrepend` entries ahead of user shell startup PATH changes on POSIX gateway runs. (#81403) Thanks @medns.
185185
- Gateway/sessions: allow shared-secret bearer callers to read and stream session history without an explicit scope header. (#81815) Thanks @medns.
186186
- 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.
187+
- 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.
187188
- 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.
188189

189190
## 2026.5.20

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

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

186+
it("dismisses a pending system notice by runId", () => {
187+
const chatLog = new ChatLog(40);
188+
189+
chatLog.addPendingSystem("run-1", "taking longer than expected");
190+
let rendered = chatLog.render(120).join("\n");
191+
expect(rendered).toContain("taking longer than expected");
192+
193+
const dismissed = chatLog.dismissPendingSystem("run-1");
194+
expect(dismissed).toBe(true);
195+
196+
rendered = chatLog.render(120).join("\n");
197+
expect(rendered).not.toContain("taking longer than expected");
198+
expect(chatLog.dismissPendingSystem("run-1")).toBe(false);
199+
});
200+
201+
it("replaces an existing pending system notice for the same runId", () => {
202+
const chatLog = new ChatLog(40);
203+
204+
chatLog.addPendingSystem("run-1", "first notice");
205+
chatLog.addPendingSystem("run-1", "second notice");
206+
207+
const rendered = chatLog.render(120).join("\n");
208+
expect(rendered).not.toContain("first notice");
209+
expect(rendered).toContain("second notice");
210+
expect(chatLog.children.length).toBe(1);
211+
});
212+
186213
it("does not hide a new repeated prompt when only older history matches", () => {
187214
const chatLog = new ChatLog(40);
188215

src/tui/components/chat-log.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export class ChatLog extends Container {
2727
createdAt: number;
2828
}
2929
>();
30+
private pendingSystemNotices = new Map<string, Container>();
3031
private btwMessage: BtwInlineMessage | null = null;
3132
private toolsExpanded = false;
3233
private repeatableSystemMessage: RepeatableSystemMessage | null = null;
@@ -52,6 +53,11 @@ export class ChatLog extends Container {
5253
this.pendingUsers.delete(runId);
5354
}
5455
}
56+
for (const [runId, entry] of this.pendingSystemNotices.entries()) {
57+
if (entry === component) {
58+
this.pendingSystemNotices.delete(runId);
59+
}
60+
}
5561
if (this.btwMessage === component) {
5662
this.btwMessage = null;
5763
}
@@ -85,6 +91,7 @@ export class ChatLog extends Container {
8591
this.clear();
8692
this.toolById.clear();
8793
this.streamingRuns.clear();
94+
this.pendingSystemNotices.clear();
8895
this.btwMessage = null;
8996
this.repeatableSystemMessage = null;
9097
if (!opts?.preservePendingUsers) {
@@ -142,6 +149,26 @@ export class ChatLog extends Container {
142149
this.repeatableSystemMessage = opts?.coalesceConsecutive ? message : null;
143150
}
144151

152+
addPendingSystem(runId: string, text: string) {
153+
const existing = this.pendingSystemNotices.get(runId);
154+
if (existing) {
155+
this.removeChild(existing);
156+
}
157+
const message = this.createSystemMessage(text);
158+
this.pendingSystemNotices.set(runId, message.component);
159+
this.append(message.component);
160+
}
161+
162+
dismissPendingSystem(runId: string) {
163+
const existing = this.pendingSystemNotices.get(runId);
164+
if (!existing) {
165+
return false;
166+
}
167+
this.removeChild(existing);
168+
this.pendingSystemNotices.delete(runId);
169+
return true;
170+
}
171+
145172
addUser(text: string) {
146173
this.appendNonSystem(new UserMessageComponent(text));
147174
}

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(),
@@ -1178,7 +1184,7 @@ describe("tui-event-handlers: streaming watchdog", () => {
11781184

11791185
expect(setActivityStatus).toHaveBeenLastCalledWith("idle");
11801186
expect(state.activeChatRunId).toBeNull();
1181-
expect(chatLog.addSystem).toHaveBeenCalledWith(expectedTimeoutMessage);
1187+
expect(chatLog.addPendingSystem).toHaveBeenCalledWith("run-stuck", expectedTimeoutMessage);
11821188

11831189
handlers.dispose?.();
11841190
});
@@ -1384,7 +1390,7 @@ describe("tui-event-handlers: streaming watchdog", () => {
13841390
expect(setActivityStatus).toHaveBeenLastCalledWith("idle");
13851391
expect(state.activeChatRunId).toBeNull();
13861392
expect(loadHistory).toHaveBeenCalledTimes(1);
1387-
expect(chatLog.addSystem).not.toHaveBeenCalledWith(expectedTimeoutMessage);
1393+
expect(chatLog.addPendingSystem).not.toHaveBeenCalled();
13881394

13891395
handlers.dispose?.();
13901396
});
@@ -1410,8 +1416,8 @@ describe("tui-event-handlers: streaming watchdog", () => {
14101416
vi.advanceTimersByTime(10_000);
14111417

14121418
const statusCalls = setActivityStatus.mock.calls.map((c) => c[0]);
1413-
expect(statusCalls.reduce((count, s) => count + (s === "idle" ? 1 : 0), 0)).toBe(1);
1414-
expect(chatLog.addSystem).not.toHaveBeenCalledWith(expectedTimeoutMessage);
1419+
expect(statusCalls.filter((s) => s === "idle").length).toBe(1);
1420+
expect(chatLog.addPendingSystem).not.toHaveBeenCalled();
14151421
expect(state.activeChatRunId).toBeNull();
14161422

14171423
handlers.dispose?.();
@@ -1432,7 +1438,7 @@ describe("tui-event-handlers: streaming watchdog", () => {
14321438
vi.advanceTimersByTime(60_000);
14331439

14341440
expect(setActivityStatus).not.toHaveBeenCalledWith("idle");
1435-
expect(chatLog.addSystem).not.toHaveBeenCalled();
1441+
expect(chatLog.addPendingSystem).not.toHaveBeenCalled();
14361442
expect(state.activeChatRunId).toBe("run-no-watchdog");
14371443

14381444
handlers.dispose?.();
@@ -1474,7 +1480,7 @@ describe("tui-event-handlers: streaming watchdog", () => {
14741480

14751481
expect(setActivityStatus).toHaveBeenLastCalledWith("idle");
14761482
expect(state.activeChatRunId).toBeNull();
1477-
expect(chatLog.addSystem).toHaveBeenCalledTimes(2);
1483+
expect(chatLog.addPendingSystem).toHaveBeenCalledTimes(2);
14781484

14791485
handlers.dispose?.();
14801486
});
@@ -1495,6 +1501,60 @@ describe("tui-event-handlers: streaming watchdog", () => {
14951501
vi.advanceTimersByTime(10_000);
14961502

14971503
expect(setActivityStatus).not.toHaveBeenCalledWith("idle");
1498-
expect(chatLog.addSystem).not.toHaveBeenCalled();
1504+
expect(chatLog.addPendingSystem).not.toHaveBeenCalled();
1505+
});
1506+
1507+
it("dismisses the watchdog notice when a delta arrives after the watchdog fires", () => {
1508+
const { state, chatLog, handlers } = createHarness({
1509+
streamingWatchdogMs: 5_000,
1510+
});
1511+
1512+
handlers.handleChatEvent({
1513+
runId: "run-late",
1514+
sessionKey: state.currentSessionKey,
1515+
state: "delta",
1516+
message: { content: "starting" },
1517+
} satisfies ChatEvent);
1518+
1519+
vi.advanceTimersByTime(5_001);
1520+
expect(chatLog.addPendingSystem).toHaveBeenCalledWith("run-late", expectedTimeoutMessage);
1521+
1522+
handlers.handleChatEvent({
1523+
runId: "run-late",
1524+
sessionKey: state.currentSessionKey,
1525+
state: "delta",
1526+
message: { content: "actually here" },
1527+
} satisfies ChatEvent);
1528+
1529+
expect(chatLog.dismissPendingSystem).toHaveBeenCalledWith("run-late");
1530+
1531+
handlers.dispose?.();
1532+
});
1533+
1534+
it("dismisses the watchdog notice when the final arrives after the watchdog fires", () => {
1535+
const { state, chatLog, handlers } = createHarness({
1536+
streamingWatchdogMs: 5_000,
1537+
});
1538+
1539+
handlers.handleChatEvent({
1540+
runId: "run-final-late",
1541+
sessionKey: state.currentSessionKey,
1542+
state: "delta",
1543+
message: { content: "starting" },
1544+
} satisfies ChatEvent);
1545+
1546+
vi.advanceTimersByTime(5_001);
1547+
expect(chatLog.addPendingSystem).toHaveBeenCalledWith("run-final-late", expectedTimeoutMessage);
1548+
1549+
handlers.handleChatEvent({
1550+
runId: "run-final-late",
1551+
sessionKey: state.currentSessionKey,
1552+
state: "final",
1553+
message: { content: [{ type: "text", text: "done" }], stopReason: "stop" },
1554+
} satisfies ChatEvent);
1555+
1556+
expect(chatLog.dismissPendingSystem).toHaveBeenCalledWith("run-final-late");
1557+
1558+
handlers.dispose?.();
14991559
});
15001560
});

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;
@@ -132,7 +134,7 @@ export function createEventHandlers(context: EventHandlerContext) {
132134
return;
133135
}
134136
flushPendingHistoryRefreshIfIdle();
135-
chatLog.addSystem(STREAMING_WATCHDOG_USER_MESSAGE);
137+
chatLog.addPendingSystem(runId, STREAMING_WATCHDOG_USER_MESSAGE);
136138
tui.requestRender();
137139
}, streamingWatchdogMs);
138140
const maybeUnref = (streamingWatchdogTimer as { unref?: () => void }).unref;
@@ -398,6 +400,7 @@ export function createEventHandlers(context: EventHandlerContext) {
398400
if (reconnectPendingRunId === evt.runId) {
399401
reconnectPendingRunId = null;
400402
}
403+
chatLog.dismissPendingSystem(evt.runId);
401404
noteSessionRun(evt.runId);
402405
if (!state.activeChatRunId && !isLocalBtwRunId?.(evt.runId)) {
403406
state.activeChatRunId = evt.runId;

0 commit comments

Comments
 (0)