fix(exec): honor default timeout for background runs

This commit is contained in:
Peter Steinberger
2026-04-27 11:06:18 +01:00
parent ca882aeb42
commit 92100efa04
3 changed files with 32 additions and 14 deletions

View File

@@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Memory-core/dreaming: treat request-scoped narrative fallback as expected, skip session cleanup when no subagent run was created, and remove duplicate phase-level cleanup so fallback no longer emits warning noise. Fixes #67152. Thanks @jsompis.
- Agents/exec: apply configured `tools.exec.timeoutSec` to background and `yieldMs` commands when no per-call timeout is set, preventing auto-backgrounded commands from running indefinitely. Fixes #67600; supersedes #67603. Thanks @dlmpx and @kagura-agent.
- Config/doctor: stop masking unknown-key validation diagnostics such as `agents.defaults.llm`, and have `openclaw doctor --fix` remove the retired `agents.defaults.llm` timeout block. Thanks @aidiffuser.
- CLI/plugins: preserve unversioned ClawHub install specs so `plugins update` can follow newer ClawHub releases instead of pinning to the initially resolved version. Fixes #63010; supersedes #58426. Thanks @kangsen1234 and @robinspt.
- Memory-core/subagents: tag plugin-created subagent sessions with their plugin owner so dreaming narrative cleanup can delete its own ephemeral sessions without granting broad admin session deletion. Fixes #72712. Thanks @BSG2000.

View File

@@ -177,12 +177,18 @@ async function expectBackgroundSessionTimesOut(params: {
executeParams: ExecToolExecuteParams;
signal?: AbortSignal;
abortAfterStart?: boolean;
expectedTimeoutSec?: number;
}) {
const abortController = new AbortController();
const signal = params.signal ?? abortController.signal;
const result = await params.tool.execute("toolcall", params.executeParams, signal);
expect(result.details.status).toBe("running");
const sessionId = (result.details as { sessionId: string }).sessionId;
if (typeof params.expectedTimeoutSec === "number") {
expect(supervisorMockState.spawnInputs.at(-1)?.timeoutMs).toBe(
Math.floor(params.expectedTimeoutSec * 1000),
);
}
if (params.abortAfterStart) {
abortController.abort();
@@ -223,23 +229,21 @@ test("background exec still times out after tool signal abort", async () => {
timeout: BACKGROUND_TIMEOUT_SEC,
},
abortAfterStart: true,
expectedTimeoutSec: BACKGROUND_TIMEOUT_SEC,
});
});
test("background exec without explicit timeout ignores default timeout", async () => {
test("background exec without explicit timeout applies default timeout", async () => {
const tool = createTestExecTool({
allowBackground: true,
backgroundMs: 0,
timeoutSec: BACKGROUND_TIMEOUT_SEC,
});
const result = await tool.execute("toolcall", { command: BACKGROUND_HOLD_CMD, background: true });
expect(result.details.status).toBe("running");
const sessionId = (result.details as { sessionId: string }).sessionId;
expect(supervisorMockState.spawnInputs.at(-1)?.timeoutMs).toBeUndefined();
expect(getFinishedSession(sessionId)).toBeUndefined();
expect(getSession(sessionId)?.exited).toBe(false);
cleanupRunningSession(sessionId);
await expectBackgroundSessionTimesOut({
tool,
executeParams: { command: BACKGROUND_HOLD_CMD, background: true },
expectedTimeoutSec: BACKGROUND_TIMEOUT_SEC,
});
});
test("yielded background exec still times out", async () => {
@@ -251,5 +255,22 @@ test("yielded background exec still times out", async () => {
yieldMs: 5,
timeout: BACKGROUND_TIMEOUT_SEC,
},
expectedTimeoutSec: BACKGROUND_TIMEOUT_SEC,
});
});
test("yieldMs exec without explicit timeout applies default timeout", async () => {
const tool = createTestExecTool({
allowBackground: true,
backgroundMs: 10,
timeoutSec: BACKGROUND_TIMEOUT_SEC,
});
await expectBackgroundSessionTimesOut({
tool,
executeParams: {
command: BACKGROUND_HOLD_CMD,
yieldMs: 5,
},
expectedTimeoutSec: BACKGROUND_TIMEOUT_SEC,
});
});

View File

@@ -1676,11 +1676,7 @@ export function createExecTool(
}
const explicitTimeoutSec = typeof params.timeout === "number" ? params.timeout : null;
const backgroundTimeoutBypass =
allowBackground && explicitTimeoutSec === null && (backgroundRequested || yieldRequested);
const effectiveTimeout = backgroundTimeoutBypass
? null
: (explicitTimeoutSec ?? defaultTimeoutSec);
const effectiveTimeout = explicitTimeoutSec ?? defaultTimeoutSec;
const getWarningText = () => (warnings.length ? `${warnings.join("\n")}\n\n` : "");
const usePty = params.pty === true && !sandbox;