UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

594 lines (593 loc) 24.7 kB
import { r as asNullableRecord } from "./record-coerce-DItp3I4t.js"; import { n as sliceUtf16Safe } from "./utf16-slice-D_ngcYKd.js"; import { m as stripInternalRuntimeContext } from "./internal-runtime-context-UgZVJlox.js"; import { c as stripLeadingSilentToken, n as SILENT_REPLY_TOKEN, s as startsWithSilentToken } from "./tokens-DbeAFqg7.js"; import { r as jsonUtf8Bytes } from "./json-utf8-bytes-fm9i4b7G.js"; import { a as stripInlineDirectiveTagsForDisplay } from "./directive-tags-Wp1S72ap.js"; import { o as resolveAssistantEventPhase } from "./chat-message-content-Dws_XUEQ.js"; import { n as splitMediaFromOutput, t as isRelativeAssistantMediaReference } from "./parse-BR5Zk6rK.js"; import { n as isSuppressedControlReplyText, r as stripSuppressedControlReplyToken, t as isSuppressedControlReplyLeadFragment } from "./control-reply-text-D2BZ73-q.js"; import { m as stripAssistantMediaDirectivesForDisplay } from "./chat-display-projection.helpers-DYkWC7LH.js"; import { n as splitTrailingDirective } from "./streaming-directives-teBjtpNF.js"; //#region src/gateway/live-chat-projector.ts const MAX_LIVE_CHAT_BUFFER_CHARS = 5e5; /** Normalizes assistant event payloads that contain a snapshot, a delta, or both. */ function resolveAssistantLiveChatInput(data) { if (!data || typeof data !== "object") return; const record = data; if (typeof record.text !== "string" && typeof record.delta !== "string") return; return { text: typeof record.text === "string" ? record.text : "", delta: typeof record.delta === "string" ? record.delta : "", replaceStream: record.replace === true && record.replaceable === true, ...typeof record.itemId === "string" && record.itemId ? { itemId: record.itemId } : {}, ...Array.isArray(record.managedMediaUrls) ? { managedMediaUrls: record.managedMediaUrls.filter((url) => typeof url === "string") } : {} }; } /** Merges assistant full-text and delta events into a capped live buffer. */ function resolveMergedAssistantText(params) { const { previousText, nextText, nextDelta, scope } = params; let text; if (scope) text = scope.prefix + nextText; else if (previousText && nextText.length > previousText.length && nextText.startsWith(previousText)) text = nextText; else if (nextDelta) text = previousText + nextDelta; else text = previousText.startsWith(nextText) ? previousText : nextText; const capped = text.length > MAX_LIVE_CHAT_BUFFER_CHARS ? sliceUtf16Safe(text, -5e5) : text; if (scope) scope.prefix = sliceUtf16Safe(scope.prefix, text.length - capped.length); return capped; } /** Removes runtime-only context/directive tags from the merged live assistant buffer. */ function normalizeLiveAssistantBufferedText(text, options) { const normalized = stripInternalRuntimeContext(stripInlineDirectiveTagsForDisplay(text).text); const trailing = options?.final ? { text: normalized, tail: "" } : splitTrailingDirective(normalized); const parsedTail = trailing.tail ? splitMediaFromOutput(trailing.tail, { extractAudioDirectives: false, extractMarkdownImages: false }) : void 0; const withoutPendingMediaTail = parsedTail?.mediaUrls?.length && parsedTail.mediaUrls.every((url) => !isRelativeAssistantMediaReference(url)) ? normalized : trailing.text; return stripAssistantMediaDirectivesForDisplay(withoutPendingMediaTail, options?.managedMediaUrls ?? []); } /** Projects buffered assistant text into display text or a suppressed/pending state. */ function projectLiveAssistantBufferedText(rawText, options) { if (!rawText) return { text: "", suppress: true, pendingLeadFragment: false }; if (isSuppressedControlReplyText(rawText)) return { text: "", suppress: true, pendingLeadFragment: false }; if (options?.suppressLeadFragments !== false && isSuppressedControlReplyLeadFragment(rawText)) return { text: rawText, suppress: true, pendingLeadFragment: true }; const withoutTrailingControlToken = stripSuppressedControlReplyToken(rawText); if (!withoutTrailingControlToken) return { text: "", suppress: true, pendingLeadFragment: false }; const text = startsWithSilentToken(withoutTrailingControlToken, "NO_REPLY") ? stripLeadingSilentToken(withoutTrailingControlToken, SILENT_REPLY_TOKEN) : withoutTrailingControlToken; if (!text || isSuppressedControlReplyText(text)) return { text: "", suppress: true, pendingLeadFragment: false }; if (options?.suppressLeadFragments !== false && isSuppressedControlReplyLeadFragment(text)) return { text, suppress: true, pendingLeadFragment: true }; return { text, suppress: false, pendingLeadFragment: false }; } /** Returns true when an assistant event phase should not appear in live chat. */ function shouldSuppressAssistantEventForLiveChat(data) { return resolveAssistantEventPhase(data) === "commentary"; } //#endregion //#region src/gateway/server-chat-progress-snapshot.ts const CHAT_RUN_PROGRESS_MAX_EVENTS = 50; const CHAT_RUN_PROGRESS_MAX_BYTES = 131072; const CHAT_RUN_PROGRESS_MAX_EVENT_BYTES = 65536; const CHAT_RUN_PROGRESS_MAX_REVIEWS_PER_TOOL = 16; function updateChatRunProgressSnapshot(snapshot, event, mode = "full") { const data = event.data ?? {}; const phase = typeof data.phase === "string" ? data.phase : ""; const toolCallId = typeof data.toolCallId === "string" ? data.toolCallId.trim() : ""; const review = asNullableRecord(data.review) ?? void 0; const reviewId = typeof review?.id === "string" ? review.id.trim() : ""; const isStartupStatus = event.stream === "run_status" && [ "preparing_workspace", "naming_worktree", "creating_worktree", "running_setup", "provisioning_environment", "preparing_context", "starting_model" ].includes(phase); const preambleItemId = typeof data.itemId === "string" && data.itemId.trim() ? data.itemId.trim() : typeof data.id === "string" && data.id.trim() ? data.id.trim() : ""; const isTool = event.stream === "tool" && Boolean(toolCallId) && [ "start", "input_delta", "update", "review", "result" ].includes(phase) && (phase !== "review" || mode === "full" && Boolean(reviewId)); const isPreamble = event.stream === "item" && data.kind === "preamble"; const isUsage = event.stream === "usage"; const isNotice = event.stream === "notice" && phase === "warning"; const guardianTargetItemId = typeof data.targetItemId === "string" ? data.targetItemId.trim() : ""; const isGuardian = event.stream === "codex_app_server.guardian"; const isStandaloneGuardian = isGuardian && (phase === "warning" || phase === "strict_review_required" || (phase === "started" || phase === "completed") && !guardianTargetItemId); const resolvesStrictReview = isGuardian && phase === "completed" && Boolean(guardianTargetItemId) && snapshot?.events.some((candidate) => candidate.stream === event.stream && candidate.data.phase === "strict_review_required" && candidate.data.reviewId === data.reviewId); if (mode === "summary" && !isTool && !isPreamble && !isUsage) return snapshot; if (!isTool && !isPreamble && !isUsage && !isStartupStatus && !isStandaloneGuardian && !isNotice && !resolvesStrictReview) return snapshot; const next = snapshot ?? { events: [], byteLength: 0, lastSeq: 0 }; if (event.seq <= next.lastSeq) return next; next.lastSeq = event.seq; const matchesPreamble = (candidate) => candidate.stream === "item" && candidate.data?.kind === "preamble" && (candidate.data.itemId ?? "") === preambleItemId; const previousPreamble = preambleItemId ? next.events.find(matchesPreamble) : void 0; const previousUsage = isUsage ? next.events.find((candidate) => candidate.stream === "usage") : void 0; const removeWhere = (predicate) => { next.events = next.events.filter((candidate) => !predicate(candidate)); next.byteLength = next.events.reduce((total, candidate) => total + jsonUtf8Bytes(candidate), 0); }; if (isStartupStatus && next.events.some((candidate) => candidate.stream === "tool" || candidate.stream === "item")) return next; if (isUsage) removeWhere((candidate) => candidate.stream === "usage"); else if (isStartupStatus || isTool || isPreamble) { removeWhere((candidate) => { if (candidate.stream === "run_status") return true; if (isPreamble) return matchesPreamble(candidate); if (!isTool || candidate.stream !== "tool" || candidate.data?.toolCallId !== toolCallId) return false; if (phase === "start") return true; if (phase === "result") return candidate.data?.phase === "result"; if (phase !== "review" || candidate.data?.phase !== "review") return candidate.data?.phase === phase; return asNullableRecord(candidate.data.review)?.id === reviewId; }); if (isPreamble && !(typeof data.progressText === "string" && data.progressText.trim())) return next; } else if ((isStandaloneGuardian || resolvesStrictReview) && typeof data.reviewId === "string") { removeWhere((candidate) => candidate.stream === event.stream && candidate.data?.reviewId === data.reviewId); if (resolvesStrictReview) return next; } const storedData = isTool ? mode === "summary" ? { phase, name: typeof data.name === "string" ? data.name : void 0, toolCallId } : { phase, name: typeof data.name === "string" ? data.name : void 0, toolCallId, args: phase === "start" ? data.args : void 0, partialResult: phase === "update" ? data.partialResult : void 0, diff: phase === "input_delta" ? data.diff : void 0, review: phase === "review" ? data.review : void 0, approvalReviewOutcome: phase === "review" || phase === "result" ? data.approvalReviewOutcome : void 0, isError: phase === "result" ? data.isError : void 0, result: phase === "result" ? data.result : void 0 } : isPreamble ? { kind: "preamble", itemId: preambleItemId || void 0, progressText: data.progressText } : { ...previousUsage?.data, ...data }; for (const key of Object.keys(storedData)) if (storedData[key] === void 0) delete storedData[key]; let storedEvent = { runId: event.runId, seq: event.seq, stream: event.stream, ts: previousPreamble?.ts ?? event.ts, data: storedData, ...event.sessionKey ? { sessionKey: event.sessionKey } : {}, ...event.agentId ? { agentId: event.agentId } : {} }; let eventBytes = jsonUtf8Bytes(storedEvent); if (eventBytes > CHAT_RUN_PROGRESS_MAX_EVENT_BYTES && isTool) { delete storedData.args; delete storedData.partialResult; delete storedData.diff; delete storedData.result; storedEvent = { ...storedEvent, data: storedData }; eventBytes = jsonUtf8Bytes(storedEvent); } if (eventBytes > CHAT_RUN_PROGRESS_MAX_EVENT_BYTES) return next; next.events.push(storedEvent); next.byteLength += eventBytes; if (phase === "review") { const reviews = next.events.filter((candidate) => candidate.stream === "tool" && candidate.data?.toolCallId === toolCallId && candidate.data?.phase === "review"); const overflow = reviews.length - CHAT_RUN_PROGRESS_MAX_REVIEWS_PER_TOOL; if (overflow > 0) { const evicted = new Set(reviews.slice(0, overflow)); removeWhere((candidate) => evicted.has(candidate)); } } while (next.events.length > CHAT_RUN_PROGRESS_MAX_EVENTS || next.byteLength > CHAT_RUN_PROGRESS_MAX_BYTES) { const oldest = next.events.find((candidate) => candidate.stream !== "usage"); if (!oldest) break; const oldestToolCallId = oldest.stream === "tool" && typeof oldest.data?.toolCallId === "string" ? oldest.data.toolCallId : ""; removeWhere((candidate) => oldestToolCallId ? candidate.stream === "tool" && candidate.data?.toolCallId === oldestToolCallId : candidate === oldest); } return next; } //#endregion //#region src/gateway/server-chat-state.ts let chatRunOrderingSequence = 0; function nextChatRunOrderingSequence() { chatRunOrderingSequence += 1; return chatRunOrderingSequence; } /** Stamp a chat run registration with the process-local ordering metadata used for abort freshness checks. */ function createChatRunEntry(entry) { return { ...entry, registeredSequence: nextChatRunOrderingSequence() }; } /** Create an abort marker ordered against chat run registrations, using a shared monotonic sequence. */ function createChatAbortMarker(now = Date.now()) { return { abortedAtMs: now, sequence: nextChatRunOrderingSequence() }; } /** Return the wall-clock timestamp used by maintenance TTL pruning. */ function chatAbortMarkerTimestampMs(marker) { return marker.abortedAtMs; } /** * Return whether an abort marker should suppress events for the given chat run registration. * The shared monotonic sequence keeps same-millisecond aborts ordered; a missing * entry preserves suppress-on-presence behavior. */ function isChatAbortMarkerCurrent(marker, entry) { if (marker === void 0) return false; return !entry || marker.sequence >= entry.registeredSequence; } function createChatRunRecordStore() { const runs = /* @__PURE__ */ new Map(); const getOrCreate = (runId) => { const existing = runs.get(runId); if (existing) return existing; const record = {}; runs.set(runId, record); return record; }; const releaseIfEmpty = (runId) => { const record = runs.get(runId); if (!record || Object.keys(record).length > 0) return; runs.delete(runId); }; return { runs, getOrCreate, releaseIfEmpty }; } function clearPendingLiveTextFlushes(record) { for (const pending of Object.values(record.pendingTextFlushes ?? {})) clearTimeout(pending.timer); delete record.pendingTextFlushes; } function createChatRunRegistryForStore(store) { const add = (sessionId, entry) => { const registeredEntry = createChatRunEntry(entry); const record = store.getOrCreate(sessionId); const queue = record.registrations; if (queue) queue.push(registeredEntry); else record.registrations = [registeredEntry]; }; const peek = (sessionId) => store.runs.get(sessionId)?.registrations?.[0]; const shift = (sessionId) => { const record = store.runs.get(sessionId); if (!record) return; const queue = record.registrations; if (!queue || queue.length === 0) return; const entry = queue.shift(); if (!queue.length) { delete record.registrations; store.releaseIfEmpty(sessionId); } return entry; }; const remove = (sessionId, clientRunId, sessionKey) => { const record = store.runs.get(sessionId); if (!record) return; const queue = record.registrations; if (!queue || queue.length === 0) return; const idx = queue.findIndex((entry) => entry.clientRunId === clientRunId && (sessionKey ? entry.sessionKey === sessionKey : true)); if (idx < 0) return; const [entry] = queue.splice(idx, 1); if (!queue.length) { delete record.registrations; store.releaseIfEmpty(sessionId); } return entry; }; return { add, peek, shift, remove }; } /** Create the single record map used by Gateway chat-run runtime state. */ function createChatRunState() { const store = createChatRunRecordStore(); const registry = createChatRunRegistryForStore(store); const toolEventRecipients = createToolEventRecipientRegistryForStore(store); const recordProgressEvent = (runId, event, mode) => { const progressSnapshot = updateChatRunProgressSnapshot(store.runs.get(runId)?.progressSnapshot, event, mode); if (progressSnapshot) store.getOrCreate(runId).progressSnapshot = progressSnapshot; }; const clearRun = (runId) => { const record = store.runs.get(runId); if (!record) return; delete record.rawBuffer; delete record.buffer; delete record.bufferIsCurrent; record.liveTextGroup?.abort(); delete record.liveTextGroup; delete record.bufferProjection; delete record.planSnapshot; delete record.progressSnapshot; delete record.bufferUpdatedAt; delete record.deltaSentAt; delete record.assistantScope; delete record.managedMediaUrls; delete record.deltaLastBroadcastText; clearPendingLiveTextFlushes(record); delete record.agentText; store.releaseIfEmpty(runId); }; const clear = () => { for (const record of store.runs.values()) { clearPendingLiveTextFlushes(record); record.liveTextGroup?.abort(); } store.runs.clear(); }; const resolveBuffer = (runId, options) => { const record = store.runs.get(runId); if (!record || record.bufferIsCurrent?.() === false) return projectLiveAssistantBufferedText(""); const rawText = record.rawBuffer; if (rawText === void 0) return projectLiveAssistantBufferedText(record.buffer ?? ""); if (!options?.final && record.bufferProjection?.source === rawText && record.buffer !== void 0) return { text: record.buffer, suppress: record.bufferProjection.suppress }; const projected = projectLiveAssistantBufferedText(normalizeLiveAssistantBufferedText(rawText, { ...options, managedMediaUrls: record.managedMediaUrls ? [...record.managedMediaUrls] : void 0 })); if (!options?.final) { record.buffer = projected.text; record.bufferProjection = { source: rawText, suppress: projected.suppress }; } return projected; }; return { runs: store.runs, registry, toolEventRecipients, getOrCreate: store.getOrCreate, resolveBuffer, hasAbortMarker: (runId) => store.runs.get(runId)?.abortMarker !== void 0, deleteAbortMarker: (runId) => { const record = store.runs.get(runId); if (!record) return; delete record.abortMarker; store.releaseIfEmpty(runId); }, recordProgressEvent, clearRun, clear }; } const TOOL_EVENT_RECIPIENT_TTL_MS = 6e5; const TOOL_EVENT_RECIPIENT_FINAL_GRACE_MS = 3e4; /** Create the broad sessions.changed subscriber registry. */ function createSessionEventSubscriberRegistry(isConnectionActive) { const connIds = /* @__PURE__ */ new Set(); const empty = /* @__PURE__ */ new Set(); return { subscribe: (connId) => { const normalized = connId.trim(); if (!normalized || isConnectionActive?.(normalized) === false) return; connIds.add(normalized); }, unsubscribe: (connId) => { const normalized = connId.trim(); if (!normalized) return; connIds.delete(normalized); }, getAll: () => connIds.size > 0 ? connIds : empty }; } /** Create the per-session message subscriber registry. */ function createSessionMessageSubscriberRegistry(isConnectionActive) { const sessionToConnIds = /* @__PURE__ */ new Map(); const connections = /* @__PURE__ */ new Map(); const approvalSessionToConnIds = /* @__PURE__ */ new Map(); const changeListeners = /* @__PURE__ */ new Set(); const empty = /* @__PURE__ */ new Set(); let subscriptionSequence = 0; const normalize = (value) => value.trim(); const setMessageSubscription = (connId, sessionKey, subscribed) => { const connIds = sessionToConnIds.get(sessionKey); const wasSubscribed = connIds?.has(connId) === true; if (subscribed) { const nextConnIds = connIds ?? /* @__PURE__ */ new Set(); nextConnIds.add(connId); sessionToConnIds.set(sessionKey, nextConnIds); if (!wasSubscribed) for (const listener of changeListeners) listener(sessionKey); return; } connIds?.delete(connId); if (connIds?.size === 0) sessionToConnIds.delete(sessionKey); if (wasSubscribed) for (const listener of changeListeners) listener(sessionKey); }; const setApprovalSubscription = (connId, sessionKey, subscribed) => { const connIds = approvalSessionToConnIds.get(sessionKey); if (subscribed) { const nextConnIds = connIds ?? /* @__PURE__ */ new Set(); nextConnIds.add(connId); approvalSessionToConnIds.set(sessionKey, nextConnIds); return; } connIds?.delete(connId); if (connIds?.size === 0) approvalSessionToConnIds.delete(sessionKey); }; return { subscribe: (connId, sessionKey, opts) => { const normalizedConnId = normalize(connId); const normalizedSessionKey = normalize(sessionKey); if (!normalizedConnId || !normalizedSessionKey || isConnectionActive?.(normalizedConnId) === false) return; const states = connections.get(normalizedConnId) ?? /* @__PURE__ */ new Map(); const previous = states.get(normalizedSessionKey); const state = typeof previous === "object" ? previous : { base: previous, inflight: 0 }; state.inflight += 1; states.set(normalizedSessionKey, state); connections.set(normalizedConnId, states); subscriptionSequence += 1; const provisionalRecency = subscriptionSequence; setMessageSubscription(normalizedConnId, normalizedSessionKey, true); setApprovalSubscription(normalizedConnId, normalizedSessionKey, opts?.includeApprovals === true); let settled = false; const settle = (succeeded) => { if (settled || connections.get(normalizedConnId)?.get(normalizedSessionKey) !== state) return; settled = true; if (succeeded) { if (provisionalRecency >= (state.lastSuccess?.sequence ?? -Infinity)) state.lastSuccess = { sequence: provisionalRecency, includeApprovals: opts?.includeApprovals === true }; } state.inflight -= 1; if (state.inflight > 0) return; const committed = state.lastSuccess?.includeApprovals ?? state.base; if (committed === void 0) { states.delete(normalizedSessionKey); setMessageSubscription(normalizedConnId, normalizedSessionKey, false); setApprovalSubscription(normalizedConnId, normalizedSessionKey, false); } else { states.set(normalizedSessionKey, committed); setMessageSubscription(normalizedConnId, normalizedSessionKey, true); setApprovalSubscription(normalizedConnId, normalizedSessionKey, committed); } if (states.size === 0) connections.delete(normalizedConnId); }; const rollback = (() => settle(false)); rollback.commit = () => settle(true); if (!opts?.provisional) { rollback.commit(); return; } return rollback; }, unsubscribe: (connId, sessionKey) => { const normalizedConnId = normalize(connId); const normalizedSessionKey = normalize(sessionKey); if (!normalizedConnId || !normalizedSessionKey) return; const states = connections.get(normalizedConnId); states?.delete(normalizedSessionKey); if (states?.size === 0) connections.delete(normalizedConnId); setMessageSubscription(normalizedConnId, normalizedSessionKey, false); setApprovalSubscription(normalizedConnId, normalizedSessionKey, false); }, unsubscribeAll: (connId) => { const normalizedConnId = normalize(connId); if (!normalizedConnId) return; const states = connections.get(normalizedConnId); if (!states) return; connections.delete(normalizedConnId); for (const sessionKey of states.keys()) setMessageSubscription(normalizedConnId, sessionKey, false); for (const sessionKey of states.keys()) setApprovalSubscription(normalizedConnId, sessionKey, false); }, get: (sessionKey) => { const normalizedSessionKey = normalize(sessionKey); if (!normalizedSessionKey) return empty; return sessionToConnIds.get(normalizedSessionKey) ?? empty; }, getApprovals: (sessionKey) => { const normalizedSessionKey = normalize(sessionKey); if (!normalizedSessionKey) return empty; return approvalSessionToConnIds.get(normalizedSessionKey) ?? empty; }, onChange: (listener) => { changeListeners.add(listener); return () => changeListeners.delete(listener); } }; } function createToolEventRecipientRegistryForStore(store) { const prune = () => { if (store.runs.size === 0) return; const now = Date.now(); for (const [runId, record] of store.runs) { const entry = record.toolRecipient; if (!entry) continue; if (now >= (entry.finalizedAt ? entry.finalizedAt + TOOL_EVENT_RECIPIENT_FINAL_GRACE_MS : entry.updatedAt + TOOL_EVENT_RECIPIENT_TTL_MS)) { delete record.toolRecipient; store.releaseIfEmpty(runId); } } }; const add = (runId, connId) => { if (!runId || !connId) return; const now = Date.now(); const record = store.getOrCreate(runId); const existing = record.toolRecipient; if (existing) { existing.connIds.add(connId); existing.updatedAt = now; } else record.toolRecipient = { connIds: /* @__PURE__ */ new Set([connId]), updatedAt: now }; prune(); }; const get = (runId) => { const entry = store.runs.get(runId)?.toolRecipient; if (entry) { entry.updatedAt = Date.now(); prune(); } return store.runs.get(runId)?.toolRecipient?.connIds; }; const markFinal = (runId) => { const entry = store.runs.get(runId)?.toolRecipient; if (!entry) return; entry.finalizedAt = Date.now(); prune(); }; return { add, get, markFinal }; } //#endregion export { createSessionMessageSubscriberRegistry as a, projectLiveAssistantBufferedText as c, shouldSuppressAssistantEventForLiveChat as d, createSessionEventSubscriberRegistry as i, resolveAssistantLiveChatInput as l, createChatAbortMarker as n, isChatAbortMarkerCurrent as o, createChatRunState as r, normalizeLiveAssistantBufferedText as s, chatAbortMarkerTimestampMs as t, resolveMergedAssistantText as u };