Skip to content

Commit 6d7a77d

Browse files
authored
fix(tui): recover stale streaming status after unbound final (#73749)
* fix(tui): clear stale streaming after unbound final events * fix(clownfish): address review for ghcrawl-156749-autonomous-smoke (1) * fix(tui): address stale streaming review
1 parent 77a5d82 commit 6d7a77d

3 files changed

Lines changed: 115 additions & 9 deletions

File tree

CHANGELOG.md

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

2121
- Agents/auth: keep OAuth auth profiles inherited from the main agent read-through instead of copying refresh tokens into secondary agents, and refresh Codex app-server tokens against the owning store so multi-agent swarms avoid reused refresh-token failures. Fixes #74055. Thanks @ClarityInvest.
2222
- ACP/commands: accept forwarded ACP timeout config controls in the OpenClaw bridge, treat unsupported discard-close controls as recoverable cleanup, and restore native `/verbose full` plus no-arg status behavior, so Discord command menus and nested ACP turns no longer fail on supported session controls. Thanks @vincentkoc.
23+
- TUI/status: clear stale `streaming` footer state when a final event arrives after the active run was already cleared and no tracked runs remain, while preserving concurrent-run ownership and inactive local `/btw` terminal handling. Fixes #64825; carries forward #64842, #64843, #64847, and #64862. Thanks @briandevans and @Yanhu007.
2324
- Channels/Discord: fail startup closed when Discord cannot resolve the bot's own identity and keep mention gating active when only configured mention patterns can detect mentions, so the provider no longer continues with a missing bot id. Fixes #42219; carries forward #46856 and #49218. Thanks @education-01 and @BenediktSchackenberg.
2425
- Channels/Discord: split long CJK replies at punctuation and code-point-safe fallback boundaries so Discord chunking stays readable without corrupting astral characters. Fixes #38597; repairs #71384. Thanks @p3nchan.
2526
- Browser/gateway: ignore Playwright dialog-close races from `Page.handleJavaScriptDialog` so browser automation no longer crashes the Gateway when a dialog disappears before Playwright accepts it. (#40067) Thanks @randyjtw.

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

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,46 @@ describe("tui-event-handlers: handleAgentEvent", () => {
306306
});
307307
});
308308

309+
it("clears stale streaming for a local BTW empty final without hiding the result", () => {
310+
const {
311+
state,
312+
btw,
313+
loadHistory,
314+
setActivityStatus,
315+
noteLocalBtwRunId,
316+
handleBtwEvent,
317+
handleChatEvent,
318+
} = createHandlersHarness({
319+
state: { activeChatRunId: null, activityStatus: "streaming" },
320+
});
321+
322+
noteLocalBtwRunId("run-btw");
323+
handleBtwEvent({
324+
kind: "btw",
325+
runId: "run-btw",
326+
sessionKey: state.currentSessionKey,
327+
question: "what changed?",
328+
text: "nothing important",
329+
} satisfies BtwEvent);
330+
setActivityStatus.mockClear();
331+
332+
handleChatEvent({
333+
runId: "run-btw",
334+
sessionKey: state.currentSessionKey,
335+
state: "final",
336+
} satisfies ChatEvent);
337+
338+
expect(state.activeChatRunId).toBeNull();
339+
expect(state.activityStatus).toBe("idle");
340+
expect(setActivityStatus).toHaveBeenCalledWith("idle");
341+
expect(loadHistory).not.toHaveBeenCalled();
342+
expect(btw.showResult).toHaveBeenCalledWith({
343+
question: "what changed?",
344+
text: "nothing important",
345+
isError: undefined,
346+
});
347+
});
348+
309349
it("does not cross-match canonical session keys from different agents", () => {
310350
const { chatLog, handleChatEvent } = createHandlersHarness({
311351
state: {
@@ -548,6 +588,48 @@ describe("tui-event-handlers: handleAgentEvent", () => {
548588
expect(setActivityStatus).toHaveBeenCalledWith("idle");
549589
});
550590

591+
it("clears stale streaming when a duplicate final arrives after inactive /btw terminal cleanup", () => {
592+
const { state, setActivityStatus, noteLocalBtwRunId, handleChatEvent } = createHandlersHarness({
593+
state: { activeChatRunId: null, activityStatus: "streaming" },
594+
});
595+
596+
handleChatEvent({
597+
runId: "run-finalized",
598+
sessionKey: state.currentSessionKey,
599+
state: "final",
600+
message: { content: [{ type: "text", text: "done" }] },
601+
});
602+
603+
noteLocalBtwRunId("run-btw-error");
604+
handleChatEvent({
605+
runId: "run-btw-error",
606+
sessionKey: state.currentSessionKey,
607+
state: "delta",
608+
message: { content: "background status update" },
609+
});
610+
handleChatEvent({
611+
runId: "run-btw-error",
612+
sessionKey: state.currentSessionKey,
613+
state: "error",
614+
errorMessage: "background failure",
615+
});
616+
617+
expect(state.activeChatRunId).toBeNull();
618+
expect(state.activityStatus).toBe("streaming");
619+
setActivityStatus.mockClear();
620+
621+
handleChatEvent({
622+
runId: "run-finalized",
623+
sessionKey: state.currentSessionKey,
624+
state: "final",
625+
message: { content: [{ type: "text", text: "done" }] },
626+
});
627+
628+
expect(state.activeChatRunId).toBeNull();
629+
expect(state.activityStatus).toBe("idle");
630+
expect(setActivityStatus).toHaveBeenCalledWith("idle");
631+
});
632+
551633
it("flushes deferred history reload after stale streaming clear makes the TUI idle", () => {
552634
const { state, loadHistory, noteLocalRunId, setActivityStatus, handleChatEvent } =
553635
createHandlersHarness({
@@ -589,6 +671,31 @@ describe("tui-event-handlers: handleAgentEvent", () => {
589671
expect(setActivityStatus).not.toHaveBeenCalledWith("error");
590672
});
591673

674+
it("does not clear global streaming for inactive local /btw aborted or error events", () => {
675+
const { state, setActivityStatus, noteLocalBtwRunId, handleChatEvent } = createHandlersHarness({
676+
state: { activeChatRunId: null, activityStatus: "streaming" },
677+
});
678+
679+
for (const terminalState of ["aborted", "error"] as const) {
680+
const runId = `run-btw-${terminalState}`;
681+
state.activeChatRunId = null;
682+
state.activityStatus = "streaming";
683+
setActivityStatus.mockClear();
684+
noteLocalBtwRunId(runId);
685+
686+
handleChatEvent({
687+
runId,
688+
sessionKey: state.currentSessionKey,
689+
state: terminalState,
690+
errorMessage: terminalState === "error" ? "boom" : undefined,
691+
});
692+
693+
expect(state.activeChatRunId).toBeNull();
694+
expect(state.activityStatus).toBe("streaming");
695+
expect(setActivityStatus).not.toHaveBeenCalled();
696+
}
697+
});
698+
592699
it("does not force idle for an inactive final while another tracked run is active", () => {
593700
const { state, setActivityStatus, handleChatEvent } = createConcurrentRunHarness("partial");
594701
state.activityStatus = "streaming";

src/tui/tui-event-handlers.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -196,14 +196,11 @@ export function createEventHandlers(context: EventHandlerContext) {
196196
}
197197
};
198198

199-
const clearStaleStreamingRunIfNoTrackedRunRemains = () => {
199+
const clearStaleStreamingIfNoTrackedRunRemains = () => {
200200
const activeRunId = state.activeChatRunId;
201-
if (
202-
!activeRunId ||
203-
sessionRuns.has(activeRunId) ||
204-
sessionRuns.size > 0 ||
205-
state.activityStatus !== "streaming"
206-
) {
201+
// A missing active run is the recovery case; only tracked active runs block cleanup.
202+
const activeRunIsStillTracked = activeRunId ? sessionRuns.has(activeRunId) : false;
203+
if (state.activityStatus !== "streaming" || activeRunIsStillTracked || sessionRuns.size > 0) {
207204
return;
208205
}
209206
state.activeChatRunId = null;
@@ -228,7 +225,7 @@ export function createEventHandlers(context: EventHandlerContext) {
228225
if (streamingWatchdogRunId === params.runId) {
229226
clearStreamingWatchdog();
230227
}
231-
clearStaleStreamingRunIfNoTrackedRunRemains();
228+
clearStaleStreamingIfNoTrackedRunRemains();
232229
}
233230
void refreshSessionInfo?.();
234231
};
@@ -249,7 +246,6 @@ export function createEventHandlers(context: EventHandlerContext) {
249246
if (streamingWatchdogRunId === params.runId) {
250247
clearStreamingWatchdog();
251248
}
252-
clearStaleStreamingRunIfNoTrackedRunRemains();
253249
}
254250
void refreshSessionInfo?.();
255251
};
@@ -324,6 +320,7 @@ export function createEventHandlers(context: EventHandlerContext) {
324320
return;
325321
}
326322
if (evt.state === "final") {
323+
clearStaleStreamingIfNoTrackedRunRemains();
327324
return;
328325
}
329326
}
@@ -355,6 +352,7 @@ export function createEventHandlers(context: EventHandlerContext) {
355352
if (!evt.message && isLocalBtwRun) {
356353
forgetLocalBtwRunId?.(evt.runId);
357354
noteFinalizedRun(evt.runId);
355+
clearStaleStreamingIfNoTrackedRunRemains();
358356
tui.requestRender();
359357
return;
360358
}

0 commit comments

Comments
 (0)