UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

501 lines (500 loc) 22.9 kB
import { t as sanitizeForLog } from "./ansi-DrXAcdMD.js"; import { a as getNodeSqliteKysely, i as executeSqliteQueryTakeFirstSync, r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js"; import { q as ensureContextEngineTurnOutboxSchema } from "./openclaw-agent-db-maintenance-wTIy-jt-.js"; import { p as openOpenClawAgentDatabase } from "./openclaw-agent-db-CWtDoRbC.js"; import { d as resolveLogicalTurnContextEngines } from "./registry-B0bOFDjU.js"; import { N as readClosedTranscriptTurn, R as resolveSessionTranscriptDatabasePath } from "./session-accessor-YsytfDtG.js"; import { a as evaluateContextEngineHostSupport, o as supportsContextEngineDurableTurnAdvancement } from "./host-compat-xESS3bi6.js"; import { t as ensureContextEnginesInitialized } from "./init-Bjn0o-UY.js"; import { sql } from "kysely"; //#region src/agents/harness/context-engine-logical-turn.ts function selectContextEngineForTranscriptHost(params) { const admission = params.recorder?.getAdmissionReceipt(); if (params.recorder && !admission && params.recorder.hasPersisted()) return params.lease.degradeBeforeStart("current-turn transcript admission receipt is unavailable"); return params.lease.selectForHost({ host: params.host, operation: params.operation, requiresDurableCommit: params.recorder !== void 0 }); } async function createContextEngineLogicalTurnLease(params) { ensureContextEnginesInitialized(); const resolution = await resolveLogicalTurnContextEngines(params.config, { agentDir: params.agentDir, workspaceDir: params.workspaceDir }); let state = "unselected"; let effective = resolution.configured; let degradedReason = resolution.configuredFailure; let warned = false; const disposalHolds = /* @__PURE__ */ new Set(); const isBaselineEngineSelection = resolution.configuredFailure === void 0 && resolution.configured.registeredId === resolution.fallback.registeredId; const asEffective = () => Object.freeze({ ...effective, mode: degradedReason ? "legacy-degraded" : "configured", ...degradedReason ? { reason: degradedReason } : {} }); const warnOnce = (reason) => { if (warned) return; warned = true; (params.warn ?? console.warn)(`[context-engine] Context engine "${sanitizeForLog(resolution.configuredId)}" degraded to "${sanitizeForLog(resolution.fallback.registeredId)}" for this logical turn: ${sanitizeForLog(reason)}. The "${sanitizeForLog(resolution.fallback.registeredId)}" engine will handle only this turn; configuration is unchanged, and "${sanitizeForLog(resolution.configuredId)}" will be retried next turn.`); }; const degradeBeforeStart = (reason) => { if (state === "disposed") throw new Error("context-engine logical turn selection is already pinned"); if (isBaselineEngineSelection) { if (state === "unselected") state = "selected"; return asEffective(); } if (state === "started") throw new Error("context-engine logical turn selection is already pinned"); degradedReason ??= reason; effective = resolution.fallback; state = "selected"; warnOnce(degradedReason); return asEffective(); }; const resolveSelectionIssue = (selection) => { const support = evaluateContextEngineHostSupport({ contextEngineInfo: effective.engine.info, operation: selection.operation, host: selection.host }); if (!support.ok) return `host "${selection.host.id}" is missing ${support.missingCapabilities.join(", ")}`; if (isBaselineEngineSelection) return; if (selection.requiresDurableCommit && effective.engine.info.transcriptSemantics?.currentTurnFence !== "before-current-turn-entry-v1") return "current-turn transcript fencing is not declared"; if (selection.requiresDurableCommit && !supportsContextEngineDurableTurnAdvancement(effective.engine)) return "atomic idempotent turn advancement is not declared"; }; if (resolution.configuredFailure) degradeBeforeStart(resolution.configuredFailure); return { get engine() { return effective.engine; }, get effectiveEngine() { return effective.engine; }, get effectiveEngineId() { return effective.registeredId; }, get effectiveEnginePluginId() { return effective.ownerPluginId; }, get degraded() { return degradedReason !== void 0; }, get degradedReason() { return degradedReason; }, selectForHost(selection) { if (state === "disposed") throw new Error("context-engine logical turn lease is already disposed"); if (degradedReason) return asEffective(); const issue = resolveSelectionIssue(selection); if (issue) { if (state === "started") throw new Error(`context-engine logical turn cannot change to incompatible ${selection.host.label}: ${issue}`); return degradeBeforeStart(issue); } if (state === "unselected") state = "selected"; return asEffective(); }, degradeBeforeStart, begin() { if (state === "disposed") throw new Error("context-engine logical turn lease is already disposed"); state = "started"; return asEffective(); }, deferDisposalUntil(promise) { if (state === "disposed") throw new Error("context-engine logical turn lease is already disposed"); disposalHolds.add(promise); promise.finally(() => disposalHolds.delete(promise)).catch(() => {}); }, async dispose() { if (state === "disposed") return; state = "disposed"; const engines = /* @__PURE__ */ new Set([resolution.configured.engine, resolution.fallback.engine]); const disposeEngines = async () => { await Promise.allSettled([...engines].map(async (engine) => await engine.dispose?.())); }; if (disposalHolds.size > 0) { Promise.allSettled(disposalHolds).then(disposeEngines); return; } await disposeEngines(); } }; } //#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" && existingPayload.state === "accepted" && existingPayload.boundary.admission.entryId === admission.entryId && existingPayload.boundary.terminal.entryId === params.payload.boundary.terminal.entryId || 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" } }); } 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)); } 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).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") { params.warn(`[context-engine] durable turn advancement is blocked: ${row.advancement_key}: transcript range is ${payload.failure}`); continue; } if (payload.state === "admitted") { discardContextEngineTurnIntent({ admission: payload.admission, database: params.database, engineId: params.engineId, ownerPluginId: params.ownerPluginId }); continue; } const closedTurn = readClosedTranscriptTurn({ 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 } }); } } async function drainContextEngineTurnOutbox(params) { if (typeof params.engine.commitTurn !== "function") return { pending: false }; let remaining = Math.max(0, params.limit ?? 16); if (remaining === 0) return { pending: hasPendingContextEngineTurn(params) }; const db = outboxDb(params.database); let pendingSessionsQuery = db.selectFrom("context_engine_turn_outbox").select("session_id").select(oldestOutboxEnqueueSequence().as("oldest_enqueue_sequence")).where("engine_id", "=", params.engineId).where("owner_plugin_id", params.ownerPluginId ? "=" : "is", params.ownerPluginId ?? null).where(outboxPayloadRequiresAdvancement()); if (params.sessionId) pendingSessionsQuery = pendingSessionsQuery.where("session_id", "=", params.sessionId); let activeSessionIds = executeSqliteQuerySync(params.database.db, pendingSessionsQuery.groupBy("session_id").orderBy("oldest_enqueue_sequence", "asc").limit(remaining)).rows.map(({ session_id }) => session_id); while (remaining > 0 && activeSessionIds.length > 0) { const continuingSessionIds = []; for (const sessionId of activeSessionIds) { if (remaining === 0) break; const row = executeSqliteQueryTakeFirstSync(params.database.db, db.selectFrom("context_engine_turn_outbox").select([ "advancement_key", "payload_json", "session_id" ]).where("engine_id", "=", params.engineId).where("owner_plugin_id", params.ownerPluginId ? "=" : "is", params.ownerPluginId ?? null).where("session_id", "=", sessionId).where(outboxPayloadRequiresAdvancement()).orderBy(outboxEnqueueSequence(), "asc").limit(1)); if (!row) continue; remaining -= 1; if (await commitPendingContextEngineTurn({ ...params, db, row })) continuingSessionIds.push(sessionId); } activeSessionIds = continuingSessionIds; } return { pending: hasPendingContextEngineTurn(params) }; } function hasPendingContextEngineTurn(params) { let query = outboxDb(params.database).selectFrom("context_engine_turn_outbox").select("advancement_key").where("engine_id", "=", params.engineId).where("owner_plugin_id", params.ownerPluginId ? "=" : "is", params.ownerPluginId ?? null).where(outboxPayloadRequiresAdvancement()); if (params.sessionId) query = query.where("session_id", "=", params.sessionId); return executeSqliteQueryTakeFirstSync(params.database.db, query.limit(1)) !== void 0; } 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 }; 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)}`); executeSqliteQuerySync(params.database.db, params.db.deleteFrom("context_engine_turn_outbox").where("advancement_key", "=", row.advancement_key)); return true; } catch (error) { const message = error instanceof Error ? error.message : String(error); executeSqliteQuerySync(params.database.db, params.db.updateTable("context_engine_turn_outbox").set((eb) => ({ attempt_count: eb("attempt_count", "+", 1), last_attempt_at: Date.now(), last_error: message })).where("advancement_key", "=", row.advancement_key)); params.warn(`[context-engine] durable turn advancement remains queued: ${row.advancement_key}: ${message}`); return false; } } //#endregion //#region src/agents/harness/context-engine-turn-attempt.ts const ACCEPTED_TURN_MAX_EVENTS = 2e4; const ACCEPTED_TURN_MAX_BYTES = 8388608; async function drainPendingContextEngineTurnsBeforeRun(params) { if (!params.admission && !params.recorder || params.lease.degraded || !supportsContextEngineDurableTurnAdvancement(params.lease.engine)) return; const warn = params.warn ?? console.warn; try { const target = params.admission ?? params.sessionTarget; if (!target?.agentId || !target.sessionId || !target.sessionKey || !target.storePath) { params.lease.degradeBeforeStart("durable transcript target is unavailable before context assembly"); return; } const databasePath = params.admission ? params.admission.storePath : resolveSessionTranscriptDatabasePath({ agentId: target.agentId, sessionId: target.sessionId, sessionKey: target.sessionKey, storePath: target.storePath }); const database = openOpenClawAgentDatabase({ agentId: target.agentId, path: databasePath }); recoverContextEngineTurnOutbox({ database, engineId: params.lease.effectiveEngineId, ownerPluginId: params.lease.effectiveEnginePluginId, sessionId: target.sessionId, warn }); if ((await drainContextEngineTurnOutbox({ database, engine: params.lease.engine, engineId: params.lease.effectiveEngineId, ownerPluginId: params.lease.effectiveEnginePluginId, sessionId: target.sessionId, warn })).pending) { params.lease.degradeBeforeStart("pending durable turn advancement could not be completed before the next turn"); return; } const enqueueAdmission = (admission) => { if (admission.agentId !== target.agentId || admission.sessionId !== target.sessionId || admission.sessionKey !== target.sessionKey || admission.storePath !== databasePath) throw new Error("context-engine transcript target changed before provider dispatch"); enqueueContextEngineTurnIntent({ admission, database, engineId: params.lease.effectiveEngineId, isHeartbeat: params.isHeartbeat === true, ownerPluginId: params.lease.effectiveEnginePluginId }); }; if (params.admission) { enqueueAdmission(params.admission); return; } if (!params.recorder?.setAdmissionHandler) { params.lease.degradeBeforeStart("current-turn transcript admission cannot be recorded for durable advancement"); return; } params.recorder.setAdmissionHandler(enqueueAdmission); } catch (error) { warn(`[context-engine] failed to retry pending turn advancement: ${error instanceof Error ? error.message : String(error)}`); params.lease.degradeBeforeStart("pending durable turn advancement could not be checked before the next turn"); } } function discardContextEngineTurnAttemptIntent(params) { const warn = params.warn ?? console.warn; try { const admission = params.facts.boundary.admission; discardContextEngineTurnIntent({ admission, database: openOpenClawAgentDatabase({ agentId: admission.agentId, path: admission.storePath }), engineId: params.lease.effectiveEngineId, ownerPluginId: params.lease.effectiveEnginePluginId }); } catch (error) { warn(`[context-engine] failed to discard unaccepted turn intent: ${error instanceof Error ? error.message : String(error)}`); } } function assertAcceptedTranscriptTarget(facts) { const { admission, terminal } = facts.boundary; if (facts.sessionIdUsed !== admission.sessionId || terminal.agentId !== admission.agentId || terminal.sessionId !== admission.sessionId || terminal.sessionKey !== admission.sessionKey || terminal.storePath !== admission.storePath || facts.sessionKey !== void 0 && facts.sessionKey !== admission.sessionKey || facts.sessionTarget?.agentId !== void 0 && facts.sessionTarget.agentId !== admission.agentId || facts.sessionTarget?.sessionId !== void 0 && facts.sessionTarget.sessionId !== admission.sessionId || facts.sessionTarget?.sessionKey !== void 0 && facts.sessionTarget.sessionKey !== admission.sessionKey) throw new Error("accepted context-engine transcript target changed after admission"); } async function finalizeAcceptedContextEngineTurn(params) { const declaresDurableAdvancement = params.lease.engine.info.transcriptSemantics?.turnAdvancementIdempotency !== void 0; const implementsDurableAdvancement = supportsContextEngineDurableTurnAdvancement(params.lease.engine); if (!declaresDurableAdvancement && !implementsDurableAdvancement) return; const warn = params.warn ?? console.warn; if (params.facts.promptError || params.facts.aborted || params.facts.yieldAborted) { discardContextEngineTurnAttemptIntent({ facts: params.facts, lease: params.lease, warn }); return; } try { assertAcceptedTranscriptTarget(params.facts); if (params.lease.degraded || !declaresDurableAdvancement || !implementsDurableAdvancement) throw new Error("accepted context engine does not support durable turn advancement"); const admission = params.facts.boundary.admission; const database = openOpenClawAgentDatabase({ agentId: admission.agentId, path: admission.storePath }); acceptContextEngineTurnIntent({ boundary: params.facts.boundary, database, engineId: params.lease.effectiveEngineId, isHeartbeat: params.facts.isHeartbeat === true, ownerPluginId: params.lease.effectiveEnginePluginId }); const closedTurn = readClosedTranscriptTurn({ boundary: params.facts.boundary, maxEvents: ACCEPTED_TURN_MAX_EVENTS, maxBytes: ACCEPTED_TURN_MAX_BYTES }); if (closedTurn.kind !== "ok") { if (!isRetryableContextEngineTurnReadFailure(closedTurn.kind)) blockContextEngineTurnIntent({ boundary: params.facts.boundary, database, engineId: params.lease.effectiveEngineId, failure: closedTurn.kind, isHeartbeat: params.facts.isHeartbeat === true, ownerPluginId: params.lease.effectiveEnginePluginId }); throw new Error(`accepted context-engine transcript range is ${closedTurn.kind}`); } enqueueContextEngineTurnCommit({ database, engineId: params.lease.effectiveEngineId, ownerPluginId: params.lease.effectiveEnginePluginId, payload: { boundary: params.facts.boundary, isHeartbeat: params.facts.isHeartbeat === true, messages: closedTurn.messages } }); await drainContextEngineTurnOutbox({ database, engine: params.lease.engine, engineId: params.lease.effectiveEngineId, ownerPluginId: params.lease.effectiveEnginePluginId, warn }); } catch (error) { warn(`[context-engine] skipped accepted turn advancement: ${error instanceof Error ? error.message : String(error)}`); } } //#endregion export { selectContextEngineForTranscriptHost as a, createContextEngineLogicalTurnLease as i, drainPendingContextEngineTurnsBeforeRun as n, finalizeAcceptedContextEngineTurn as r, discardContextEngineTurnAttemptIntent as t };