Skip to content

Commit 7e41913

Browse files
committed
fix(gateway): reduce TUI history startup latency
1 parent f4a9d34 commit 7e41913

6 files changed

Lines changed: 171 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ Docs: https://docs.openclaw.ai
2121
- Tasks: keep terminal mirrored TaskFlow timestamps pinned to task completion time and let maintenance repair stale mirrors, so ACP terminal delivery updates no longer leave inconsistent flow audits. Refs #73609. Thanks @joerod26.
2222
- Gateway/sessions: add conservative stuck-session recovery that releases only stale session lanes while active embedded runs, reply operations, and lane tasks remain serialized, so queued follow-ups can drain without aborting legitimate long-running turns. Refs #73581, #73655, #73652, #73705, #73647, #73602, #73592, and #73601. Thanks @WS-Q0758, @bryangauvin, @spenceryang1996-dot, @bmilne1981, @mattmcintyre, @Vksh07, and @Spolen23.
2323
- Plugins: cache unchanged plugin manifest loads by file signature, reducing repeated JSON/JSON5 parsing and manifest normalization in bursty startup and runtime registry paths. Refs #73532 and #73647; carries forward #73678. Thanks @TheDutchRuler.
24+
- CLI/TUI: keep `chat.history` off model-catalog discovery so initial Gateway-backed TUI history loads cannot block behind slow provider/plugin model scans on low-core hosts. Refs #73524. Thanks @harshcatsystems-collab.
25+
- Channels/WhatsApp: flag recently reconnected linked accounts in channel status even when the socket is currently healthy, so flapping WhatsApp Web sessions no longer look clean after a brief reconnect. Refs #73602. Thanks @Vksh07.
2426
- Agents/model selection: resolve slash-form aliases before provider/model parsing and keep alias-resolved primary models subject to transient provider cooldowns, so cron and persisted sessions do not retry cooled-down raw aliases. Fixes #73573 and #73657. Thanks @akai-shuuichi and @hashslingers.
2527
- Agents/Claude CLI: reuse already-cached macOS Keychain credentials for no-prompt Claude credential reads, so doctor/runtime checks do not miss fresh interactive Claude auth. Fixes #73682. Thanks @RyanSandoval.
2628
- Agents/transcripts: strip empty assistant text blocks while preserving valid text, images, and signatures, so Anthropic-style providers no longer reject sanitized transcript turns. Fixes #73640. Thanks @jowhee327.

docs/channels/troubleshooting.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ Healthy baseline:
3131

3232
### WhatsApp failure signatures
3333

34-
| Symptom | Fastest check | Fix |
35-
| ------------------------------- | --------------------------------------------------- | -------------------------------------------------------- |
36-
| Connected but no DM replies | `openclaw pairing list whatsapp` | Approve sender or switch DM policy/allowlist. |
37-
| Group messages ignored | Check `requireMention` + mention patterns in config | Mention the bot or relax mention policy for that group. |
38-
| QR login times out with 408 | Check gateway `HTTPS_PROXY` / `HTTP_PROXY` env | Set a reachable proxy; use `NO_PROXY` only for bypasses. |
39-
| Random disconnect/relogin loops | `openclaw channels status --probe` + logs | Re-login and verify credentials directory is healthy. |
34+
| Symptom | Fastest check | Fix |
35+
| ------------------------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
36+
| Connected but no DM replies | `openclaw pairing list whatsapp` | Approve sender or switch DM policy/allowlist. |
37+
| Group messages ignored | Check `requireMention` + mention patterns in config | Mention the bot or relax mention policy for that group. |
38+
| QR login times out with 408 | Check gateway `HTTPS_PROXY` / `HTTP_PROXY` env | Set a reachable proxy; use `NO_PROXY` only for bypasses. |
39+
| Random disconnect/relogin loops | `openclaw channels status --probe` + logs | Recent reconnects are flagged even when currently connected; watch logs, restart the gateway, then relink if flapping continues. |
4040

4141
Full troubleshooting: [WhatsApp troubleshooting](/channels/whatsapp#troubleshooting)
4242

extensions/whatsapp/src/status-issues.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,54 @@ describe("collectWhatsAppStatusIssues", () => {
8484
}),
8585
]);
8686
});
87+
88+
it("reports recently reconnected accounts even when the socket is currently healthy", () => {
89+
const issues = collectWhatsAppStatusIssues([
90+
{
91+
accountId: "default",
92+
enabled: true,
93+
linked: true,
94+
running: true,
95+
connected: true,
96+
reconnectAttempts: 3,
97+
healthState: "healthy",
98+
lastDisconnect: {
99+
at: Date.now() - 2 * 60_000,
100+
status: 408,
101+
error: "status=408 Request Time-out Connection was lost",
102+
},
103+
},
104+
]);
105+
106+
expect(issues).toEqual([
107+
expect.objectContaining({
108+
channel: "whatsapp",
109+
accountId: "default",
110+
kind: "runtime",
111+
message:
112+
"Linked but recently reconnected (reconnectAttempts=3): status=408 Request Time-out Connection was lost",
113+
}),
114+
]);
115+
});
116+
117+
it("does not report old reconnect history after a stable healthy period", () => {
118+
const issues = collectWhatsAppStatusIssues([
119+
{
120+
accountId: "default",
121+
enabled: true,
122+
linked: true,
123+
running: true,
124+
connected: true,
125+
reconnectAttempts: 1,
126+
healthState: "healthy",
127+
lastDisconnect: {
128+
at: Date.now() - 60 * 60_000,
129+
status: 408,
130+
error: "old disconnect",
131+
},
132+
},
133+
]);
134+
135+
expect(issues).toEqual([]);
136+
});
87137
});

extensions/whatsapp/src/status-issues.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@ type WhatsAppAccountStatus = {
1717
connected?: unknown;
1818
running?: unknown;
1919
reconnectAttempts?: unknown;
20+
lastDisconnect?: unknown;
2021
lastInboundAt?: unknown;
2122
lastError?: unknown;
2223
healthState?: unknown;
2324
};
2425

26+
const RECENT_DISCONNECT_WARNING_WINDOW_MS = 15 * 60 * 1000;
27+
2528
function readWhatsAppAccountStatus(value: ChannelAccountSnapshot): WhatsAppAccountStatus | null {
2629
if (!isRecord(value)) {
2730
return null;
@@ -34,12 +37,34 @@ function readWhatsAppAccountStatus(value: ChannelAccountSnapshot): WhatsAppAccou
3437
connected: value.connected,
3538
running: value.running,
3639
reconnectAttempts: value.reconnectAttempts,
40+
lastDisconnect: value.lastDisconnect,
3741
lastInboundAt: value.lastInboundAt,
3842
lastError: value.lastError,
3943
healthState: value.healthState,
4044
};
4145
}
4246

47+
function readLastDisconnect(value: unknown): { at: number | null; error?: string } | null {
48+
if (typeof value === "string") {
49+
const error = asString(value);
50+
return error ? { at: null, error } : null;
51+
}
52+
if (!isRecord(value)) {
53+
return null;
54+
}
55+
return {
56+
at: typeof value.at === "number" ? value.at : null,
57+
error: asString(value.error),
58+
};
59+
}
60+
61+
function isRecentDisconnect(disconnect: { at: number | null } | null, now = Date.now()): boolean {
62+
if (disconnect?.at == null) {
63+
return false;
64+
}
65+
return now - disconnect.at <= RECENT_DISCONNECT_WARNING_WINDOW_MS;
66+
}
67+
4368
export function collectWhatsAppStatusIssues(
4469
accounts: ChannelAccountSnapshot[],
4570
): ChannelStatusIssue[] {
@@ -55,7 +80,8 @@ export function collectWhatsAppStatusIssues(
5580
typeof account.reconnectAttempts === "number" ? account.reconnectAttempts : null;
5681
const lastInboundAt =
5782
typeof account.lastInboundAt === "number" ? account.lastInboundAt : null;
58-
const lastError = asString(account.lastError);
83+
const lastDisconnect = readLastDisconnect(account.lastDisconnect);
84+
const lastError = asString(account.lastError) ?? lastDisconnect?.error;
5985
const healthState = asString(account.healthState);
6086

6187
if (statusState === "unstable") {
@@ -127,6 +153,24 @@ export function collectWhatsAppStatusIssues(
127153
return;
128154
}
129155

156+
if (
157+
linked &&
158+
running &&
159+
connected &&
160+
reconnectAttempts != null &&
161+
reconnectAttempts > 0 &&
162+
isRecentDisconnect(lastDisconnect)
163+
) {
164+
issues.push({
165+
channel: "whatsapp",
166+
accountId,
167+
kind: "runtime",
168+
message: `Linked but recently reconnected (reconnectAttempts=${reconnectAttempts})${lastError ? `: ${lastError}` : "."}`,
169+
fix: `Watch: ${formatCliCommand("openclaw logs --follow")} and run ${formatCliCommand("openclaw channels status --probe")} if disconnects continue. If it keeps flapping, restart the gateway or relink via channels login.`,
170+
});
171+
return;
172+
}
173+
130174
if (running && !connected) {
131175
issues.push({
132176
channel: "whatsapp",

src/gateway/server-methods/chat.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1706,14 +1706,11 @@ export const chatHandlers: GatewayRequestHandlers = {
17061706
}
17071707
let thinkingLevel = entry?.thinkingLevel;
17081708
if (!thinkingLevel) {
1709-
const loadedCatalog = await context.loadGatewayModelCatalog().catch(() => undefined);
1710-
const modelCatalog = Array.isArray(loadedCatalog) ? loadedCatalog : undefined;
17111709
thinkingLevel = resolveGatewaySessionThinkingDefault({
17121710
cfg,
17131711
agentId: sessionAgentId,
17141712
provider: resolvedSessionModel.provider,
17151713
model: resolvedSessionModel.model,
1716-
modelCatalog,
17171714
});
17181715
}
17191716
const verboseLevel = entry?.verboseLevel ?? cfg.agents?.defaults?.verboseDefault;

src/gateway/server.chat.gateway-server-chat-b.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,74 @@ async function prepareMainHistoryHarness(params: {
135135
}
136136

137137
describe("gateway server chat", () => {
138+
test("chat.history does not wait for model catalog discovery to return history", async () => {
139+
const sessionDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-"));
140+
try {
141+
testState.sessionStorePath = path.join(sessionDir, "sessions.json");
142+
testState.agentConfig = {
143+
model: { primary: "test-provider/slow-catalog-model" },
144+
};
145+
await writeSessionStore({
146+
entries: {
147+
main: {
148+
sessionId: "sess-main",
149+
modelProvider: "test-provider",
150+
model: "slow-catalog-model",
151+
updatedAt: Date.now(),
152+
},
153+
},
154+
});
155+
const responses: Array<{ ok: boolean; payload?: unknown; error?: unknown }> = [];
156+
const context = {
157+
loadGatewayModelCatalog: vi.fn<GatewayRequestContext["loadGatewayModelCatalog"]>(
158+
async () => {
159+
throw new Error("model catalog should not load for chat.history");
160+
},
161+
),
162+
logGateway: {
163+
info: vi.fn(),
164+
warn: vi.fn(),
165+
error: vi.fn(),
166+
debug: vi.fn(),
167+
},
168+
} as unknown as GatewayRequestContext;
169+
const { chatHandlers } = await import("./server-methods/chat.js");
170+
171+
await chatHandlers["chat.history"]({
172+
req: {
173+
type: "req",
174+
id: "history-no-catalog",
175+
method: "chat.history",
176+
params: { sessionKey: "main" },
177+
},
178+
params: { sessionKey: "main" },
179+
client: null,
180+
isWebchatConnect: () => false,
181+
respond: ((ok, payload, error) => {
182+
responses.push({ ok, payload, error });
183+
}) as RespondFn,
184+
context,
185+
});
186+
187+
expect(context.loadGatewayModelCatalog).not.toHaveBeenCalled();
188+
expect(responses).toEqual([
189+
expect.objectContaining({
190+
ok: true,
191+
payload: expect.objectContaining({
192+
sessionKey: "main",
193+
sessionId: "sess-main",
194+
messages: expect.any(Array),
195+
}),
196+
}),
197+
]);
198+
} finally {
199+
clearConfigCache();
200+
testState.agentConfig = undefined;
201+
testState.sessionStorePath = undefined;
202+
await fs.rm(sessionDir, { recursive: true, force: true });
203+
}
204+
});
205+
138206
test("chat.send returns in_flight when duplicate attachment send wins parsing race", async () => {
139207
const sessionDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-"));
140208
const dispatchRelease = createDeferred<void>();

0 commit comments

Comments
 (0)