diff --git a/src/config/sessions/transcript-mirror.ts b/src/config/sessions/transcript-mirror.ts new file mode 100644 index 00000000000..7a926b95820 --- /dev/null +++ b/src/config/sessions/transcript-mirror.ts @@ -0,0 +1,52 @@ +import path from "node:path"; + +function stripQuery(value: string): string { + const noHash = value.split("#")[0] ?? value; + return noHash.split("?")[0] ?? noHash; +} + +function extractFileNameFromMediaUrl(value: string): string | null { + const trimmed = value.trim(); + if (!trimmed) { + return null; + } + const cleaned = stripQuery(trimmed); + try { + const parsed = new URL(cleaned); + const base = path.basename(parsed.pathname); + if (!base) { + return null; + } + try { + return decodeURIComponent(base); + } catch { + return base; + } + } catch { + const base = path.basename(cleaned); + if (!base || base === "/" || base === ".") { + return null; + } + return base; + } +} + +export function resolveMirroredTranscriptText(params: { + text?: string; + mediaUrls?: string[]; +}): string | null { + const mediaUrls = params.mediaUrls?.filter((url) => url && url.trim()) ?? []; + if (mediaUrls.length > 0) { + const names = mediaUrls + .map((url) => extractFileNameFromMediaUrl(url)) + .filter((name): name is string => Boolean(name && name.trim())); + if (names.length > 0) { + return names.join(", "); + } + return "media"; + } + + const text = params.text ?? ""; + const trimmed = text.trim(); + return trimmed ? trimmed : null; +} diff --git a/src/config/sessions/transcript.runtime.ts b/src/config/sessions/transcript.runtime.ts new file mode 100644 index 00000000000..b886816b75c --- /dev/null +++ b/src/config/sessions/transcript.runtime.ts @@ -0,0 +1 @@ +export { appendAssistantMessageToSessionTranscript } from "./transcript.js"; diff --git a/src/config/sessions/transcript.ts b/src/config/sessions/transcript.ts index aba99d02945..b9d70901962 100644 --- a/src/config/sessions/transcript.ts +++ b/src/config/sessions/transcript.ts @@ -11,59 +11,9 @@ import { } from "./paths.js"; import { resolveAndPersistSessionFile } from "./session-file.js"; import { loadSessionStore, normalizeStoreSessionKey } from "./store.js"; +import { resolveMirroredTranscriptText } from "./transcript-mirror.js"; import type { SessionEntry } from "./types.js"; -function stripQuery(value: string): string { - const noHash = value.split("#")[0] ?? value; - return noHash.split("?")[0] ?? noHash; -} - -function extractFileNameFromMediaUrl(value: string): string | null { - const trimmed = value.trim(); - if (!trimmed) { - return null; - } - const cleaned = stripQuery(trimmed); - try { - const parsed = new URL(cleaned); - const base = path.basename(parsed.pathname); - if (!base) { - return null; - } - try { - return decodeURIComponent(base); - } catch { - return base; - } - } catch { - const base = path.basename(cleaned); - if (!base || base === "/" || base === ".") { - return null; - } - return base; - } -} - -export function resolveMirroredTranscriptText(params: { - text?: string; - mediaUrls?: string[]; -}): string | null { - const mediaUrls = params.mediaUrls?.filter((url) => url && url.trim()) ?? []; - if (mediaUrls.length > 0) { - const names = mediaUrls - .map((url) => extractFileNameFromMediaUrl(url)) - .filter((name): name is string => Boolean(name && name.trim())); - if (names.length > 0) { - return names.join(", "); - } - return "media"; - } - - const text = params.text ?? ""; - const trimmed = text.trim(); - return trimmed ? trimmed : null; -} - async function ensureSessionHeader(params: { sessionFile: string; sessionId: string; diff --git a/src/infra/outbound/deliver.test-helpers.ts b/src/infra/outbound/deliver.test-helpers.ts index 73f65bb025a..5760d8c1be2 100644 --- a/src/infra/outbound/deliver.test-helpers.ts +++ b/src/infra/outbound/deliver.test-helpers.ts @@ -100,10 +100,10 @@ export const internalHookMocks = _internalHookMocks; export const queueMocks = _queueMocks; export const logMocks = _logMocks; -vi.mock("../../config/sessions.js", async () => { - const actual = await vi.importActual( - "../../config/sessions.js", - ); +vi.mock("../../config/sessions/transcript.runtime.js", async () => { + const actual = await vi.importActual< + typeof import("../../config/sessions/transcript.runtime.js") + >("../../config/sessions/transcript.runtime.js"); return { ...actual, appendAssistantMessageToSessionTranscript: _mocks.appendAssistantMessageToSessionTranscript, diff --git a/src/infra/outbound/deliver.test.ts b/src/infra/outbound/deliver.test.ts index b314595a5ec..7582c91a268 100644 --- a/src/infra/outbound/deliver.test.ts +++ b/src/infra/outbound/deliver.test.ts @@ -41,10 +41,10 @@ const logMocks = vi.hoisted(() => ({ warn: vi.fn(), })); -vi.mock("../../config/sessions.js", async () => { - const actual = await vi.importActual( - "../../config/sessions.js", - ); +vi.mock("../../config/sessions/transcript.runtime.js", async () => { + const actual = await vi.importActual< + typeof import("../../config/sessions/transcript.runtime.js") + >("../../config/sessions/transcript.runtime.js"); return { ...actual, appendAssistantMessageToSessionTranscript: mocks.appendAssistantMessageToSessionTranscript, diff --git a/src/infra/outbound/deliver.ts b/src/infra/outbound/deliver.ts index 0e6cf93d32b..4cf125b107c 100644 --- a/src/infra/outbound/deliver.ts +++ b/src/infra/outbound/deliver.ts @@ -15,10 +15,7 @@ import type { ChannelOutboundContext, } from "../../channels/plugins/types.js"; import type { OpenClawConfig } from "../../config/config.js"; -import { - appendAssistantMessageToSessionTranscript, - resolveMirroredTranscriptText, -} from "../../config/sessions.js"; +import { resolveMirroredTranscriptText } from "../../config/sessions/transcript-mirror.js"; import { fireAndForgetHook } from "../../hooks/fire-and-forget.js"; import { createInternalHookEvent, triggerInternalHook } from "../../hooks/internal-hooks.js"; import { @@ -49,6 +46,14 @@ export { normalizeOutboundPayloads } from "./payloads.js"; export { resolveOutboundSendDep, type OutboundSendDeps } from "./send-deps.js"; const log = createSubsystemLogger("outbound/deliver"); +let transcriptRuntimePromise: + | Promise + | undefined; + +async function loadTranscriptRuntime() { + transcriptRuntimePromise ??= import("../../config/sessions/transcript.runtime.js"); + return await transcriptRuntimePromise; +} export type OutboundDeliveryResult = { channel: Exclude; @@ -791,6 +796,7 @@ async function deliverOutboundPayloadsCore( mediaUrls: params.mirror.mediaUrls, }); if (mirrorText) { + const { appendAssistantMessageToSessionTranscript } = await loadTranscriptRuntime(); await appendAssistantMessageToSessionTranscript({ agentId: params.mirror.agentId, sessionKey: params.mirror.sessionKey,