perf(agents): skip idle wait on abort to release session lock synchro… · openclaw/openclaw@b96ac71
medns
·
2026-05-08
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
@@ -625,6 +625,7 @@ Docs: https://docs.openclaw.ai
|
625 | 625 | - 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. |
626 | 626 | - Gateway/nodes: preserve the live node registry session and invoke ownership when an older same-node WebSocket closes after reconnecting. (#78351) Thanks @samzong. |
627 | 627 | - 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. |
| 628 | +- 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. |
628 | 629 | |
629 | 630 | ## 2026.5.3-1 |
630 | 631 | |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -138,4 +138,43 @@ describe("flushPendingToolResultsAfterIdle", () => {
|
138 | 138 | }); |
139 | 139 | expect(vi.getTimerCount()).toBe(0); |
140 | 140 | }); |
| 141 | + |
| 142 | +it("immediately clears pending tool results without waiting when timeoutMs is 0 or less", async () => { |
| 143 | +const sm = guardSessionManager(SessionManager.inMemory()); |
| 144 | +const appendMessage = sm.appendMessage.bind(sm) as unknown as (message: AgentMessage) => void; |
| 145 | + |
| 146 | +// Agent that never resolves idle |
| 147 | +const idle = deferred<void>(); |
| 148 | +const waitForIdleSpy = vi.fn(() => idle.promise); |
| 149 | +const agent = { waitForIdle: waitForIdleSpy }; |
| 150 | + |
| 151 | +appendMessage(assistantToolCall("call_orphan_immediate")); |
| 152 | + |
| 153 | +// Should resolve immediately without advancing timers |
| 154 | +await flushPendingToolResultsAfterIdle({ |
| 155 | + agent, |
| 156 | +sessionManager: sm, |
| 157 | +timeoutMs: 0, |
| 158 | +clearPendingOnTimeout: true, |
| 159 | +}); |
| 160 | + |
| 161 | +// Verify waitForIdle was completely bypassed |
| 162 | +expect(waitForIdleSpy).not.toHaveBeenCalled(); |
| 163 | + |
| 164 | +// The pending tool result should be cleared immediately. |
| 165 | +expect(getMessages(sm).map((m) => m.role)).toEqual(["assistant"]); |
| 166 | + |
| 167 | +// Test negative timeout as well |
| 168 | +appendMessage(assistantToolCall("call_orphan_negative")); |
| 169 | +await flushPendingToolResultsAfterIdle({ |
| 170 | + agent, |
| 171 | +sessionManager: sm, |
| 172 | +timeoutMs: -100, |
| 173 | +clearPendingOnTimeout: true, |
| 174 | +}); |
| 175 | + |
| 176 | +// Verify waitForIdle was still bypassed |
| 177 | +expect(waitForIdleSpy).not.toHaveBeenCalled(); |
| 178 | +expect(getMessages(sm).map((m) => m.role)).toEqual(["assistant", "assistant"]); |
| 179 | +}); |
141 | 180 | }); |
| Original file line number | Diff line number | Diff line change |
|---|
@@ -30,18 +30,23 @@ export async function cleanupEmbeddedAttemptResources(params: {
|
30 | 30 | bundleMcpRuntime?: { dispose(): Promise<void> | void }; |
31 | 31 | bundleLspRuntime?: { dispose(): Promise<void> | void }; |
32 | 32 | sessionLock: { release(): Promise<void> | void }; |
| 33 | +aborted?: boolean; |
33 | 34 | }): Promise<void> { |
34 | 35 | try { |
35 | 36 | try { |
36 | 37 | params.removeToolResultContextGuard?.(); |
37 | 38 | } catch { |
38 | 39 | /* best-effort */ |
39 | 40 | } |
| 41 | +// PERF: When the run was aborted (user stop / timeout), skip the expensive |
| 42 | +// waitForIdle (up to 30 s) and just clear pending tool results synchronously |
| 43 | +// so the session write-lock is released ASAP and the next message is not blocked. |
40 | 44 | try { |
41 | 45 | await params.flushPendingToolResultsAfterIdle({ |
42 | 46 | agent: params.session?.agent as IdleAwareAgent | null | undefined, |
43 | 47 | sessionManager: params.sessionManager as ToolResultFlushManager | null | undefined, |
44 | 48 | clearPendingOnTimeout: true, |
| 49 | + ...(params.aborted ? { timeoutMs: 0 } : {}), |
45 | 50 | }); |
46 | 51 | } catch { |
47 | 52 | /* best-effort */ |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -2345,6 +2345,10 @@ export async function runEmbeddedAttempt(
|
2345 | 2345 | agent: activeSession?.agent, |
2346 | 2346 | sessionManager, |
2347 | 2347 | clearPendingOnTimeout: true, |
| 2348 | +// PERF: If the run was aborted during the setup, |
| 2349 | +// skip the idle wait and clear pending results synchronously so we can |
| 2350 | +// immediately dispose the session and throw the error without blocking. |
| 2351 | + ...(params.abortSignal?.aborted ? { timeoutMs: 0 } : {}), |
2348 | 2352 | }); |
2349 | 2353 | activeSession.dispose(); |
2350 | 2354 | throw err; |
@@ -3845,6 +3849,14 @@ export async function runEmbeddedAttempt(
|
3845 | 3849 | bundleMcpRuntime, |
3846 | 3850 | bundleLspRuntime, |
3847 | 3851 | sessionLock, |
| 3852 | +// PERF: If the run was aborted (user stop, timeout, etc.), skip the idle wait |
| 3853 | +// and clear pending results synchronously so we can release the session lock ASAP. |
| 3854 | +aborted: |
| 3855 | +Boolean(params.abortSignal?.aborted) || |
| 3856 | +aborted || |
| 3857 | +timedOut || |
| 3858 | +idleTimedOut || |
| 3859 | +timedOutDuringCompaction, |
3848 | 3860 | }); |
3849 | 3861 | } catch (err) { |
3850 | 3862 | cleanupError = err; |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -46,10 +46,14 @@ export async function flushPendingToolResultsAfterIdle(opts: {
|
46 | 46 | timeoutMs?: number; |
47 | 47 | clearPendingOnTimeout?: boolean; |
48 | 48 | }): Promise<void> { |
49 | | -const timedOut = await waitForAgentIdleBestEffort( |
50 | | -opts.agent, |
51 | | -opts.timeoutMs ?? DEFAULT_WAIT_FOR_IDLE_TIMEOUT_MS, |
52 | | -); |
| 49 | +const isImmediateTimeout = opts.timeoutMs !== undefined && opts.timeoutMs <= 0; |
| 50 | +const timedOut = |
| 51 | +isImmediateTimeout || |
| 52 | +(await waitForAgentIdleBestEffort( |
| 53 | +opts.agent, |
| 54 | +opts.timeoutMs ?? DEFAULT_WAIT_FOR_IDLE_TIMEOUT_MS, |
| 55 | +)); |
| 56 | + |
53 | 57 | if (timedOut && opts.clearPendingOnTimeout && opts.sessionManager?.clearPendingToolResults) { |
54 | 58 | opts.sessionManager.clearPendingToolResults(); |
55 | 59 | return; |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。