UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

230 lines (229 loc) 10.9 kB
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js"; import { i as getOrCreatePromise } from "./lazy-promise-DGqyc4Y4.js"; import { k as withTimeout } from "./fs-safe-B6pvPGnf.js"; import { O as parseAgentSessionKey } from "./session-key-BnWWjqNc.js"; import { i as stripInboundMetadata } from "./strip-inbound-meta-D5F_RINt.js"; import { t as splitTrailingAuthProfile } from "./model-ref-profile-BIKs-96s.js"; import { o as resolveAgentEffectiveModelPrimary } from "./agent-scope-DbtJyKUL.js"; import "./session-accessor-YsytfDtG.js"; import { d as patchSessionEntryCore, l as loadSessionEntry } from "./session-accessor.sqlite-entry-CWk3jL7s.js"; import { a as resolveStoredSessionKeyForAgentStore } from "./session-store-key-8xEjWSNi.js"; import { i as resolveSessionRuntimeOverrideForProvider } from "./session-runtime-compat-DNAICzi6.js"; import { n as resolveSessionModelRef } from "./session-model-ref-CPZiclLt.js"; import { d as readSessionTitleFieldsFromTranscript } from "./session-utils-list-B0k8KJn5.js"; import { n as resolveUtilityModelRefForAgent } from "./utility-model-C0ozcQ73.js"; import { n as generateConversationLabelWithFallback } from "./conversation-label-generator-CrCENK4e.js"; import { a as isValidAttachmentBase64 } from "./chat-attachments-DAJvw83V.js"; //#region src/gateway/dashboard-session-title.ts const DASHBOARD_SESSION_TITLE_MAX_CHARS = 60; const DASHBOARD_SESSION_TITLE_SOURCE_MAX_CHARS = 1e3; const WORKTREE_SESSION_TITLE_TIMEOUT_MS = 8e3; const WORKTREE_SESSION_TITLE_ATTEMPT_TIMEOUT_MS = 4e3; const DASHBOARD_SESSION_TITLE_PROMPT = "Generate a concise session title (3-6 words, max 60 characters) from the user's first message. Use the same language as the message, in sentence case: capitalize only the first word and words that language always capitalizes. No emoji. Return only the title."; const sessionTitleRequests = /* @__PURE__ */ new Map(); function decodeTextAttachmentPrefix(attachment, maxChars) { const mimeType = attachment.mimeType?.trim().toLowerCase(); const content = attachment.content; if (!mimeType?.startsWith("text/") || typeof content !== "string" || !content) return null; if (!isValidAttachmentBase64(content)) return null; const maxBase64Chars = Math.ceil((maxChars * 3 + 3) / 3) * 4; const truncated = content.length > maxBase64Chars; const prefixLength = truncated ? maxBase64Chars : content.length; const prefix = content.slice(0, prefixLength); const bytes = Buffer.from(prefix, "base64"); try { return new TextDecoder("utf-8", { fatal: true }).decode(bytes, { stream: truncated }); } catch { return null; } } /** Builds the bounded model source shared by dashboard and worktree titles. */ function buildDashboardSessionTitleSource(params) { const visibleMessage = params.message.trim(); const slashCommand = visibleMessage.startsWith("/"); let source = slashCommand ? "" : visibleMessage; for (const attachment of params.attachments ?? []) { const separatorLength = source ? 1 : 0; const remaining = DASHBOARD_SESSION_TITLE_SOURCE_MAX_CHARS - source.length - separatorLength; if (remaining <= 0) break; const text = decodeTextAttachmentPrefix(attachment, remaining)?.trim(); if (!text) continue; source += `${source ? "\n" : ""}${truncateUtf16Safe(text, remaining)}`; } if (!source && slashCommand) return truncateUtf16Safe(visibleMessage, DASHBOARD_SESSION_TITLE_SOURCE_MAX_CHARS); return truncateUtf16Safe(source.trim(), DASHBOARD_SESSION_TITLE_SOURCE_MAX_CHARS); } function resolveExplicitSessionName(entry) { return [ entry?.label, entry?.displayName, entry?.subject, entry?.groupChannel, entry?.space ].map((value) => value?.trim()).find(Boolean); } function hasExplicitSessionName(entry) { return Boolean(resolveExplicitSessionName(entry)); } function isDashboardSessionKey(sessionKey) { return parseAgentSessionKey(sessionKey)?.rest.startsWith("dashboard:") === true; } function isDashboardSessionTitleCandidate(params) { const sourceText = params.userMessage.trim(); return Boolean(sourceText && !sourceText.startsWith("/") && isDashboardSessionKey(params.sessionKey)); } function resolveDashboardTitleAuthProfile(params) { const sessionProfile = params.entry?.authProfileOverride?.trim(); if (sessionProfile) return sessionProfile; const configuredRef = resolveAgentEffectiveModelPrimary(params.cfg, params.agentId)?.trim(); const configuredProfile = configuredRef ? splitTrailingAuthProfile(configuredRef).profile : void 0; if (!configuredProfile) return; return resolveSessionModelRef(params.cfg, void 0, params.agentId).provider === params.regularProvider ? configuredProfile : void 0; } function normalizeDashboardSessionTitle(raw) { const firstLine = raw.replace(/\r/g, "").split("\n").map((line) => line.trim()).find((line) => line && !line.startsWith("```")); if (!firstLine) return null; const normalized = firstLine.replace(/^\s*(?:title\s*:\s*)?/i, "").replace(/^["'`]+|["'`]+$/g, "").replace(/\s+/g, " ").trim(); return normalized ? truncateUtf16Safe(normalized, DASHBOARD_SESSION_TITLE_MAX_CHARS) : null; } async function generateDashboardSessionTitle(params) { const sourceText = buildDashboardSessionTitleSource({ message: params.userMessage, attachments: params.attachments }); if (!sourceText || sourceText.startsWith("/")) return null; const regularModel = resolveSessionModelRef(params.cfg, params.entry, params.agentId); const agentHarnessRuntimeOverride = resolveSessionRuntimeOverrideForProvider({ provider: regularModel.provider, entry: params.entry, cfg: params.cfg }); const preferredProfile = resolveDashboardTitleAuthProfile({ cfg: params.cfg, agentId: params.agentId, entry: params.entry, regularProvider: regularModel.provider }); const regularModelRef = `${regularModel.provider}/${regularModel.model}${preferredProfile ? `@${preferredProfile}` : ""}`; const utilityModelRef = resolveUtilityModelRefForAgent({ cfg: params.cfg, agentId: params.agentId, primaryProvider: regularModel.provider, primaryModelRef: regularModelRef }); const generated = await generateConversationLabelWithFallback({ userMessage: truncateUtf16Safe(sourceText, DASHBOARD_SESSION_TITLE_SOURCE_MAX_CHARS), prompt: DASHBOARD_SESSION_TITLE_PROMPT, cfg: params.cfg, agentId: params.agentId, ...agentHarnessRuntimeOverride ? { agentHarnessRuntimeOverride } : {}, ...utilityModelRef ? { utilityModelRef } : {}, regularModelRef, ...preferredProfile ? { preferredProfile } : {}, normalizeLabel: normalizeDashboardSessionTitle, maxLength: DASHBOARD_SESSION_TITLE_MAX_CHARS, abortSignal: params.abortSignal, assertCurrent: params.assertCurrent, ...params.timeoutMs ? { timeoutMs: params.timeoutMs } : {}, ...params.utilityOnly ? { utilityOnly: true } : {} }); return generated ? normalizeDashboardSessionTitle(generated) : null; } /** Prepares a creation draft's title without creating or updating a session. */ async function prepareDashboardSessionTitle(params) { try { return await generateDashboardSessionTitle({ ...params, utilityOnly: true }); } catch { params.assertCurrent?.(); params.abortSignal?.throwIfAborted(); return null; } } /** Worktree callers bound their own wait without cancelling another naming owner's request. */ async function generateWorktreeSessionTitle(params) { const request = maybeGenerateSessionTitle({ ...params, worktree: true }).then(async (attempt) => { if (attempt.kind === "in-flight") await attempt.settled; else if (attempt.kind === "persisted") params.onPersisted(); }); try { await withTimeout(request, WORKTREE_SESSION_TITLE_TIMEOUT_MS, "worktree title generation"); } catch (error) { params.onError(error); } params.commitGuard?.(); const current = loadSessionEntry({ agentId: params.agentId, sessionKey: resolveStoredSessionKeyForAgentStore(params), storePath: params.storePath }); if (current?.sessionId !== params.sessionId) throw new Error("Session changed while naming its worktree; retry from the current session."); return resolveExplicitSessionName(current); } async function maybeGenerateDashboardSessionTitle(params) { const sourceText = params.userMessage.trim(); if (!isDashboardSessionTitleCandidate({ sessionKey: params.sessionKey, userMessage: sourceText })) return false; return (await maybeGenerateSessionTitle({ ...params, userMessage: sourceText })).kind === "persisted"; } async function maybeGenerateSessionTitle(params) { const sessionKey = resolveStoredSessionKeyForAgentStore(params); const scope = { agentId: params.agentId, sessionKey, storePath: params.storePath }; const entry = loadSessionEntry(scope); if (hasExplicitSessionName(entry) || entry?.sessionId !== params.sessionId) return { kind: "skipped" }; const requestKey = `${params.storePath}\0${sessionKey}\0${params.sessionId}`; const existing = sessionTitleRequests.get(requestKey); if (existing) return { kind: "in-flight", settled: existing }; const transcriptSource = readSessionTitleFieldsFromTranscript({ agentId: params.agentId, sessionEntry: entry, sessionId: params.sessionId, sessionKey, storePath: params.storePath }).firstUserMessage; const transcriptText = transcriptSource ? stripInboundMetadata(transcriptSource).trim() : ""; const currentText = params.currentUserMessage?.trim() ?? ""; const sourceText = entry.pendingWorktree?.titleSource?.trim() ?? (!transcriptText || currentText && currentText === transcriptText ? params.userMessage.trim() : transcriptText); if (!sourceText) return { kind: "skipped" }; return await getOrCreatePromise(sessionTitleRequests, requestKey, () => Promise.resolve().then(async () => { params.commitGuard?.(); const generation = generateDashboardSessionTitle({ cfg: params.cfg, agentId: params.agentId, entry: params.entry ?? entry, userMessage: sourceText, ...params.worktree ? { timeoutMs: WORKTREE_SESSION_TITLE_ATTEMPT_TIMEOUT_MS } : {} }); const displayName = await (params.worktree ? withTimeout(generation, WORKTREE_SESSION_TITLE_TIMEOUT_MS, "worktree title generation") : generation); if (!displayName) return false; let persisted = false; await patchSessionEntryCore(scope, (current) => { if (current.sessionId !== params.sessionId || hasExplicitSessionName(current)) return null; persisted = true; return { displayName }; }, { requireWriteSuccess: true, ...params.commitGuard ? { assertCommitAllowed: params.commitGuard } : {} }); return persisted; }), { evictOnSettled: true }) ? { kind: "persisted" } : { kind: "skipped" }; } //#endregion export { maybeGenerateDashboardSessionTitle as a, resolveExplicitSessionName as c, isDashboardSessionTitleCandidate as i, generateWorktreeSessionTitle as n, maybeGenerateSessionTitle as o, hasExplicitSessionName as r, prepareDashboardSessionTitle as s, buildDashboardSessionTitleSource as t };