Fix subagent announce failover race (always emit lifecycle end + treat timeout=0 as no-timeout) (#6621)
* Fix subagent announce race and timeout handling Bug 1: Subagent announce fires before model failover retries finish - Problem: CLI provider emitted lifecycle error on each attempt, causing subagent registry to prematurely call beginSubagentCleanup() and announce with incorrect status before failover retries completed - Fix: Removed lifecycle error emission from CLI provider's attempt-level .catch() in agent-runner-execution.ts. Errors still propagate to runWithModelFallback for retry, but no intermediate lifecycle events are emitted. Only the final outcome (after all retries) emits lifecycle events. Bug 2: Hard 600s per-prompt timeout ignores runTimeoutSeconds=0 - Problem: When runTimeoutSeconds=0 (meaning 'no timeout'), the code returned the default 600s timeout instead of respecting the 0 setting - Fix: Modified resolveAgentTimeoutMs() to treat 0 as 'no timeout' and return a very large timeout value (30 days) instead of the default. This avoids setTimeout issues with Infinity while effectively providing unlimited time for long-running tasks. * fix: emit lifecycle:error for CLI failures (#6621) (thanks @tyler6204) * chore: satisfy format/lint gates (#6621) (thanks @tyler6204) * fix: restore build after upstream type changes (#6621) (thanks @tyler6204) * test: fix createSystemPromptOverride tests to match new return type (#6621) (thanks @tyler6204)
This commit is contained in:
parent
d5f6caba3f
commit
8d2f98fb01
14 changed files with 81 additions and 42 deletions
|
|
@ -2167,10 +2167,9 @@ async function processMessage(
|
||||||
sendBlueBubblesTyping(chatGuidForActions, true, {
|
sendBlueBubblesTyping(chatGuidForActions, true, {
|
||||||
cfg: config,
|
cfg: config,
|
||||||
accountId: account.accountId,
|
accountId: account.accountId,
|
||||||
})
|
}).catch((err) => {
|
||||||
.catch((err) => {
|
runtime.error?.(`[bluebubbles] typing restart failed: ${String(err)}`);
|
||||||
runtime.error?.(`[bluebubbles] typing restart failed: ${String(err)}`);
|
});
|
||||||
});
|
|
||||||
}, typingRestartDelayMs);
|
}, typingRestartDelayMs);
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@
|
||||||
## 2026.2.1
|
## 2026.2.1
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
- Version alignment with core OpenClaw release numbers.
|
- Version alignment with core OpenClaw release numbers.
|
||||||
|
|
||||||
## 2026.1.31
|
## 2026.1.31
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
- Version alignment with core OpenClaw release numbers.
|
- Version alignment with core OpenClaw release numbers.
|
||||||
|
|
||||||
## 2026.1.30
|
## 2026.1.30
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@
|
||||||
## 2026.2.1
|
## 2026.2.1
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
- Version alignment with core OpenClaw release numbers.
|
- Version alignment with core OpenClaw release numbers.
|
||||||
|
|
||||||
## 2026.1.31
|
## 2026.1.31
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
- Version alignment with core OpenClaw release numbers.
|
- Version alignment with core OpenClaw release numbers.
|
||||||
|
|
||||||
## 2026.1.30
|
## 2026.1.30
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@
|
||||||
## 2026.2.1
|
## 2026.2.1
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
- Version alignment with core OpenClaw release numbers.
|
- Version alignment with core OpenClaw release numbers.
|
||||||
|
|
||||||
## 2026.1.31
|
## 2026.1.31
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
- Version alignment with core OpenClaw release numbers.
|
- Version alignment with core OpenClaw release numbers.
|
||||||
|
|
||||||
## 2026.1.30
|
## 2026.1.30
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@
|
||||||
## 2026.2.1
|
## 2026.2.1
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
- Version alignment with core OpenClaw release numbers.
|
- Version alignment with core OpenClaw release numbers.
|
||||||
|
|
||||||
## 2026.1.31
|
## 2026.1.31
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
- Version alignment with core OpenClaw release numbers.
|
- Version alignment with core OpenClaw release numbers.
|
||||||
|
|
||||||
## 2026.1.30
|
## 2026.1.30
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@
|
||||||
## 2026.2.1
|
## 2026.2.1
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
- Version alignment with core OpenClaw release numbers.
|
- Version alignment with core OpenClaw release numbers.
|
||||||
|
|
||||||
## 2026.1.31
|
## 2026.1.31
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
- Version alignment with core OpenClaw release numbers.
|
- Version alignment with core OpenClaw release numbers.
|
||||||
|
|
||||||
## 2026.1.30
|
## 2026.1.30
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@
|
||||||
## 2026.2.1
|
## 2026.2.1
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
- Version alignment with core OpenClaw release numbers.
|
- Version alignment with core OpenClaw release numbers.
|
||||||
|
|
||||||
## 2026.1.31
|
## 2026.1.31
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
- Version alignment with core OpenClaw release numbers.
|
- Version alignment with core OpenClaw release numbers.
|
||||||
|
|
||||||
## 2026.1.30
|
## 2026.1.30
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@
|
||||||
## 2026.2.1
|
## 2026.2.1
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
- Version alignment with core OpenClaw release numbers.
|
- Version alignment with core OpenClaw release numbers.
|
||||||
|
|
||||||
## 2026.1.31
|
## 2026.1.31
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
- Version alignment with core OpenClaw release numbers.
|
- Version alignment with core OpenClaw release numbers.
|
||||||
|
|
||||||
## 2026.1.30
|
## 2026.1.30
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,10 @@ import { ensureAuthStoreFile, resolveAuthStorePath } from "./paths.js";
|
||||||
import { suggestOAuthProfileIdForLegacyDefault } from "./repair.js";
|
import { suggestOAuthProfileIdForLegacyDefault } from "./repair.js";
|
||||||
import { ensureAuthProfileStore, saveAuthProfileStore } from "./store.js";
|
import { ensureAuthProfileStore, saveAuthProfileStore } from "./store.js";
|
||||||
|
|
||||||
const OAUTH_PROVIDER_IDS = new Set<OAuthProvider>(
|
const OAUTH_PROVIDER_IDS = new Set<string>(getOAuthProviders().map((provider) => provider.id));
|
||||||
getOAuthProviders().map((provider) => provider.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
const isOAuthProvider = (provider: string): provider is OAuthProvider =>
|
const isOAuthProvider = (provider: string): provider is OAuthProvider =>
|
||||||
OAUTH_PROVIDER_IDS.has(provider as OAuthProvider);
|
OAUTH_PROVIDER_IDS.has(provider);
|
||||||
|
|
||||||
const resolveOAuthProvider = (provider: string): OAuthProvider | null =>
|
const resolveOAuthProvider = (provider: string): OAuthProvider | null =>
|
||||||
isOAuthProvider(provider) ? provider : null;
|
isOAuthProvider(provider) ? provider : null;
|
||||||
|
|
|
||||||
|
|
@ -99,12 +99,12 @@ const _readSessionMessages = async (sessionFile: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("createSystemPromptOverride", () => {
|
describe("createSystemPromptOverride", () => {
|
||||||
it("returns the override prompt regardless of default prompt", () => {
|
it("returns the override prompt trimmed", () => {
|
||||||
const override = createSystemPromptOverride("OVERRIDE");
|
const override = createSystemPromptOverride("OVERRIDE");
|
||||||
expect(override("DEFAULT")).toBe("OVERRIDE");
|
expect(override).toBe("OVERRIDE");
|
||||||
});
|
});
|
||||||
it("returns an empty string for blank overrides", () => {
|
it("returns an empty string for blank overrides", () => {
|
||||||
const override = createSystemPromptOverride(" \n ");
|
const override = createSystemPromptOverride(" \n ");
|
||||||
expect(override("DEFAULT")).toBe("");
|
expect(override).toBe("");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -74,11 +74,8 @@ export function buildEmbeddedSystemPrompt(params: {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSystemPromptOverride(
|
export function createSystemPromptOverride(systemPrompt: string): string {
|
||||||
systemPrompt: string,
|
return systemPrompt.trim();
|
||||||
): (defaultPrompt?: string) => string {
|
|
||||||
const override = systemPrompt.trim();
|
|
||||||
return (_defaultPrompt?: string) => override;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applySystemPromptOverrideToSession(session: AgentSession, override: string) {
|
export function applySystemPromptOverrideToSession(session: AgentSession, override: string) {
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,9 @@ export function toToolDefinitions(tools: AnyAgentTool[]): ToolDefinition[] {
|
||||||
execute: async (
|
execute: async (
|
||||||
toolCallId,
|
toolCallId,
|
||||||
params,
|
params,
|
||||||
|
signal: AbortSignal | undefined,
|
||||||
onUpdate: AgentToolUpdateCallback<unknown> | undefined,
|
onUpdate: AgentToolUpdateCallback<unknown> | undefined,
|
||||||
_ctx,
|
_ctx,
|
||||||
signal,
|
|
||||||
): Promise<AgentToolResult<unknown>> => {
|
): Promise<AgentToolResult<unknown>> => {
|
||||||
try {
|
try {
|
||||||
return await tool.execute(toolCallId, params, signal, onUpdate);
|
return await tool.execute(toolCallId, params, signal, onUpdate);
|
||||||
|
|
@ -91,9 +91,9 @@ export function toClientToolDefinitions(
|
||||||
execute: async (
|
execute: async (
|
||||||
toolCallId,
|
toolCallId,
|
||||||
params,
|
params,
|
||||||
|
_signal: AbortSignal | undefined,
|
||||||
_onUpdate: AgentToolUpdateCallback<unknown> | undefined,
|
_onUpdate: AgentToolUpdateCallback<unknown> | undefined,
|
||||||
_ctx,
|
_ctx,
|
||||||
_signal,
|
|
||||||
): Promise<AgentToolResult<unknown>> => {
|
): Promise<AgentToolResult<unknown>> => {
|
||||||
const outcome = await runBeforeToolCallHook({
|
const outcome = await runBeforeToolCallHook({
|
||||||
toolName: func.name,
|
toolName: func.name,
|
||||||
|
|
|
||||||
|
|
@ -19,16 +19,25 @@ export function resolveAgentTimeoutMs(opts: {
|
||||||
}): number {
|
}): number {
|
||||||
const minMs = Math.max(normalizeNumber(opts.minMs) ?? 1, 1);
|
const minMs = Math.max(normalizeNumber(opts.minMs) ?? 1, 1);
|
||||||
const defaultMs = resolveAgentTimeoutSeconds(opts.cfg) * 1000;
|
const defaultMs = resolveAgentTimeoutSeconds(opts.cfg) * 1000;
|
||||||
|
// Use a very large timeout value (30 days) to represent "no timeout"
|
||||||
|
// when explicitly set to 0. This avoids setTimeout issues with Infinity.
|
||||||
|
const NO_TIMEOUT_MS = 30 * 24 * 60 * 60 * 1000;
|
||||||
const overrideMs = normalizeNumber(opts.overrideMs);
|
const overrideMs = normalizeNumber(opts.overrideMs);
|
||||||
if (overrideMs !== undefined) {
|
if (overrideMs !== undefined) {
|
||||||
if (overrideMs <= 0) {
|
if (overrideMs === 0) {
|
||||||
|
return NO_TIMEOUT_MS;
|
||||||
|
}
|
||||||
|
if (overrideMs < 0) {
|
||||||
return defaultMs;
|
return defaultMs;
|
||||||
}
|
}
|
||||||
return Math.max(overrideMs, minMs);
|
return Math.max(overrideMs, minMs);
|
||||||
}
|
}
|
||||||
const overrideSeconds = normalizeNumber(opts.overrideSeconds);
|
const overrideSeconds = normalizeNumber(opts.overrideSeconds);
|
||||||
if (overrideSeconds !== undefined) {
|
if (overrideSeconds !== undefined) {
|
||||||
if (overrideSeconds <= 0) {
|
if (overrideSeconds === 0) {
|
||||||
|
return NO_TIMEOUT_MS;
|
||||||
|
}
|
||||||
|
if (overrideSeconds < 0) {
|
||||||
return defaultMs;
|
return defaultMs;
|
||||||
}
|
}
|
||||||
return Math.max(overrideSeconds * 1000, minMs);
|
return Math.max(overrideSeconds * 1000, minMs);
|
||||||
|
|
|
||||||
|
|
@ -172,24 +172,27 @@ export async function runAgentTurnWithFallback(params: {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const cliSessionId = getCliSessionId(params.getActiveSessionEntry(), provider);
|
const cliSessionId = getCliSessionId(params.getActiveSessionEntry(), provider);
|
||||||
return runCliAgent({
|
return (async () => {
|
||||||
sessionId: params.followupRun.run.sessionId,
|
let lifecycleTerminalEmitted = false;
|
||||||
sessionKey: params.sessionKey,
|
try {
|
||||||
sessionFile: params.followupRun.run.sessionFile,
|
const result = await runCliAgent({
|
||||||
workspaceDir: params.followupRun.run.workspaceDir,
|
sessionId: params.followupRun.run.sessionId,
|
||||||
config: params.followupRun.run.config,
|
sessionKey: params.sessionKey,
|
||||||
prompt: params.commandBody,
|
sessionFile: params.followupRun.run.sessionFile,
|
||||||
provider,
|
workspaceDir: params.followupRun.run.workspaceDir,
|
||||||
model,
|
config: params.followupRun.run.config,
|
||||||
thinkLevel: params.followupRun.run.thinkLevel,
|
prompt: params.commandBody,
|
||||||
timeoutMs: params.followupRun.run.timeoutMs,
|
provider,
|
||||||
runId,
|
model,
|
||||||
extraSystemPrompt: params.followupRun.run.extraSystemPrompt,
|
thinkLevel: params.followupRun.run.thinkLevel,
|
||||||
ownerNumbers: params.followupRun.run.ownerNumbers,
|
timeoutMs: params.followupRun.run.timeoutMs,
|
||||||
cliSessionId,
|
runId,
|
||||||
images: params.opts?.images,
|
extraSystemPrompt: params.followupRun.run.extraSystemPrompt,
|
||||||
})
|
ownerNumbers: params.followupRun.run.ownerNumbers,
|
||||||
.then((result) => {
|
cliSessionId,
|
||||||
|
images: params.opts?.images,
|
||||||
|
});
|
||||||
|
|
||||||
// CLI backends don't emit streaming assistant events, so we need to
|
// CLI backends don't emit streaming assistant events, so we need to
|
||||||
// emit one with the final text so server-chat can populate its buffer
|
// emit one with the final text so server-chat can populate its buffer
|
||||||
// and send the response to TUI/WebSocket clients.
|
// and send the response to TUI/WebSocket clients.
|
||||||
|
|
@ -201,6 +204,7 @@ export async function runAgentTurnWithFallback(params: {
|
||||||
data: { text: cliText },
|
data: { text: cliText },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
emitAgentEvent({
|
emitAgentEvent({
|
||||||
runId,
|
runId,
|
||||||
stream: "lifecycle",
|
stream: "lifecycle",
|
||||||
|
|
@ -210,9 +214,10 @@ export async function runAgentTurnWithFallback(params: {
|
||||||
endedAt: Date.now(),
|
endedAt: Date.now(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
lifecycleTerminalEmitted = true;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
})
|
} catch (err) {
|
||||||
.catch((err) => {
|
|
||||||
emitAgentEvent({
|
emitAgentEvent({
|
||||||
runId,
|
runId,
|
||||||
stream: "lifecycle",
|
stream: "lifecycle",
|
||||||
|
|
@ -220,11 +225,28 @@ export async function runAgentTurnWithFallback(params: {
|
||||||
phase: "error",
|
phase: "error",
|
||||||
startedAt,
|
startedAt,
|
||||||
endedAt: Date.now(),
|
endedAt: Date.now(),
|
||||||
error: err instanceof Error ? err.message : String(err),
|
error: String(err),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
lifecycleTerminalEmitted = true;
|
||||||
throw err;
|
throw err;
|
||||||
});
|
} finally {
|
||||||
|
// Defensive backstop: never let a CLI run complete without a terminal
|
||||||
|
// lifecycle event, otherwise downstream consumers can hang.
|
||||||
|
if (!lifecycleTerminalEmitted) {
|
||||||
|
emitAgentEvent({
|
||||||
|
runId,
|
||||||
|
stream: "lifecycle",
|
||||||
|
data: {
|
||||||
|
phase: "error",
|
||||||
|
startedAt,
|
||||||
|
endedAt: Date.now(),
|
||||||
|
error: "CLI run completed without lifecycle terminal event",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
const authProfileId =
|
const authProfileId =
|
||||||
provider === params.followupRun.run.provider
|
provider === params.followupRun.run.provider
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue