diff --git a/CHANGELOG.md b/CHANGELOG.md index cceb6e03bb4..13b7921e8d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Agents/sessions: acquire the session write lock only after cold bootstrap, plugin, and tool setup so fallback runs are not blocked by stalled pre-model startup work. Thanks @codex. - Gateway/models: skip external OpenRouter and LiteLLM pricing refreshes for local/self-hosted model endpoints so startup does not wait on remote pricing catalogs for local-only Ollama, vLLM, and compatible providers. Thanks @codex. - CLI/plugins: stop security-blocked plugin installs from retrying as hook packs, so normal plugin packages report the scanner failure without a misleading "not a valid hook pack" follow-up. Fixes #61175; supersedes #64102. Thanks @KonsultDigital and @ziyincody. - Control UI/Dreaming: require explicit confirmation before applying restart-impacting Dreaming mode changes, with restart warning copy and loading feedback. Fixes #63804. (#63807) Thanks @bbddbb1. diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts index f309443c46c..57ad1690e41 100644 --- a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts @@ -253,6 +253,27 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => { expectCalledWithSessionKey(afterTurn, sessionKey); }); + it("resolves bootstrap context before acquiring the session write lock", async () => { + const events: string[] = []; + hoisted.resolveBootstrapContextForRunMock.mockImplementation(async () => { + events.push("bootstrap"); + return { bootstrapFiles: [], contextFiles: [] }; + }); + hoisted.acquireSessionWriteLockMock.mockImplementation(async () => { + events.push("lock"); + return { release: async () => {} }; + }); + + await createContextEngineAttemptRunner({ + contextEngine: createContextEngineBootstrapAndAssemble(), + sessionKey, + tempPaths, + }); + + expect(events).toEqual(expect.arrayContaining(["bootstrap", "lock"])); + expect(events.indexOf("bootstrap")).toBeLessThan(events.indexOf("lock")); + }); + it("forwards modelId to assemble", async () => { const { bootstrap, assemble } = createContextEngineBootstrapAndAssemble(); const contextEngine = createTestContextEngine({ bootstrap, assemble }); diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 68f88c7e9bf..99d79b06789 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -639,16 +639,6 @@ export async function runEmbeddedAttempt( agentId: sessionAgentId, }); - const sessionLock = await acquireSessionWriteLock({ - sessionFile: params.sessionFile, - maxHoldMs: resolveSessionLockMaxHoldFromTimeout({ - timeoutMs: resolveRunTimeoutWithCompactionGraceMs({ - runTimeoutMs: params.timeoutMs, - compactionTimeoutMs: resolveCompactionTimeoutMs(params.config), - }), - }), - }); - const sessionLabel = params.sessionKey ?? params.sessionId; const contextInjectionMode = resolveContextInjectionMode(params.config); const agentDir = params.agentDir ?? resolveOpenClawAgentDir(); @@ -1205,6 +1195,19 @@ export async function runEmbeddedAttempt( let systemPromptText = systemPromptOverride(); const userPromptPrefixText = bootstrapRouting.userPromptPrefixText; + // Keep the session lock scoped to transcript/session mutations. Cold plugin + // and tool setup can be slow, and holding the lock there blocks CLI fallback + // from taking over the same session when a gateway run stalls before model I/O. + const sessionLock = await acquireSessionWriteLock({ + sessionFile: params.sessionFile, + maxHoldMs: resolveSessionLockMaxHoldFromTimeout({ + timeoutMs: resolveRunTimeoutWithCompactionGraceMs({ + runTimeoutMs: params.timeoutMs, + compactionTimeoutMs: resolveCompactionTimeoutMs(params.config), + }), + }), + }); + let sessionManager: ReturnType | undefined; let session: Awaited>["session"] | undefined; let removeToolResultContextGuard: (() => void) | undefined;