From dfed74b2546d32214cb79c331333ead517540475 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 14 Apr 2026 10:28:09 +0100 Subject: [PATCH] fix(hooks): honor configured ollama slug timeout (#66455) --- CHANGELOG.md | 1 + src/hooks/llm-slug-generator.test.ts | 60 ++++++++++++++++++++++++++++ src/hooks/llm-slug-generator.ts | 13 +++++- 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 src/hooks/llm-slug-generator.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7860bc6ea79..02c77fcee01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai - Browser: keep loopback CDP readiness checks reachable under strict SSRF defaults so OpenClaw can reconnect to locally started managed Chrome. (#66354) Thanks @hxy91819. - Agents/context engine: compact engine-owned sessions from the first tool-loop delta and preserve ingest fallback when `afterTurn` is absent, so long-running tool loops can stay bounded without dropping engine state. (#63555) Thanks @Bikkies. - Discord/native commands: return the real status card for native `/status` interactions instead of falling through to the synthetic `✅ Done.` ack when the generic dispatcher produces no visible reply. (#54629) Thanks @tkozzer and @vincentkoc. +- Hooks/Ollama: let LLM-backed session-memory slug generation honor an explicit `agents.defaults.timeoutSeconds` override instead of always aborting after 15 seconds, so slow local Ollama runs stop silently dropping back to generic filenames. (#66237) Thanks @dmak and @vincentkoc. ## 2026.4.14-beta.1 diff --git a/src/hooks/llm-slug-generator.test.ts b/src/hooks/llm-slug-generator.test.ts new file mode 100644 index 00000000000..94c8d81f499 --- /dev/null +++ b/src/hooks/llm-slug-generator.test.ts @@ -0,0 +1,60 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; + +const runEmbeddedPiAgentMock = vi.fn(); + +vi.mock("../agents/agent-scope.js", () => ({ + resolveDefaultAgentId: vi.fn(() => "main"), + resolveAgentWorkspaceDir: vi.fn(() => "/tmp/openclaw-agent"), + resolveAgentDir: vi.fn(() => "/tmp/openclaw-agent/.openclaw-agent"), + resolveAgentEffectiveModelPrimary: vi.fn(() => null), +})); + +vi.mock("../agents/pi-embedded.js", () => ({ + runEmbeddedPiAgent: (...args: unknown[]) => runEmbeddedPiAgentMock(...args), +})); + +import { generateSlugViaLLM } from "./llm-slug-generator.js"; + +describe("generateSlugViaLLM", () => { + beforeEach(() => { + runEmbeddedPiAgentMock.mockReset(); + runEmbeddedPiAgentMock.mockResolvedValue({ + payloads: [{ text: "test-slug" }], + }); + }); + + it("keeps the helper default timeout when no agent timeout is configured", async () => { + await generateSlugViaLLM({ + sessionContent: "hello", + cfg: {} as OpenClawConfig, + }); + + expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce(); + expect(runEmbeddedPiAgentMock.mock.calls[0]?.[0]).toEqual( + expect.objectContaining({ + timeoutMs: 15_000, + }), + ); + }); + + it("honors configured agent timeoutSeconds for slow local providers", async () => { + await generateSlugViaLLM({ + sessionContent: "hello", + cfg: { + agents: { + defaults: { + timeoutSeconds: 500, + }, + }, + } as OpenClawConfig, + }); + + expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce(); + expect(runEmbeddedPiAgentMock.mock.calls[0]?.[0]).toEqual( + expect.objectContaining({ + timeoutMs: 500_000, + }), + ); + }); +}); diff --git a/src/hooks/llm-slug-generator.ts b/src/hooks/llm-slug-generator.ts index 83104f12b8f..fe600d39a4d 100644 --- a/src/hooks/llm-slug-generator.ts +++ b/src/hooks/llm-slug-generator.ts @@ -14,11 +14,21 @@ import { import { DEFAULT_PROVIDER, DEFAULT_MODEL } from "../agents/defaults.js"; import { parseModelRef } from "../agents/model-selection.js"; import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; +import { resolveAgentTimeoutMs } from "../agents/timeout.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; const log = createSubsystemLogger("llm-slug-generator"); +const DEFAULT_SLUG_GENERATOR_TIMEOUT_MS = 15_000; + +function resolveSlugGeneratorTimeoutMs(cfg: OpenClawConfig): number { + const configuredTimeoutSeconds = cfg.agents?.defaults?.timeoutSeconds; + if (typeof configuredTimeoutSeconds !== "number" || !Number.isFinite(configuredTimeoutSeconds)) { + return DEFAULT_SLUG_GENERATOR_TIMEOUT_MS; + } + return resolveAgentTimeoutMs({ cfg }); +} /** * Generate a short 1-2 word filename slug from session content using LLM @@ -50,6 +60,7 @@ Reply with ONLY the slug, nothing else. Examples: "vendor-pitch", "api-design", const parsed = modelRef ? parseModelRef(modelRef, DEFAULT_PROVIDER) : null; const provider = parsed?.provider ?? DEFAULT_PROVIDER; const model = parsed?.model ?? DEFAULT_MODEL; + const timeoutMs = resolveSlugGeneratorTimeoutMs(params.cfg); const result = await runEmbeddedPiAgent({ sessionId: `slug-generator-${Date.now()}`, @@ -62,7 +73,7 @@ Reply with ONLY the slug, nothing else. Examples: "vendor-pitch", "api-design", prompt, provider, model, - timeoutMs: 15_000, // 15 second timeout + timeoutMs, runId: `slug-gen-${Date.now()}`, });