UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

447 lines (446 loc) • 22 kB
import { i as getNodeSqliteKysely, n as executeSqliteQuerySync, r as executeSqliteQueryTakeFirstSync, u as sql } from "./kysely-sync-BMSZn9yz.mjs"; import { r as runSqliteDeferredTransactionSync } from "./sqlite-transaction-DlnLAQiX.mjs"; import { B as ensureContextEngineTurnOutboxSchema } from "./openclaw-agent-db-validation-cache-D5sa3PLF.mjs"; import { s as transcriptEventJsonSql, u as transcriptEventNavigationSql } from "./transcript-payload-Dph8Bvhj.mjs"; import { i as getSessionKysely } from "./session-accessor.sqlite-scope-Dovl36Ht.mjs"; import { n as assertSqliteJsonlReadBudget, t as SqliteJsonlReadBudgetExceededError } from "./sqlite-jsonl-budget-u6dozmIs.mjs"; import { toUSVString } from "node:util"; //#region src/config/sessions/session-accessor.transcript-range.ts function anchorsShareTarget(boundary) { const { admission, terminal } = boundary; return admission.agentId === terminal.agentId && admission.sessionId === terminal.sessionId && admission.sessionKey === terminal.sessionKey && admission.storePath === terminal.storePath && admission.generation === terminal.generation; } function validateAnchorRow(anchor, row) { return Boolean(row && row.generation === anchor.generation && row.seq === anchor.rawSeq && row.parent_id === anchor.effectiveParentId && row.message_position === anchor.activeMessagePosition); } function validateTerminalAncestry(params) { if (params.terminalEntryId === params.admissionEntryId) return "descendant"; if (!(params.maxDepth > 0)) return "too-large"; if (params.terminalParentId === params.admissionEntryId) return "descendant"; if (params.terminalParentId === null || params.terminalParentId === params.terminalEntryId) return "non-descendant"; const db = getSessionKysely(params.database); const depthLimit = Math.ceil(params.maxDepth); const admissionIsSqlText = toUSVString(params.admissionEntryId) === params.admissionEntryId; const terminalIsSqlText = toUSVString(params.terminalEntryId) === params.terminalEntryId; const ancestry = executeSqliteQueryTakeFirstSync(params.database, db.withRecursive("turn_ancestors", (query) => query.selectFrom("transcript_event_identities").select(["event_id", "parent_id"]).where("session_id", "=", params.sessionId).where("event_id", "=", params.terminalParentId).union(query.selectFrom("transcript_event_identities as identity").innerJoin("turn_ancestors as previous", "identity.event_id", "previous.parent_id").select(["identity.event_id", "identity.parent_id"]).where("identity.session_id", "=", params.sessionId).$if(admissionIsSqlText, (ancestors) => ancestors.where("previous.parent_id", "!=", params.admissionEntryId)).$if(terminalIsSqlText, (ancestors) => ancestors.where("identity.event_id", "!=", params.terminalEntryId))).limit(Number.isSafeInteger(depthLimit) ? depthLimit : -1)).selectFrom("turn_ancestors").select((eb) => [eb.fn.countAll().as("depth"), eb.fn.max(eb.case().when(eb.and([eb.val(admissionIsSqlText ? 1 : 0), eb("parent_id", "=", params.admissionEntryId)])).then(1).else(0).end()).as("found")])); if (ancestry.depth >= depthLimit) return "too-large"; return ancestry.found === 1 ? "descendant" : "non-descendant"; } /** Reads a closed turn through the caller's connection, such as an agent database worker's. */ function readClosedTranscriptTurnInDatabase(connection, params) { if (!anchorsShareTarget(params.boundary)) return { kind: "session-rebound" }; const target = params.boundary.admission; return runSqliteDeferredTransactionSync(connection, () => { const db = getSessionKysely(connection); if (!executeSqliteQueryTakeFirstSync(connection, db.selectFrom("session_windows").select(["session_id"]).where("session_id", "=", target.sessionId).where("session_key", "=", target.sessionKey).limit(1))) return { kind: "session-rebound" }; const frontier = executeSqliteQueryTakeFirstSync(connection, db.selectFrom("transcript_events").select("seq").where("session_id", "=", target.sessionId).orderBy("seq", "desc").limit(1))?.seq; const projection = executeSqliteQueryTakeFirstSync(connection, db.selectFrom("session_transcript_index_state").select(["indexed_seq", "needs_rebuild"]).where("session_id", "=", target.sessionId)); if (frontier === void 0 || !projection || projection.needs_rebuild !== 0 || projection.indexed_seq !== frontier) return { kind: "projection-unavailable" }; const readAnchor = (anchor) => executeSqliteQueryTakeFirstSync(connection, 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_rewrite_watermarks as rewrite", (join) => join.onRef("rewrite.session_id", "=", "identity.session_id")).innerJoin("transcript_events as event", (join) => join.onRef("event.session_id", "=", "identity.session_id").onRef("event.seq", "=", "identity.seq")).select([ "identity.seq", "identity.parent_id", "active.message_position", "rewrite.generation", transcriptEventNavigationSql("event").as("event_json") ]).where("identity.session_id", "=", target.sessionId).where("identity.event_id", "=", anchor.entryId).limit(1)); const admissionRow = readAnchor(params.boundary.admission); const terminalRow = readAnchor(params.boundary.terminal); if (!validateAnchorRow(params.boundary.admission, admissionRow) || !validateAnchorRow(params.boundary.terminal, terminalRow)) return { kind: "stale" }; const admissionEvent = JSON.parse(admissionRow.event_json); if (admissionEvent.type !== "message" || admissionEvent.message?.role !== "user") return { kind: "stale" }; const ancestry = validateTerminalAncestry({ database: connection, sessionId: target.sessionId, admissionEntryId: params.boundary.admission.entryId, terminalEntryId: params.boundary.terminal.entryId, terminalParentId: terminalRow.parent_id, maxDepth: params.maxEvents }); if (ancestry !== "descendant") return { kind: ancestry }; const selected = db.selectFrom("session_transcript_active_events as active").innerJoin("transcript_events as event", (join) => join.onRef("event.session_id", "=", "active.session_id").onRef("event.seq", "=", "active.event_seq")).where("active.session_id", "=", target.sessionId).where("active.message_position", "is not", null).where("active.message_position", ">=", params.boundary.admission.activeMessagePosition).where("active.message_position", "<=", params.boundary.terminal.activeMessagePosition).orderBy("active.message_position", "asc"); try { assertSqliteJsonlReadBudget(connection, selected.clearOrderBy().select(["event.event_json", "event.event_utf8_bytes"]).as("events"), params.maxBytes, "Closed transcript turn", { hasExactUtf8Bytes: true, separatorBytes: 0, maxRows: params.maxEvents }); } catch (error) { if (error instanceof SqliteJsonlReadBudgetExceededError) return { kind: "too-large" }; throw error; } return { kind: "ok", messages: executeSqliteQuerySync(connection, selected.select(transcriptEventJsonSql(connection, "event").as("event_json"))).rows.flatMap((row) => { const event = JSON.parse(row.event_json); return event.type === "message" && event.message ? [event.message] : []; }) }; }, { databaseLabel: target.storePath, operationLabel: "session transcript accepted turn read" }); } //#endregion //#region src/agents/harness/context-engine-turn-outbox.ts const RECOVERED_TURN_MAX_EVENTS = 2e4; const RECOVERED_TURN_MAX_BYTES = 8388608; function outboxEnqueueSequence() { return sql`context_engine_turn_outbox.rowid`; } function oldestOutboxEnqueueSequence() { return sql`MIN(context_engine_turn_outbox.rowid)`; } function outboxPayloadRequiresAdvancement() { return sql`json_extract(context_engine_turn_outbox.payload_json, '$.state') IS NOT 'blocked'`; } function isRetryableContextEngineTurnReadFailure(kind) { return kind === "projection-unavailable"; } function outboxDb(database) { ensureContextEngineTurnOutboxSchema(database.db); return getNodeSqliteKysely(database.db); } function assertMatchingOutboxOwner(existing, params, advancementKey) { if (existing.engine_id !== params.engineId || existing.owner_plugin_id !== (params.ownerPluginId ?? null)) throw new Error(`context-engine advancement key collision: ${advancementKey}`); } function writeContextEngineTurnOutboxPayload(params) { const db = outboxDb(params.database); const admission = params.payload.state === "admitted" ? params.payload.admission : params.payload.boundary.admission; const advancementKey = admission.logicalTurnId; const payloadJson = JSON.stringify(params.payload); const existing = executeSqliteQueryTakeFirstSync(params.database.db, db.selectFrom("context_engine_turn_outbox").select([ "engine_id", "owner_plugin_id", "payload_json" ]).where("advancement_key", "=", advancementKey)); if (existing) { assertMatchingOutboxOwner(existing, params, advancementKey); const existingPayload = JSON.parse(existing.payload_json); if (params.payload.state === "accepted" && existingPayload.state === "admitted" && existingPayload.admission.entryId === admission.entryId || (params.payload.state === "blocked" || params.payload.state === "ready") && existingPayload.state === "accepted" && existingPayload.boundary.admission.entryId === admission.entryId && existingPayload.boundary.terminal.entryId === params.payload.boundary.terminal.entryId) { executeSqliteQuerySync(params.database.db, db.updateTable("context_engine_turn_outbox").set({ attempt_count: 0, last_attempt_at: null, last_error: null, payload_json: payloadJson }).where("advancement_key", "=", advancementKey)); return; } if (existing.payload_json !== payloadJson) throw new Error(`context-engine advancement key collision: ${advancementKey}`); return; } executeSqliteQuerySync(params.database.db, db.insertInto("context_engine_turn_outbox").values({ advancement_key: advancementKey, engine_id: params.engineId, owner_plugin_id: params.ownerPluginId ?? null, session_id: admission.sessionId, payload_json: payloadJson, created_at: Date.now(), last_attempt_at: null, last_error: null }).onConflict((conflict) => conflict.column("advancement_key").doNothing())); } function enqueueContextEngineTurnIntent(params) { writeContextEngineTurnOutboxPayload({ ...params, payload: { admission: params.admission, isHeartbeat: params.isHeartbeat, state: "admitted" } }); } function acceptContextEngineTurnIntent(params) { writeContextEngineTurnOutboxPayload({ ...params, payload: { boundary: params.boundary, isHeartbeat: params.isHeartbeat, state: "accepted", runtimeContext: params.runtimeContext } }); } function enqueueContextEngineTurnCommit(params) { writeContextEngineTurnOutboxPayload({ ...params, payload: { ...params.payload, state: "ready" } }); } function blockContextEngineTurnIntent(params) { writeContextEngineTurnOutboxPayload({ ...params, payload: { boundary: params.boundary, failure: params.failure, isHeartbeat: params.isHeartbeat, state: "blocked" } }); } function discardContextEngineTurnIntent(params) { const db = outboxDb(params.database); executeSqliteQuerySync(params.database.db, db.deleteFrom("context_engine_turn_outbox").where("advancement_key", "=", params.admission.logicalTurnId).where("engine_id", "=", params.engineId).where("owner_plugin_id", params.ownerPluginId ? "=" : "is", params.ownerPluginId ?? null)); } /** * Reads an accepted turn's bounded range and publishes it as ready or blocked. * The acceptance commits first in its own transaction, so a failed read or * publication leaves the turn accepted and the next recovery advances it. */ function publishClosedContextEngineTurn(params) { const existing = executeSqliteQueryTakeFirstSync(params.database.db, outboxDb(params.database).selectFrom("context_engine_turn_outbox").select("payload_json").where("advancement_key", "=", params.boundary.admission.logicalTurnId)); if (!existing) return "ok"; if (JSON.parse(existing.payload_json).state !== "accepted") return "ok"; const closedTurn = readClosedTranscriptTurnInDatabase(params.database.db, { boundary: params.boundary, maxEvents: params.maxEvents, maxBytes: params.maxBytes }); if (closedTurn.kind !== "ok") { if (!isRetryableContextEngineTurnReadFailure(closedTurn.kind)) blockContextEngineTurnIntent({ ...params, failure: closedTurn.kind }); return closedTurn.kind; } enqueueContextEngineTurnCommit({ database: params.database, engineId: params.engineId, ownerPluginId: params.ownerPluginId, payload: { boundary: params.boundary, isHeartbeat: params.isHeartbeat, messages: closedTurn.messages, runtimeContext: params.runtimeContext } }); return closedTurn.kind; } function recoverContextEngineTurnOutbox(params) { const db = outboxDb(params.database); const rows = executeSqliteQuerySync(params.database.db, db.selectFrom("context_engine_turn_outbox").select(["advancement_key", "payload_json"]).where("engine_id", "=", params.engineId).where("owner_plugin_id", params.ownerPluginId ? "=" : "is", params.ownerPluginId ?? null).where("session_id", "=", params.sessionId).where(outboxPayloadRequiresAdvancement()).orderBy(outboxEnqueueSequence(), "asc")).rows; for (const row of rows) { const payload = JSON.parse(row.payload_json); if (payload.state === "ready") continue; if (payload.state === "blocked") continue; if (payload.state === "admitted") { discardContextEngineTurnIntent({ admission: payload.admission, database: params.database, engineId: params.engineId, ownerPluginId: params.ownerPluginId }); continue; } const closedTurn = readClosedTranscriptTurnInDatabase(params.database.db, { boundary: payload.boundary, maxEvents: RECOVERED_TURN_MAX_EVENTS, maxBytes: RECOVERED_TURN_MAX_BYTES }); if (closedTurn.kind !== "ok") { if (isRetryableContextEngineTurnReadFailure(closedTurn.kind)) { params.warn(`[context-engine] durable turn recovery remains queued: ${row.advancement_key}: transcript range is ${closedTurn.kind}`); continue; } params.warn(`[context-engine] blocked unrecoverable turn advancement: ${row.advancement_key}: transcript range is ${closedTurn.kind}`); blockContextEngineTurnIntent({ boundary: payload.boundary, database: params.database, engineId: params.engineId, failure: closedTurn.kind, isHeartbeat: payload.isHeartbeat, ownerPluginId: params.ownerPluginId }); continue; } enqueueContextEngineTurnCommit({ database: params.database, engineId: params.engineId, ownerPluginId: params.ownerPluginId, payload: { boundary: payload.boundary, isHeartbeat: payload.isHeartbeat, messages: closedTurn.messages, runtimeContext: payload.runtimeContext } }); } } function listPendingContextEngineTurnSessions(database, filter) { let query = outboxDb(database).selectFrom("context_engine_turn_outbox").select("session_id").select(oldestOutboxEnqueueSequence().as("oldest_enqueue_sequence")).where("engine_id", "=", filter.engineId).where("owner_plugin_id", filter.ownerPluginId ? "=" : "is", filter.ownerPluginId ?? null).where(outboxPayloadRequiresAdvancement()); if (filter.sessionId) query = query.where("session_id", "=", filter.sessionId); return executeSqliteQuerySync(database.db, query.groupBy("session_id").orderBy("oldest_enqueue_sequence", "asc").limit(filter.limit)).rows.map(({ session_id }) => session_id); } function readNextPendingContextEngineTurn(database, filter) { return executeSqliteQueryTakeFirstSync(database.db, outboxDb(database).selectFrom("context_engine_turn_outbox").select([ "advancement_key", "payload_json", "session_id" ]).where("engine_id", "=", filter.engineId).where("owner_plugin_id", filter.ownerPluginId ? "=" : "is", filter.ownerPluginId ?? null).where("session_id", "=", filter.sessionId).where(outboxPayloadRequiresAdvancement()).orderBy(outboxEnqueueSequence(), "asc").limit(1)); } function completeContextEngineTurn(database, advancementKey) { executeSqliteQuerySync(database.db, outboxDb(database).deleteFrom("context_engine_turn_outbox").where("advancement_key", "=", advancementKey)); } function recordContextEngineTurnFailure(database, advancementKey, message, attemptedAt) { executeSqliteQuerySync(database.db, outboxDb(database).updateTable("context_engine_turn_outbox").set((eb) => ({ attempt_count: eb("attempt_count", "+", 1), last_attempt_at: attemptedAt, last_error: message })).where("advancement_key", "=", advancementKey)); } function hasPendingContextEngineTurn(database, filter) { let query = outboxDb(database).selectFrom("context_engine_turn_outbox").select("advancement_key").where("engine_id", "=", filter.engineId).where("owner_plugin_id", filter.ownerPluginId ? "=" : "is", filter.ownerPluginId ?? null).where(outboxPayloadRequiresAdvancement()); if (filter.sessionId) query = query.where("session_id", "=", filter.sessionId); return executeSqliteQueryTakeFirstSync(database.db, query.limit(1)) !== void 0; } /** * Recovers a session's outbox before a run and, when nothing remains to * advance, records the known admission in the same transaction. The common * turn start therefore needs one database round trip. */ function prepareContextEngineTurnRun(params) { const warnings = []; recoverContextEngineTurnOutbox({ ...params, warn: (message) => warnings.push(message) }); const pending = hasPendingContextEngineTurn(params.database, params); if (pending || !params.admission) return { warnings, pending, admitted: false }; enqueueContextEngineTurnIntent({ ...params, admission: params.admission }); return { warnings, pending, admitted: true }; } async function drainContextEngineTurnOutbox(params) { const { store } = params; const filter = { engineId: params.engineId, ownerPluginId: params.ownerPluginId }; if (typeof params.engine.commitTurn !== "function") return { pending: false }; let remaining = Math.max(0, params.limit ?? 16); if (remaining === 0) return { pending: await store.hasPending({ ...filter, sessionId: params.sessionId }) }; let activeSessionIds = await store.listPendingSessions({ ...filter, sessionId: params.sessionId, limit: remaining }); while (remaining > 0 && activeSessionIds.length > 0) { const continuingSessionIds = []; for (const sessionId of activeSessionIds) { if (remaining === 0) break; const row = await store.readNextPending({ ...filter, sessionId }); if (!row) continue; remaining -= 1; if (await commitPendingContextEngineTurn({ ...params, store, row })) continuingSessionIds.push(sessionId); } activeSessionIds = continuingSessionIds; } return { pending: await store.hasPending({ ...filter, sessionId: params.sessionId }) }; } async function commitPendingContextEngineTurn(params) { const { row } = params; try { const payload = JSON.parse(row.payload_json); if (payload.state !== "ready") return false; const commonParams = { advancementKey: row.advancement_key, admission: payload.boundary.admission, terminal: payload.boundary.terminal, messages: payload.messages, sessionId: payload.boundary.admission.sessionId, sessionKey: payload.boundary.admission.sessionKey, sessionTarget: { agentId: payload.boundary.admission.agentId, sessionId: payload.boundary.admission.sessionId, sessionKey: payload.boundary.admission.sessionKey, storePath: payload.boundary.admission.storePath }, isHeartbeat: payload.isHeartbeat, ...payload.runtimeContext ? { runtimeContext: payload.runtimeContext } : {} }; const result = await params.engine.commitTurn?.(commonParams); if (!result) throw new Error("context engine does not implement commitTurn"); if (result.status !== "committed" && result.status !== "duplicate") throw new Error(`invalid commitTurn result status: ${String(result.status)}`); await params.store.complete(row.advancement_key); try { params.onCommitted?.(commonParams); } catch (error) { params.warn(`[context-engine] committed turn notification failed: ${error instanceof Error ? error.message : String(error)}`); } return true; } catch (error) { const message = error instanceof Error ? error.message : String(error); await params.store.recordFailure(row.advancement_key, message, Date.now()); params.warn(`[context-engine] durable turn advancement remains queued: ${row.advancement_key}: ${message}`); return false; } } /** Runs one outbox command's kernel on the borrowed connection. */ function executeContextEngineTurnOutboxCommand(db, command) { const database = { db }; switch (command.type) { case "prepareRun": return prepareContextEngineTurnRun({ ...command.input, database }); case "listPendingSessions": return listPendingContextEngineTurnSessions(database, command.input); case "readNextPending": return readNextPendingContextEngineTurn(database, command.input); case "complete": completeContextEngineTurn(database, command.input.advancementKey); return; case "recordFailure": recordContextEngineTurnFailure(database, command.input.advancementKey, command.input.message, command.input.attemptedAt); return; case "hasPending": return hasPendingContextEngineTurn(database, command.input); case "enqueueIntent": enqueueContextEngineTurnIntent({ ...command.input, database }); return; case "acceptIntent": acceptContextEngineTurnIntent({ ...command.input, database }); return; case "publishClosedTurn": return publishClosedContextEngineTurn({ ...command.input, database }); case "discardIntent": discardContextEngineTurnIntent({ ...command.input, database }); return; } throw new Error("Unknown context-engine turn outbox command"); } //#endregion export { executeContextEngineTurnOutboxCommand as n, drainContextEngineTurnOutbox as t };