mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 11:10:46 +00:00
perf(agents): skip idle wait on abort to release session lock synchronously (#74919)
Merged via squash.
Prepared head SHA: 0af4c4685f
Co-authored-by: medns <1575008+medns@users.noreply.github.com>
Co-authored-by: odysseus0 <8635094+odysseus0@users.noreply.github.com>
Reviewed-by: @odysseus0
This commit is contained in:
@@ -625,6 +625,7 @@ Docs: https://docs.openclaw.ai
|
||||
- WhatsApp: stop Gateway-originated outbound echoes from advancing inbound activity in `openclaw channels status`, so outbound self-sends no longer look like handled inbound messages. Fixes #79056. (#79057) Thanks @ai-hpc and @bittoby.
|
||||
- Gateway/nodes: preserve the live node registry session and invoke ownership when an older same-node WebSocket closes after reconnecting. (#78351) Thanks @samzong.
|
||||
- Browser/downloads: route explicit and managed browser download output directories through `fs-safe` validation before staging final files, so symlinked output roots are rejected before writes. (#78780) Thanks @jesse-merhi.
|
||||
- Agents/PI: skip the idle wait during aborted embedded-run cleanup, so stopped or timed-out runs clear pending tool state and release the session lock promptly. (#74919) Thanks @medns.
|
||||
|
||||
## 2026.5.3-1
|
||||
|
||||
|
||||
@@ -138,4 +138,43 @@ describe("flushPendingToolResultsAfterIdle", () => {
|
||||
});
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
});
|
||||
|
||||
it("immediately clears pending tool results without waiting when timeoutMs is 0 or less", async () => {
|
||||
const sm = guardSessionManager(SessionManager.inMemory());
|
||||
const appendMessage = sm.appendMessage.bind(sm) as unknown as (message: AgentMessage) => void;
|
||||
|
||||
// Agent that never resolves idle
|
||||
const idle = deferred<void>();
|
||||
const waitForIdleSpy = vi.fn(() => idle.promise);
|
||||
const agent = { waitForIdle: waitForIdleSpy };
|
||||
|
||||
appendMessage(assistantToolCall("call_orphan_immediate"));
|
||||
|
||||
// Should resolve immediately without advancing timers
|
||||
await flushPendingToolResultsAfterIdle({
|
||||
agent,
|
||||
sessionManager: sm,
|
||||
timeoutMs: 0,
|
||||
clearPendingOnTimeout: true,
|
||||
});
|
||||
|
||||
// Verify waitForIdle was completely bypassed
|
||||
expect(waitForIdleSpy).not.toHaveBeenCalled();
|
||||
|
||||
// The pending tool result should be cleared immediately.
|
||||
expect(getMessages(sm).map((m) => m.role)).toEqual(["assistant"]);
|
||||
|
||||
// Test negative timeout as well
|
||||
appendMessage(assistantToolCall("call_orphan_negative"));
|
||||
await flushPendingToolResultsAfterIdle({
|
||||
agent,
|
||||
sessionManager: sm,
|
||||
timeoutMs: -100,
|
||||
clearPendingOnTimeout: true,
|
||||
});
|
||||
|
||||
// Verify waitForIdle was still bypassed
|
||||
expect(waitForIdleSpy).not.toHaveBeenCalled();
|
||||
expect(getMessages(sm).map((m) => m.role)).toEqual(["assistant", "assistant"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,6 +30,7 @@ export async function cleanupEmbeddedAttemptResources(params: {
|
||||
bundleMcpRuntime?: { dispose(): Promise<void> | void };
|
||||
bundleLspRuntime?: { dispose(): Promise<void> | void };
|
||||
sessionLock: { release(): Promise<void> | void };
|
||||
aborted?: boolean;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
try {
|
||||
@@ -37,11 +38,15 @@ export async function cleanupEmbeddedAttemptResources(params: {
|
||||
} catch {
|
||||
/* best-effort */
|
||||
}
|
||||
// PERF: When the run was aborted (user stop / timeout), skip the expensive
|
||||
// waitForIdle (up to 30 s) and just clear pending tool results synchronously
|
||||
// so the session write-lock is released ASAP and the next message is not blocked.
|
||||
try {
|
||||
await params.flushPendingToolResultsAfterIdle({
|
||||
agent: params.session?.agent as IdleAwareAgent | null | undefined,
|
||||
sessionManager: params.sessionManager as ToolResultFlushManager | null | undefined,
|
||||
clearPendingOnTimeout: true,
|
||||
...(params.aborted ? { timeoutMs: 0 } : {}),
|
||||
});
|
||||
} catch {
|
||||
/* best-effort */
|
||||
|
||||
@@ -2345,6 +2345,10 @@ export async function runEmbeddedAttempt(
|
||||
agent: activeSession?.agent,
|
||||
sessionManager,
|
||||
clearPendingOnTimeout: true,
|
||||
// PERF: If the run was aborted during the setup,
|
||||
// skip the idle wait and clear pending results synchronously so we can
|
||||
// immediately dispose the session and throw the error without blocking.
|
||||
...(params.abortSignal?.aborted ? { timeoutMs: 0 } : {}),
|
||||
});
|
||||
activeSession.dispose();
|
||||
throw err;
|
||||
@@ -3845,6 +3849,14 @@ export async function runEmbeddedAttempt(
|
||||
bundleMcpRuntime,
|
||||
bundleLspRuntime,
|
||||
sessionLock,
|
||||
// PERF: If the run was aborted (user stop, timeout, etc.), skip the idle wait
|
||||
// and clear pending results synchronously so we can release the session lock ASAP.
|
||||
aborted:
|
||||
Boolean(params.abortSignal?.aborted) ||
|
||||
aborted ||
|
||||
timedOut ||
|
||||
idleTimedOut ||
|
||||
timedOutDuringCompaction,
|
||||
});
|
||||
} catch (err) {
|
||||
cleanupError = err;
|
||||
|
||||
@@ -46,10 +46,14 @@ export async function flushPendingToolResultsAfterIdle(opts: {
|
||||
timeoutMs?: number;
|
||||
clearPendingOnTimeout?: boolean;
|
||||
}): Promise<void> {
|
||||
const timedOut = await waitForAgentIdleBestEffort(
|
||||
opts.agent,
|
||||
opts.timeoutMs ?? DEFAULT_WAIT_FOR_IDLE_TIMEOUT_MS,
|
||||
);
|
||||
const isImmediateTimeout = opts.timeoutMs !== undefined && opts.timeoutMs <= 0;
|
||||
const timedOut =
|
||||
isImmediateTimeout ||
|
||||
(await waitForAgentIdleBestEffort(
|
||||
opts.agent,
|
||||
opts.timeoutMs ?? DEFAULT_WAIT_FOR_IDLE_TIMEOUT_MS,
|
||||
));
|
||||
|
||||
if (timedOut && opts.clearPendingOnTimeout && opts.sessionManager?.clearPendingToolResults) {
|
||||
opts.sessionManager.clearPendingToolResults();
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user