Files
openclaw/src/hooks/llm-slug-generator.ts
SudeepMalipeddi d32298cbd8 fix: slug-generator uses effective model instead of agent-primary
resolveAgentModelPrimary() only checks the agent-level model config and
does not fall back to the system-wide default. When users configure a
non-Anthropic provider (e.g. Gemini, Minimax) as their global default
without setting it at the agent level, the slug-generator falls through
to DEFAULT_PROVIDER (anthropic) and fails with a missing API key error.

Switch to resolveAgentEffectiveModelPrimary() which correctly respects
the full model resolution chain including global defaults.

Fixes #25365
2026-02-24 14:27:01 +00:00

101 lines
3.2 KiB
TypeScript

/**
* LLM-based slug generator for session memory filenames
*/
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import {
resolveDefaultAgentId,
resolveAgentWorkspaceDir,
resolveAgentDir,
resolveAgentEffectiveModelPrimary,
} from "../agents/agent-scope.js";
import { DEFAULT_PROVIDER, DEFAULT_MODEL } from "../agents/defaults.js";
import { parseModelRef } from "../agents/model-selection.js";
import { runEmbeddedPiAgent } from "../agents/pi-embedded.js";
import type { OpenClawConfig } from "../config/config.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
const log = createSubsystemLogger("llm-slug-generator");
/**
* Generate a short 1-2 word filename slug from session content using LLM
*/
export async function generateSlugViaLLM(params: {
sessionContent: string;
cfg: OpenClawConfig;
}): Promise<string | null> {
let tempSessionFile: string | null = null;
try {
const agentId = resolveDefaultAgentId(params.cfg);
const workspaceDir = resolveAgentWorkspaceDir(params.cfg, agentId);
const agentDir = resolveAgentDir(params.cfg, agentId);
// Create a temporary session file for this one-off LLM call
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-slug-"));
tempSessionFile = path.join(tempDir, "session.jsonl");
const prompt = `Based on this conversation, generate a short 1-2 word filename slug (lowercase, hyphen-separated, no file extension).
Conversation summary:
${params.sessionContent.slice(0, 2000)}
Reply with ONLY the slug, nothing else. Examples: "vendor-pitch", "api-design", "bug-fix"`;
// Resolve model from agent config instead of using hardcoded defaults
const modelRef = resolveAgentEffectiveModelPrimary(params.cfg, agentId);
const parsed = modelRef ? parseModelRef(modelRef, DEFAULT_PROVIDER) : null;
const provider = parsed?.provider ?? DEFAULT_PROVIDER;
const model = parsed?.model ?? DEFAULT_MODEL;
const result = await runEmbeddedPiAgent({
sessionId: `slug-generator-${Date.now()}`,
sessionKey: "temp:slug-generator",
agentId,
sessionFile: tempSessionFile,
workspaceDir,
agentDir,
config: params.cfg,
prompt,
provider,
model,
timeoutMs: 15_000, // 15 second timeout
runId: `slug-gen-${Date.now()}`,
});
// Extract text from payloads
if (result.payloads && result.payloads.length > 0) {
const text = result.payloads[0]?.text;
if (text) {
// Clean up the response - extract just the slug
const slug = text
.trim()
.toLowerCase()
.replace(/[^a-z0-9-]/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "")
.slice(0, 30); // Max 30 chars
return slug || null;
}
}
return null;
} catch (err) {
const message = err instanceof Error ? (err.stack ?? err.message) : String(err);
log.error(`Failed to generate slug: ${message}`);
return null;
} finally {
// Clean up temporary session file
if (tempSessionFile) {
try {
await fs.rm(path.dirname(tempSessionFile), { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
}
}
}