mirror of
https://gh-proxy.org/https://github.com/klinsmaya/LobsterAI.git
synced 2026-11-10 18:26:44 +08:00
Merge pull request #509 from netease-youdao/fix/client-side-timeout-watchdog
fix: add client-side timeout watchdog for stuck running state
This commit is contained in:
@@ -23,6 +23,9 @@ const translations: Record<LanguageType, Record<string, string>> = {
|
||||
|
||||
// Session titles (created by ChannelSessionSync)
|
||||
cronSessionPrefix: '定时',
|
||||
|
||||
// Timeout hint
|
||||
taskTimedOut: '[任务超时] 任务因超过最大允许时长而被自动停止。你可以继续对话以从中断处继续。',
|
||||
},
|
||||
en: {
|
||||
// Tray menu
|
||||
@@ -33,6 +36,9 @@ const translations: Record<LanguageType, Record<string, string>> = {
|
||||
|
||||
// Session titles
|
||||
cronSessionPrefix: 'Cron',
|
||||
|
||||
// Timeout hint
|
||||
taskTimedOut: '[Task timed out] The task was automatically stopped because it exceeded the maximum allowed duration. You can continue the conversation to pick up where it left off.',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ import {
|
||||
extractGatewayMessageText,
|
||||
} from '../openclawHistory';
|
||||
import { buildOpenClawLocalTimeContextPrompt } from '../openclawLocalTimeContextPrompt';
|
||||
import { OPENCLAW_AGENT_TIMEOUT_SECONDS } from '../openclawConfigSync';
|
||||
import { t } from '../../i18n';
|
||||
|
||||
const OPENCLAW_GATEWAY_TOOL_EVENTS_CAP = 'tool-events';
|
||||
const BRIDGE_MAX_MESSAGES = 20;
|
||||
@@ -116,6 +118,8 @@ type ActiveTurn = {
|
||||
bufferedChatPayloads: BufferedChatEvent[];
|
||||
/** Agent events buffered while pendingUserSync is true. */
|
||||
bufferedAgentPayloads: BufferedAgentEvent[];
|
||||
/** Client-side timeout watchdog timer (fallback for missing gateway abort events). */
|
||||
timeoutTimer?: ReturnType<typeof setTimeout>;
|
||||
};
|
||||
|
||||
type BufferedChatEvent = {
|
||||
@@ -554,6 +558,14 @@ export class OpenClawRuntimeAdapter extends EventEmitter implements CoworkRuntim
|
||||
private pendingMessageUpdateTimer: Map<string, ReturnType<typeof setTimeout>> = new Map();
|
||||
private static readonly MESSAGE_UPDATE_THROTTLE_MS = 100;
|
||||
|
||||
/**
|
||||
* Server-side agent timeout in seconds (mirrors agents.defaults.timeoutSeconds in openclaw config).
|
||||
* Used to set a client-side fallback timer that fires slightly after the server timeout,
|
||||
* so LobsterAI can recover even when the gateway fails to deliver the abort event.
|
||||
*/
|
||||
agentTimeoutSeconds = OPENCLAW_AGENT_TIMEOUT_SECONDS;
|
||||
private static readonly CLIENT_TIMEOUT_GRACE_MS = 30_000;
|
||||
|
||||
constructor(store: CoworkStore, engineManager: OpenClawEngineManager) {
|
||||
super();
|
||||
this.store = store;
|
||||
@@ -987,6 +999,13 @@ export class OpenClawRuntimeAdapter extends EventEmitter implements CoworkRuntim
|
||||
});
|
||||
this.sessionIdByRunId.set(runId, sessionId);
|
||||
|
||||
// Start client-side timeout watchdog.
|
||||
// OpenClaw gateway has a known issue where embedded run timeouts may not
|
||||
// produce a WS abort/final event (the subscription is torn down before the
|
||||
// lifecycle event fires). This timer fires slightly after the server-side
|
||||
// timeout to recover the UI from a stuck "running" state.
|
||||
this.startTurnTimeoutWatchdog(sessionId);
|
||||
|
||||
const client = this.requireGatewayClient();
|
||||
try {
|
||||
const attachments = options.imageAttachments?.length
|
||||
@@ -2285,6 +2304,14 @@ export class OpenClawRuntimeAdapter extends EventEmitter implements CoworkRuntim
|
||||
private handleChatAborted(sessionId: string, turn: ActiveTurn): void {
|
||||
this.store.updateSession(sessionId, { status: 'idle' });
|
||||
if (!turn.stopRequested) {
|
||||
// The run was aborted without user request — most likely a timeout.
|
||||
// Add a visible hint so the user knows the task was interrupted.
|
||||
const hintMessage = this.store.addMessage(sessionId, {
|
||||
type: 'assistant',
|
||||
content: t('taskTimedOut'),
|
||||
metadata: { isTimeout: true },
|
||||
});
|
||||
this.emit('message', sessionId, hintMessage);
|
||||
this.emit('complete', sessionId, turn.runId);
|
||||
}
|
||||
const abortedSessionKey = turn.sessionKey;
|
||||
@@ -3162,6 +3189,11 @@ export class OpenClawRuntimeAdapter extends EventEmitter implements CoworkRuntim
|
||||
private cleanupSessionTurn(sessionId: string): void {
|
||||
const turn = this.activeTurns.get(sessionId);
|
||||
if (turn) {
|
||||
// Clear client-side timeout watchdog
|
||||
if (turn.timeoutTimer) {
|
||||
clearTimeout(turn.timeoutTimer);
|
||||
turn.timeoutTimer = undefined;
|
||||
}
|
||||
// Cancel any pending throttled messageUpdate timer for this turn
|
||||
if (turn.assistantMessageId) {
|
||||
this.clearPendingMessageUpdate(turn.assistantMessageId);
|
||||
@@ -3182,6 +3214,27 @@ export class OpenClawRuntimeAdapter extends EventEmitter implements CoworkRuntim
|
||||
this.reCreatedChannelSessionIds.delete(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a client-side timeout watchdog for a turn.
|
||||
* Fires after the server-side timeout + grace period, recovering the UI
|
||||
* if the gateway fails to deliver the abort/final event.
|
||||
*/
|
||||
private startTurnTimeoutWatchdog(sessionId: string): void {
|
||||
const turn = this.activeTurns.get(sessionId);
|
||||
if (!turn) return;
|
||||
const timeoutMs = this.agentTimeoutSeconds * 1000
|
||||
+ OpenClawRuntimeAdapter.CLIENT_TIMEOUT_GRACE_MS;
|
||||
turn.timeoutTimer = setTimeout(() => {
|
||||
const currentTurn = this.activeTurns.get(sessionId);
|
||||
if (!currentTurn || currentTurn.turnToken !== turn.turnToken) return;
|
||||
console.warn(
|
||||
`[OpenClawRuntime] Client-side timeout watchdog fired for session ${sessionId}, `
|
||||
+ `runId=${currentTurn.runId} after ${timeoutMs}ms — gateway did not deliver abort event`,
|
||||
);
|
||||
this.handleChatAborted(sessionId, currentTurn);
|
||||
}, timeoutMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a session is deleted from the store.
|
||||
* Purges all in-memory references so that new channel messages
|
||||
@@ -3265,6 +3318,7 @@ export class OpenClawRuntimeAdapter extends EventEmitter implements CoworkRuntim
|
||||
this.sessionIdByRunId.set(runId, sessionId);
|
||||
}
|
||||
this.store.updateSession(sessionId, { status: 'running' });
|
||||
this.startTurnTimeoutWatchdog(sessionId);
|
||||
|
||||
// For channel sessions, prefetch user messages before streaming starts
|
||||
if (isChannel) {
|
||||
|
||||
@@ -22,6 +22,12 @@ const mapExecutionModeToSandboxMode = (_mode: CoworkExecutionMode): 'off' | 'non
|
||||
return 'off';
|
||||
};
|
||||
|
||||
/**
|
||||
* Default agent timeout in seconds written to openclaw config.
|
||||
* Also used by the runtime adapter's client-side timeout watchdog.
|
||||
*/
|
||||
export const OPENCLAW_AGENT_TIMEOUT_SECONDS = 3600;
|
||||
|
||||
const mapApiTypeToOpenClawApi = (apiType: 'anthropic' | 'openai' | undefined): 'anthropic-messages' | 'openai-completions' => {
|
||||
return apiType === 'openai' ? 'openai-completions' : 'anthropic-messages';
|
||||
};
|
||||
@@ -538,6 +544,7 @@ export class OpenClawConfigSync {
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
timeoutSeconds: OPENCLAW_AGENT_TIMEOUT_SECONDS,
|
||||
model: {
|
||||
primary: providerSelection.primaryModel,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user