UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

268 lines (267 loc) • 12.5 kB
import { c as trackAsyncWork, r as getAsyncWorkSignal } from "./async-work-scope-BYeX3gh6.mjs"; import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.mjs"; import { A as withTimeout } from "./fs-safe-BUXBSN-4.mjs"; import { y as resolveNativeModelPrimary } from "./agent-scope-config-CDYpzA0r.mjs"; import { M as parseAgentSessionKey } from "./session-key-C09ow4h0.mjs"; import { t as splitTrailingAuthProfile } from "./model-ref-profile-BNjB2hED.mjs"; import "./agent-scope-CG17uXFn.mjs"; import { i as resolveSessionRuntimeOverrideForProvider } from "./session-runtime-compat-zlHVO23h.mjs"; import { i as isValidBase64 } from "./base64-CU53EX1R.mjs"; import { a as loadSessionEntry, s as patchSessionEntryCore } from "./session-accessor.sqlite-entry-Dn_MsDKD.mjs"; import { i as stripInboundMetadata } from "./strip-inbound-meta-BWIgBUDF.mjs"; import "./session-accessor-BDmTPdtx.mjs"; import { o as resolveStoredSessionKeyForAgentStore } from "./session-store-key-Ci099tm9.mjs"; import { n as resolveSessionModelRef } from "./session-model-ref-3-J97GZs.mjs"; import { t as runWithAsyncWorkResources } from "./async-work-resources-Dpudqxo5.mjs"; import { t as createCrustaceanSlug } from "./session-slug-DsU90tbm.mjs"; import { t as readSessionTitleFieldsFromTranscript } from "./session-transcript-title-reader-CFRYAXsj.mjs"; import { a as resolveUtilityModelRefForAgent } from "./utility-model-Bw35wBFc.mjs"; import { n as resolveExplicitSessionName, r as sessionTitleRequests, t as hasExplicitSessionName } from "./session-title-state-BQCPqYS_.mjs"; import { AsyncLocalStorage } from "node:async_hooks"; //#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_WAIT_MS = 3e4; 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."; 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 (!isValidBase64(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 isAutoTitleSessionKey(sessionKey) { const rest = parseAgentSessionKey(sessionKey)?.rest ?? ""; return rest.startsWith("dashboard:") || rest.startsWith("ios-") || rest.startsWith("node-"); } /** True when this interactive chat key should receive an automatic topic title. */ function isDashboardSessionTitleCandidate(params) { const sourceText = params.userMessage.trim(); return Boolean(sourceText && !sourceText.startsWith("/") && isAutoTitleSessionKey(params.sessionKey)); } function resolveDashboardTitleAuthProfile(params) { const sessionProfile = params.entry?.authProfileOverride?.trim(); if (sessionProfile) return sessionProfile; const configuredRef = resolveNativeModelPrimary(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 }); 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 generateLabel = async () => { try { const { generateConversationLabelWithFallback } = await import("./conversation-label-generator-DV7BvihY.mjs"); params.assertCurrent?.(); params.abortSignal?.throwIfAborted(); const generated = await generateConversationLabelWithFallback({ userMessage: sourceText, 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, operatorAuthority: params.operatorAuthority, ...params.utilityOnly ? { utilityOnly: true } : {} }); if (generated) return normalizeDashboardSessionTitle(generated); } catch { params.assertCurrent?.(); params.abortSignal?.throwIfAborted(); } return null; }; let title = await generateLabel(); if (!title && params.retryAfter) { await params.retryAfter; params.assertCurrent?.(); params.abortSignal?.throwIfAborted(); title = await generateLabel(); } if (title) return title; if (params.utilityOnly) return null; params.onFallback?.(); return createCrustaceanSlug(); } /** 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).then((persisted) => { if (persisted) params.onPersisted(); }); try { await withTimeout(request, WORKTREE_SESSION_TITLE_WAIT_MS, "worktree title generation"); } catch (error) { params.onError(error); } const readCurrent = (assertSourceCurrent) => { params.commitGuard?.(); assertSourceCurrent?.(); 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); }; return params.withSource ? await params.withSource((source) => readCurrent(source.assertCurrent)) : readCurrent(); } async function maybeGenerateDashboardSessionTitle(params) { return isDashboardSessionTitleCandidate(params) && await maybeGenerateSessionTitle({ ...params, retryFailedJoin: true }); } /** Joins existing work; only the caller that persists a title returns true. */ async function maybeGenerateSessionTitle(params) { const sessionKey = resolveStoredSessionKeyForAgentStore(params); const scope = { agentId: params.agentId, sessionKey, storePath: params.storePath }; const requestTarget = { ...scope, sessionId: params.sessionId }; const existing = sessionTitleRequests.get(requestTarget); if (existing) return !await (params.retryFailedJoin ? existing.catch(() => false) : existing) && params.retryFailedJoin ? await maybeGenerateSessionTitle({ ...params, retryFailedJoin: false }) : false; const finish = async (generation) => { const displayName = await generation; if (!displayName) return false; const persist = async (assertSourceCurrent) => { const assertCommitAllowed = assertSourceCurrent ? () => { params.commitGuard?.(); assertSourceCurrent(); } : params.commitGuard; if (assertSourceCurrent) assertCommitAllowed?.(); let persisted = false; await patchSessionEntryCore(scope, (current) => { if (current.sessionId !== params.sessionId || hasExplicitSessionName(current)) return null; persisted = true; return { displayName }; }, { requireWriteSuccess: true, ...assertCommitAllowed ? { assertCommitAllowed } : {} }); return persisted; }; return params.withSource ? await params.withSource((source) => persist(source.assertCurrent)) : await persist(); }; return await sessionTitleRequests.run(requestTarget, () => Promise.resolve().then(async () => { const entry = loadSessionEntry(scope); if (hasExplicitSessionName(entry) || entry?.sessionId !== params.sessionId) return false; 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 false; const generate = (abortSignal) => generateDashboardSessionTitle({ cfg: params.cfg, agentId: params.agentId, entry: params.entry ?? entry, userMessage: sourceText, operatorAuthority: params.operatorAuthority, ...abortSignal ? { abortSignal } : {}, ...params.retryAfter ? { retryAfter: params.retryAfter } : {}, onFallback: params.onFallback }); const withSource = params.withSource; if (!withSource) { params.commitGuard?.(); return await finish(generate()); } return await runWithAsyncWorkResources(async () => { const runInGenerationContext = AsyncLocalStorage.snapshot(); const pending = await withSource((source) => { params.commitGuard?.(); source.assertCurrent(); const completion = runInGenerationContext(() => trackAsyncWork(() => generate(getAsyncWorkSignal()))); completion.catch(() => void 0); return { completion }; }); return await finish(pending.completion); }, { cancelOnError: true }); })); } //#endregion export { maybeGenerateSessionTitle as a, maybeGenerateDashboardSessionTitle as i, generateWorktreeSessionTitle as n, prepareDashboardSessionTitle as o, isDashboardSessionTitleCandidate as r, buildDashboardSessionTitleSource as t };