UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

149 lines (148 loc) 8.29 kB
import { i as executeSqliteQueryTakeFirstSync } from "./kysely-sync-COmh4HWh.js"; import { t as MODEL_CONTEXT_PRIVATE_METADATA_KEYS } from "./model-context-message-Dmngk3hL.js"; import { n as SYNTHETIC_MISSING_TOOL_RESULT_DETAIL_KEY, t as DEFAULT_MISSING_TOOL_RESULT_TEXT } from "./tool-result-pairing-Bccl6DhI.js"; import { i as getSessionKysely } from "./session-accessor.sqlite-scope-2KfMzb44.js"; import { AsyncLocalStorage } from "node:async_hooks"; import { sql } from "kysely"; //#region src/config/sessions/session-model-context-projection.ts /** Exclude storage-only fields in SQLite, before a row's JSON crosses into JavaScript. */ function projectModelContextEventSql(event, omitCheckpoint) { const paths = MODEL_CONTEXT_PRIVATE_METADATA_KEYS.map((key) => `$.message.__openclaw.${key}`); const projected = sql`json_remove(${event}, ${sql.join(paths)})`; const modelEvent = sql`CASE WHEN json_extract(${event}, '$.message.role') = 'toolResult' THEN json_remove(${projected}, '$.message.details') ELSE ${projected} END`; return sql`CASE WHEN ${omitCheckpoint} = 1 THEN json_remove(${modelEvent}, '$.message.providerReplay') ELSE ${modelEvent} END`; } function pickJsonObject(value, keys) { return sql`(SELECT json_group_object(key, CASE type WHEN 'object' THEN json(value) WHEN 'array' THEN json(value) WHEN 'true' THEN json('true') WHEN 'false' THEN json('false') ELSE value END) FROM json_each(${value}) WHERE key IN (${sql.join(keys)}))`; } const TRANSCRIPT_NAVIGATION_KEYS = [ "type", "id", "parentId", "targetId", "appendParentId", "appendMode" ]; /** Cursor resolution needs only tree facts, even when a row has an opaque body. */ function projectTranscriptNavigationSql(event) { return pickJsonObject(event, TRANSCRIPT_NAVIGATION_KEYS); } /** Reset boundaries select ancestry and replay roles without loading message bodies. */ function projectResetBoundaryNavigationSql(event) { const entry = pickJsonObject(event, [ ...TRANSCRIPT_NAVIGATION_KEYS, "timestamp", "firstKeptEntryId" ]); return sql`CASE WHEN json_valid(${event}) THEN CASE WHEN json_type(${event}) = 'object' THEN json_set(${entry}, '$.message', json_object('role', json_extract(${event}, '$.message.role'))) ELSE ${event} END ELSE ${event} END`; } /** Lightweight tree/state records; these never serve as persisted transcript evidence. */ function projectModelContextNavigationSql(event) { const entry = pickJsonObject(event, [ ...TRANSCRIPT_NAVIGATION_KEYS, "timestamp", "version", "cwd", "firstKeptEntryId", "reason", "tokensBefore", "thinkingLevel", "provider", "modelId", "fromId", "customType", "display", "label", "name" ]); const messageFacts = pickJsonObject(sql`json_extract(${event}, '$.message')`, [ "role", "provider", "model", "timestamp", "excludeFromContext", "toolCallId", "toolUseId", "tool_call_id", "tool_use_id", "callId", "call_id", "toolName", "isError", "stopReason", "customType", "display" ]); const calls = sql`(SELECT json_group_array(json_object( 'type', json_extract(value, '$.type'), 'id', json_extract(value, '$.id'), 'name', json_extract(value, '$.name'))) FROM json_each(${event}, '$.message.content') WHERE type = 'object' AND json_extract(value, '$.type') IN ('toolCall', 'toolUse', 'functionCall'))`; const synthetic = sql`COALESCE(json_extract(${event}, ${`$.message.details.${SYNTHETIC_MISSING_TOOL_RESULT_DETAIL_KEY}`}), 0) = 1 OR EXISTS ( SELECT 1 FROM json_each(${event}, '$.message.content') WHERE type = 'object' AND json_extract(value, '$.type') = 'text' AND json_extract(value, '$.text') = ${DEFAULT_MISSING_TOOL_RESULT_TEXT})`; return sql`CASE json_extract(${event}, '$.type') WHEN 'message' THEN json_set(${entry}, '$.message', json_set(${messageFacts}, '$.content', json(${calls}), '$.command', '', '$.output', '', '$.providerReplay', json_object('type', json_extract(${event}, '$.message.providerReplay.type')), '$.details', json_object(${SYNTHETIC_MISSING_TOOL_RESULT_DETAIL_KEY}, json(CASE WHEN (${synthetic}) THEN 'true' ELSE 'false' END)))) WHEN 'custom_message' THEN json_set(${entry}, '$.content', json('[]')) WHEN 'compaction' THEN json_set(${entry}, '$.summary', '') WHEN 'branch_summary' THEN json_set(${entry}, '$.summary', '') ELSE ${entry} END`; } //#endregion //#region src/config/sessions/session-transcript-read-fence.ts const transcriptReadFenceStorage = new AsyncLocalStorage(); var SessionTranscriptReadFenceError = class extends Error { constructor(message) { super(message); this.name = "SessionTranscriptReadFenceError"; } }; function runWithSessionTranscriptReadFence(receipt, run) { return receipt ? transcriptReadFenceStorage.run(receipt, run) : run(); } function withSessionContextAdmission(target, admission, read) { if (admission && (target.agentId !== admission.agentId || target.sessionId !== admission.sessionId || target.sessionKey !== admission.sessionKey)) throw new SessionTranscriptReadFenceError("Current-turn transcript admission belongs to a different transcript target"); return runWithSessionTranscriptReadFence(admission, read); } function resolveSessionTranscriptReadFence(session) { const receipt = transcriptReadFenceStorage.getStore(); return receipt?.agentId === session.agentId && receipt.sessionId === session.sessionId ? receipt : void 0; } function resolveSqliteSessionTranscriptReadFence(params) { const receipt = resolveSessionTranscriptReadFence(params); if (!receipt) return; if (receipt.role !== "user") throw new SessionTranscriptReadFenceError(`Current-turn transcript admission is not a user message: ${receipt.entryId}`); if (params.database.path !== receipt.storePath) throw new SessionTranscriptReadFenceError("Current-turn transcript admission belongs to a different transcript store"); if (params.sessionKey !== void 0 && params.sessionKey !== receipt.sessionKey) throw new SessionTranscriptReadFenceError("Current-turn transcript admission belongs to a different session key"); const db = getSessionKysely(params.database.db); const boundary = executeSqliteQueryTakeFirstSync(params.database.db, db.selectFrom("transcript_event_identities as identity").innerJoin("session_transcript_active_events as active", (join) => join.onRef("active.session_id", "=", "identity.session_id").onRef("active.event_seq", "=", "identity.seq")).innerJoin("transcript_events as event", (join) => join.onRef("event.session_id", "=", "identity.session_id").onRef("event.seq", "=", "identity.seq")).innerJoin("transcript_rewrite_watermarks as rewrite", (join) => join.onRef("rewrite.session_id", "=", "identity.session_id")).select([ "identity.seq", "identity.parent_id", "active.message_position", "rewrite.generation", sql`json_extract(event.event_json, '$.type')`.as("event_type"), sql`json_extract(event.event_json, '$.message.role')`.as("message_role") ]).where("identity.session_id", "=", params.sessionId).where("identity.event_id", "=", receipt.entryId).limit(1)); if (boundary?.message_position === null || boundary?.message_position === void 0) throw new SessionTranscriptReadFenceError(`Current-turn transcript admission is no longer a visible message: ${receipt.entryId}`); if (boundary.generation !== receipt.generation || boundary.seq !== receipt.rawSeq || boundary.parent_id !== receipt.effectiveParentId || boundary.message_position !== receipt.activeMessagePosition) throw new SessionTranscriptReadFenceError(`Current-turn transcript admission identity changed: ${receipt.entryId}`); if (boundary.event_type !== "message" || boundary.message_role !== "user") throw new SessionTranscriptReadFenceError(`Current-turn transcript admission is not a user message: ${receipt.entryId}`); return { admission: receipt, beforeActiveMessagePosition: boundary.message_position, beforeRawSeq: boundary.seq }; } //#endregion export { withSessionContextAdmission as a, projectResetBoundaryNavigationSql as c, runWithSessionTranscriptReadFence as i, projectTranscriptNavigationSql as l, resolveSessionTranscriptReadFence as n, projectModelContextEventSql as o, resolveSqliteSessionTranscriptReadFence as r, projectModelContextNavigationSql as s, SessionTranscriptReadFenceError as t };