Skip to content

Commit b0f7ac6

Browse files
Joseph Felix BAMBAclaude
andcommitted
feat(tui,cli-runner): expose streamingWatchdogMs config + bump watchdog defaults
F16 — TUI streaming-watchdog wiring: - Adds `cli.tui.streamingWatchdogMs` config field (zod-validated, optional). - `runTui` now reads this value and passes it to `createEventHandlers`, closing the gap where `context.streamingWatchdogMs` was a consumer-side knob with no producer-side wiring. - Raises the in-code default from 30s to 600000ms (10 minutes). The 30s default tripped on long-reasoning models, slow tool runs, and SSE keepalive gaps; the 10m value matches the empirically validated downstream patch. - Adds `resolveTuiStreamingWatchdogMs(config)` helper + unit tests for config-shape edge cases (NaN, negative, Infinity, fractional, string). - Adds tui-event-handlers test verifying the new default fires at 10m (not 30s) when no override is supplied. F18 — CLI watchdog defaults: - Raises `CLI_FRESH_WATCHDOG_DEFAULTS.maxMs` from 600000 (10m) to 3600000 (60m). - Raises `CLI_RESUME_WATCHDOG_DEFAULTS.maxMs` from 180000 (3m) to 3600000 (60m). - Per-CLI-backend overrides at `cli.watchdog.fresh.maxMs` / `.resume.maxMs` continue to apply unchanged; this commit only moves the fallback ceiling so users without a per-backend override don't see false aborts on long but legitimate runs. Backward compatibility: - `cli.tui.streamingWatchdogMs` is optional; existing configs without it pick up the new 10m default rather than the prior 30s. - The CLI watchdog defaults change only affects callers that did NOT set their own per-backend ratio/min/max, where previously the computed `maxMs` ceiling was 600000/180000. - All existing tests for `resolveCliNoOutputTimeoutMs` continue to pass because their assertions (e.g. 480000 = 600000 * 0.8) sit below the new ceiling. Docs: - `docs/gateway/configuration-reference.md` documents the new `cli.tui.streamingWatchdogMs` field with default + disable semantics. Co-Authored-By: Forge <[email protected]>
1 parent 2ad0282 commit b0f7ac6

9 files changed

Lines changed: 127 additions & 3 deletions

File tree

CHANGELOG.md

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

77
### Changes
88

9+
- TUI/CLI runner: expose `cli.tui.streamingWatchdogMs` config field, raise the in-code streaming watchdog default from 30s to 10m, and lift `CLI_FRESH_WATCHDOG_DEFAULTS.maxMs` (10m → 60m) and `CLI_RESUME_WATCHDOG_DEFAULTS.maxMs` (3m → 60m) so long-reasoning models, slow tool runs, and SSE keepalive gaps stop tripping false aborts; per-backend overrides at `cli.watchdog.fresh.maxMs`/`.resume.maxMs` continue to apply unchanged.
910
- Chat commands: add `/think default` and `/fast default` to clear session overrides and inherit configured/provider defaults. (#79385) Thanks @VACInc.
1011
- Dependencies: refresh workspace dependency pins and lockfile, including `@openai/codex` `0.130.0`, `acpx` `0.7.0`, AWS SDK `3.1044.0`, OpenTelemetry `0.217.0`, `typebox` `1.1.38`, `vite` `8.0.11`, `oxfmt` `0.48.0`, and `oxlint` `1.63.0`, and update the Codex harness model snapshot for the new bundled app-server catalog.
1112
- Plugins/install: add guarded plugin install overrides so onboarding and repair tests can route specific plugins to registry specs or local `npm pack` artifacts via environment variables.

docs/gateway/configuration-reference.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1145,6 +1145,9 @@ Notes:
11451145
banner: {
11461146
taglineMode: "off", // random | default | off
11471147
},
1148+
tui: {
1149+
streamingWatchdogMs: 600000, // default 10m; 0 disables
1150+
},
11481151
},
11491152
}
11501153
```
@@ -1154,6 +1157,11 @@ Notes:
11541157
- `"default"`: fixed neutral tagline (`All your chats, one OpenClaw.`).
11551158
- `"off"`: no tagline text (banner title/version still shown).
11561159
- To hide the entire banner (not just taglines), set env `OPENCLAW_HIDE_BANNER=1`.
1160+
- `cli.tui.streamingWatchdogMs` is the no-stream-delta window after which the
1161+
interactive TUI resets activity status to idle and posts the "taking longer
1162+
than expected" notice. Default: `600000` (10 minutes). Set to `0` to disable.
1163+
Raise this when working with long-reasoning models, slow tool runs, or
1164+
upstreams with long SSE keepalive gaps.
11571165

11581166
---
11591167

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
export const CLI_WATCHDOG_MIN_TIMEOUT_MS = 1_000;
22

3+
// `maxMs` caps the "no output" watchdog ceiling. Originally 600_000 (10m) for fresh
4+
// runs and 180_000 (3m) for resume. Raised to 60m: long-form reasoning, tool calls
5+
// that legitimately take 5–10m, and SSE keepalive gaps were tripping false aborts.
6+
// Per-CLI-backend overrides at `cli.watchdog.fresh.maxMs` / `.resume.maxMs` still apply.
37
export const CLI_FRESH_WATCHDOG_DEFAULTS = {
48
noOutputTimeoutRatio: 0.8,
59
minMs: 180_000,
6-
maxMs: 600_000,
10+
maxMs: 3_600_000,
711
} as const;
812

913
export const CLI_RESUME_WATCHDOG_DEFAULTS = {
1014
noOutputTimeoutRatio: 0.3,
1115
minMs: 60_000,
12-
maxMs: 180_000,
16+
maxMs: 3_600_000,
1317
} as const;

src/config/types.cli.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
export type CliBannerTaglineMode = "random" | "default" | "off";
22

3+
export type CliTuiConfig = {
4+
/**
5+
* "No stream delta arrived" watchdog window for the TUI. When the upstream
6+
* stream stalls for this long while a run is active, the TUI resets activity
7+
* status to idle and posts the "taking longer than expected" notice.
8+
* Default: 600000 (10 minutes). Set to 0 to disable.
9+
*/
10+
streamingWatchdogMs?: number;
11+
};
12+
313
export type CliConfig = {
414
banner?: {
515
/**
@@ -10,4 +20,6 @@ export type CliConfig = {
1020
*/
1121
taglineMode?: CliBannerTaglineMode;
1222
};
23+
/** TUI-side runtime knobs (interactive `openclaw` chat). */
24+
tui?: CliTuiConfig;
1325
};

src/config/zod-schema.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,12 @@ export const OpenClawSchema = z
469469
})
470470
.strict()
471471
.optional(),
472+
tui: z
473+
.object({
474+
streamingWatchdogMs: z.number().int().nonnegative().optional(),
475+
})
476+
.strict()
477+
.optional(),
472478
})
473479
.strict()
474480
.optional(),

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1233,6 +1233,30 @@ describe("tui-event-handlers: streaming watchdog", () => {
12331233
handlers.dispose?.();
12341234
});
12351235

1236+
it("falls back to the 10-minute default when streamingWatchdogMs is undefined", () => {
1237+
const { state, chatLog, setActivityStatus, handlers } = createHarness();
1238+
1239+
handlers.handleChatEvent({
1240+
runId: "run-default",
1241+
sessionKey: state.currentSessionKey,
1242+
state: "delta",
1243+
message: { content: "hi" },
1244+
} satisfies ChatEvent);
1245+
1246+
// Old default was 30s — verify it does NOT fire at 31s anymore.
1247+
vi.advanceTimersByTime(31_000);
1248+
expect(setActivityStatus).not.toHaveBeenCalledWith("idle");
1249+
expect(state.activeChatRunId).toBe("run-default");
1250+
1251+
// New default is 10m — verify it DOES fire just past the 10m boundary.
1252+
vi.advanceTimersByTime(600_001 - 31_000);
1253+
expect(setActivityStatus).toHaveBeenLastCalledWith("idle");
1254+
expect(state.activeChatRunId).toBeNull();
1255+
expect(chatLog.addSystem).toHaveBeenCalledWith(expectedTimeoutMessage);
1256+
1257+
handlers.dispose?.();
1258+
});
1259+
12361260
it("is disabled when streamingWatchdogMs is 0", () => {
12371261
const { state, chatLog, setActivityStatus, handlers } = createHarness({
12381262
streamingWatchdogMs: 0,

src/tui/tui-event-handlers.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@ type EventHandlerContext = {
4848
localMode?: boolean;
4949
};
5050

51-
const DEFAULT_STREAMING_WATCHDOG_MS = 30_000;
51+
// Streaming-watchdog default fires "response is taking longer than expected" when
52+
// no stream delta arrives for this long. Originally 30s, raised to 10m so long
53+
// reasoning / tool runs and SSE keepalive gaps don't cause spurious "idle" trips.
54+
// Override per-install via `cli.tui.streamingWatchdogMs`.
55+
const DEFAULT_STREAMING_WATCHDOG_MS = 600_000;
5256
const STREAMING_WATCHDOG_USER_MESSAGE =
5357
"This response is taking longer than expected. Send another message to continue.";
5458

src/tui/tui.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
resolveLocalAuthSpawnCwd,
2020
resolveLocalAuthSpawnOptions,
2121
resolveTuiSessionKey,
22+
resolveTuiStreamingWatchdogMs,
2223
stopTuiSafely,
2324
} from "./tui.js";
2425

@@ -498,3 +499,58 @@ describe("resolveLocalAuthSpawnCwd", () => {
498499
).toBe("/worktree/subdir");
499500
});
500501
});
502+
503+
describe("resolveTuiStreamingWatchdogMs", () => {
504+
it("returns undefined when cli.tui.streamingWatchdogMs is unset", () => {
505+
expect(resolveTuiStreamingWatchdogMs({} as OpenClawConfig)).toBeUndefined();
506+
expect(resolveTuiStreamingWatchdogMs({ cli: {} } as OpenClawConfig)).toBeUndefined();
507+
expect(resolveTuiStreamingWatchdogMs({ cli: { tui: {} } } as OpenClawConfig)).toBeUndefined();
508+
});
509+
510+
it("returns the configured value when finite and non-negative", () => {
511+
expect(
512+
resolveTuiStreamingWatchdogMs({
513+
cli: { tui: { streamingWatchdogMs: 900_000 } },
514+
} as OpenClawConfig),
515+
).toBe(900_000);
516+
});
517+
518+
it("preserves 0 (caller-side disable signal)", () => {
519+
expect(
520+
resolveTuiStreamingWatchdogMs({
521+
cli: { tui: { streamingWatchdogMs: 0 } },
522+
} as OpenClawConfig),
523+
).toBe(0);
524+
});
525+
526+
it("floors fractional values", () => {
527+
expect(
528+
resolveTuiStreamingWatchdogMs({
529+
cli: { tui: { streamingWatchdogMs: 1234.9 } },
530+
} as OpenClawConfig),
531+
).toBe(1234);
532+
});
533+
534+
it("rejects negative, NaN, Infinity, and non-number inputs", () => {
535+
expect(
536+
resolveTuiStreamingWatchdogMs({
537+
cli: { tui: { streamingWatchdogMs: -10 } },
538+
} as OpenClawConfig),
539+
).toBeUndefined();
540+
expect(
541+
resolveTuiStreamingWatchdogMs({
542+
cli: { tui: { streamingWatchdogMs: Number.NaN } },
543+
} as OpenClawConfig),
544+
).toBeUndefined();
545+
expect(
546+
resolveTuiStreamingWatchdogMs({
547+
cli: { tui: { streamingWatchdogMs: Number.POSITIVE_INFINITY } },
548+
} as OpenClawConfig),
549+
).toBeUndefined();
550+
expect(
551+
resolveTuiStreamingWatchdogMs({
552+
cli: { tui: { streamingWatchdogMs: "600000" as unknown as number } },
553+
} as OpenClawConfig),
554+
).toBeUndefined();
555+
});
556+
});

src/tui/tui.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,14 @@ const OPENCLAW_DIST_ENTRY_MJS_PATH = fileURLToPath(
7878

7979
const OPENAI_CODEX_PROVIDER = "openai-codex";
8080

81+
export function resolveTuiStreamingWatchdogMs(config: OpenClawConfig): number | undefined {
82+
const value = config.cli?.tui?.streamingWatchdogMs;
83+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
84+
return undefined;
85+
}
86+
return Math.floor(value);
87+
}
88+
8189
type RunTuiOptions = TuiOptions & {
8290
backend?: TuiBackend;
8391
config?: OpenClawConfig;
@@ -1083,6 +1091,7 @@ export async function runTui(opts: RunTuiOptions): Promise<TuiResult> {
10831091
isLocalBtwRunId,
10841092
forgetLocalBtwRunId,
10851093
clearLocalBtwRunIds,
1094+
streamingWatchdogMs: resolveTuiStreamingWatchdogMs(config),
10861095
});
10871096

10881097
const deferredFinish = createDeferredTuiFinish();

0 commit comments

Comments
 (0)