From 38de89641999d23099bb31b6acfa3bb0fcb937da Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 14 Apr 2026 09:21:22 +0100 Subject: [PATCH] fix(agents): honor embedded ollama timeouts (#66418) --- CHANGELOG.md | 1 + .../attempt.spawn-workspace.test-support.ts | 14 ++++++- .../attempt.spawn-workspace.timeout.test.ts | 42 +++++++++++++++++++ src/agents/pi-embedded-runner/run/attempt.ts | 2 +- 4 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 src/agents/pi-embedded-runner/run/attempt.spawn-workspace.timeout.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e4aba8f925..d13e9dc7076 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Agents/Ollama: forward the configured embedded-run timeout into the global undici stream timeout tuning so slow local Ollama runs no longer inherit the default stream cutoff instead of the operator-set run timeout. (#63175) Thanks @mindcraftreader and @vincentkoc. - Models/Codex: include `apiKey` in the codex provider catalog output so the Pi ModelRegistry validator no longer rejects the entry and silently drops all custom models from every provider in `models.json`. (#66180) Thanks @hoyyeva. - Slack/interactions: apply the configured global `allowFrom` owner allowlist to channel block-action and modal interactive events, require an expected sender id for cross-verification, and reject ambiguous channel types so interactive triggers can no longer bypass the documented allowlist intent in channels without a `users` list. Open-by-default behavior is preserved when no allowlists are configured. (#66028) Thanks @eleqtrizit. - Media-understanding/attachments: fail closed when a local attachment path cannot be canonically resolved via `realpath`, so a `realpath` error can no longer downgrade the canonical-roots allowlist check to a non-canonical comparison; attachments that also have a URL still fall back to the network fetch path. (#66022) Thanks @eleqtrizit. diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts index dd55bd52a1a..04c052af76b 100644 --- a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts +++ b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts @@ -47,6 +47,8 @@ type AttemptSpawnWorkspaceHoisted = { createAgentSessionMock: UnknownMock; sessionManagerOpenMock: UnknownMock; resolveSandboxContextMock: UnknownMock; + ensureGlobalUndiciEnvProxyDispatcherMock: UnknownMock; + ensureGlobalUndiciStreamTimeoutsMock: UnknownMock; buildEmbeddedMessageActionDiscoveryInputMock: UnknownMock; subscribeEmbeddedPiSessionMock: Mock; acquireSessionWriteLockMock: Mock; @@ -71,6 +73,8 @@ const hoisted = vi.hoisted((): AttemptSpawnWorkspaceHoisted => { const createAgentSessionMock = vi.fn(); const sessionManagerOpenMock = vi.fn(); const resolveSandboxContextMock = vi.fn(); + const ensureGlobalUndiciEnvProxyDispatcherMock = vi.fn(); + const ensureGlobalUndiciStreamTimeoutsMock = vi.fn(); const buildEmbeddedMessageActionDiscoveryInputMock = vi.fn((params: unknown) => params); const installToolResultContextGuardMock = vi.fn(() => () => {}); const flushPendingToolResultsAfterIdleMock = vi.fn(async () => {}); @@ -135,6 +139,8 @@ const hoisted = vi.hoisted((): AttemptSpawnWorkspaceHoisted => { createAgentSessionMock, sessionManagerOpenMock, resolveSandboxContextMock, + ensureGlobalUndiciEnvProxyDispatcherMock, + ensureGlobalUndiciStreamTimeoutsMock, buildEmbeddedMessageActionDiscoveryInputMock, subscribeEmbeddedPiSessionMock, acquireSessionWriteLockMock, @@ -209,8 +215,10 @@ vi.mock("../../../infra/machine-name.js", () => ({ })); vi.mock("../../../infra/net/undici-global-dispatcher.js", () => ({ - ensureGlobalUndiciEnvProxyDispatcher: () => {}, - ensureGlobalUndiciStreamTimeouts: () => {}, + ensureGlobalUndiciEnvProxyDispatcher: (...args: unknown[]) => + hoisted.ensureGlobalUndiciEnvProxyDispatcherMock(...args), + ensureGlobalUndiciStreamTimeouts: (...args: unknown[]) => + hoisted.ensureGlobalUndiciStreamTimeoutsMock(...args), })); vi.mock("../../bootstrap-files.js", async () => { @@ -683,6 +691,8 @@ export function resetEmbeddedAttemptHarness( hoisted.createAgentSessionMock.mockReset(); hoisted.sessionManagerOpenMock.mockReset().mockReturnValue(hoisted.sessionManager); hoisted.resolveSandboxContextMock.mockReset(); + hoisted.ensureGlobalUndiciEnvProxyDispatcherMock.mockReset(); + hoisted.ensureGlobalUndiciStreamTimeoutsMock.mockReset(); hoisted.buildEmbeddedMessageActionDiscoveryInputMock .mockReset() .mockImplementation((params) => params); diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.timeout.test.ts b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.timeout.test.ts new file mode 100644 index 00000000000..6cefe21c187 --- /dev/null +++ b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.timeout.test.ts @@ -0,0 +1,42 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { + cleanupTempPaths, + createContextEngineAttemptRunner, + getHoisted, + resetEmbeddedAttemptHarness, +} from "./attempt.spawn-workspace.test-support.js"; + +const hoisted = getHoisted(); + +describe("runEmbeddedAttempt undici timeout wiring", () => { + const tempPaths: string[] = []; + + beforeEach(() => { + resetEmbeddedAttemptHarness(); + }); + + afterEach(async () => { + await cleanupTempPaths(tempPaths); + }); + + it("forwards the configured run timeout into global undici stream tuning", async () => { + await createContextEngineAttemptRunner({ + sessionKey: "agent:main:ollama-timeout-test", + tempPaths, + contextEngine: { + assemble: async ({ messages }) => ({ + messages, + estimatedTokens: 1, + }), + }, + attemptOverrides: { + timeoutMs: 123_456, + }, + }); + + expect(hoisted.ensureGlobalUndiciEnvProxyDispatcherMock).toHaveBeenCalledOnce(); + expect(hoisted.ensureGlobalUndiciStreamTimeoutsMock).toHaveBeenCalledWith({ + timeoutMs: 123_456, + }); + }); +}); diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 7e7fce27804..3ad5c5fc65b 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -361,7 +361,7 @@ export async function runEmbeddedAttempt( // Proxy bootstrap must happen before timeout tuning so the timeouts wrap the // active EnvHttpProxyAgent instead of being replaced by a bare proxy dispatcher. ensureGlobalUndiciEnvProxyDispatcher(); - ensureGlobalUndiciStreamTimeouts(); + ensureGlobalUndiciStreamTimeouts({ timeoutMs: params.timeoutMs }); log.debug( `embedded run start: runId=${params.runId} sessionId=${params.sessionId} provider=${params.provider} model=${params.modelId} thinking=${params.thinkLevel} messageChannel=${params.messageChannel ?? params.messageProvider ?? "unknown"}`,