openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
230 lines (229 loc) • 10.9 kB
JavaScript
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 };