UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

211 lines (210 loc) • 13 kB
import { t as safeParseJson } from "./json-coercion-pfKbZnCJ.mjs"; import { a as executeSqliteQueryTakeFirstSync, i as executeSqliteQuerySync } from "./kysely-sync-DUWzQvCU.mjs"; import { i as getAdmittedSqliteSchemaFacts } from "./sqlite-schema-facts-BVf0E_0z.mjs"; import { r as runSqliteDeferredTransactionSync } from "./sqlite-transaction-DBRLvCDM.mjs"; import { n as sha256Hex } from "./node-crypto-BG4u3UnX.mjs"; import { v as SessionGoalOperationError } from "./openclaw-state-worker-error-Bi-BnNrV.mjs"; import { b as SESSION_GOAL_OPERATIONS_TABLE, x as ensureSessionGoalOperationsSchema } from "./openclaw-agent-canonical-validation-schema-4dL3LLg2.mjs"; import { c as runOpenClawAgentWriteTransaction, o as openOpenClawAgentDatabase } from "./openclaw-agent-db-DWI9Bgur.mjs"; import { i as toDatabaseOptions, t as getSessionKysely } from "./session-accessor.sqlite-scope-helpers-CfDTAEsL.mjs"; import { a as composeSessionSourceAssertion, l as releaseSessionSourceAuthorities, s as prepareSessionSourceAuthority, t as acceptSessionSourceValidation } from "./session-source-authority-h1UO3u0I.mjs"; import { n as captureOpenClawAgentDatabaseExecution, r as supportsOpenClawAgentDatabaseExecution } from "./openclaw-agent-execution-C2B2fucX.mjs"; import { t as lazyCompile } from "./protocol-validator-BeXu4Ype.mjs"; import { r as SessionsGoalMutationResultSchema } from "./sessions-goal-IBp34JDn.mjs"; import { n as captureLifecycleDatabaseScope, p as runExclusiveSqliteSessionWrite, s as resolveSqliteScope } from "./session-accessor.sqlite-scope-BkgwxatT.mjs"; import { d as readSessionEntryRow } from "./session-accessor.sqlite-entry-read-69RuwSjJ.mjs"; import { o as publishIncognitoSessionEntry, r as captureIncognitoSessionOperation } from "./session-incognito-binding-Dp6RTei2.mjs"; import { d as writeSessionEntry } from "./session-accessor.sqlite-entry-store-CIM2yoGL.mjs"; import { r as mergeSessionEntry } from "./types-BGDhC4_W.mjs"; import { a as buildUpdatedSessionGoalStatus, i as buildUpdatedSessionGoalObjective, r as buildCreatedSessionGoal, t as SessionGoalTransitionError } from "./goals-transitions-CXVW6R9y.mjs"; import { isMainThread } from "node:worker_threads"; //#region src/config/sessions/goals-operations.ts const validateReceipt = /* @__PURE__ */ lazyCompile(SessionsGoalMutationResultSchema); const OPERATION_VALIDITY_MS = 864e5; const OPERATION_FUTURE_SKEW_MS = 3e5; const MAX_SESSION_RECEIPTS = 4096; function assertSessionGoalOperationTime(operation, now) { if (!Number.isSafeInteger(operation.issuedAtMs) || operation.issuedAtMs > now + OPERATION_FUTURE_SKEW_MS) throw new SessionGoalOperationError("invalid", "Goal operation time is invalid; refresh and try again."); if (operation.issuedAtMs + OPERATION_VALIDITY_MS <= now) throw new SessionGoalOperationError("expired", "Goal operation expired; review the current Goal before trying again."); } function operationFingerprint(operation) { return sha256Hex(JSON.stringify([ operation.issuedAtMs, operation.requestFingerprint, operation.action, "goalId" in operation ? operation.goalId : null, "objective" in operation ? operation.objective : null, "tokenBudget" in operation ? operation.tokenBudget : null, "note" in operation ? operation.note : null ])); } /** Read the receipt and its session generation from one admitted snapshot. */ function readSessionGoalOperationInDatabase(database, options) { assertSessionGoalOperationTime(options.operation, Date.now()); const { db } = database; return runSqliteDeferredTransactionSync(db, () => { const schema = getAdmittedSqliteSchemaFacts(db); if (!schema) throw new Error("Goal receipt reads require admitted schema facts"); if (!schema.tables.has("session_goal_operations")) return; const receipt = readSessionGoalOperationReceipt(db, options.sessionKey, options.expectedSessionId, options.operation); if (receipt && readSessionEntryRow(database, options.sessionKey)?.entry.sessionId !== options.expectedSessionId) throw new SessionGoalOperationError("session-rebound", "Session changed after this Goal operation; refresh before trying again."); return receipt; }); } /** The caller has installed the schema before BEGIN; this read participates in its transaction. */ function readSessionGoalOperationReceipt(db, sessionKey, sessionId, operation) { assertSessionGoalOperationTime(operation, Date.now()); const row = executeSqliteQueryTakeFirstSync(db, getSessionKysely(db).selectFrom(SESSION_GOAL_OPERATIONS_TABLE).selectAll().where("session_key", "=", sessionKey).where("operation_id", "=", operation.operationId)); if (!row) return; if (row.request_fingerprint !== operationFingerprint(operation)) throw new SessionGoalOperationError("operation-conflict", "Goal operation ID was already used for a different request."); if (row.session_id !== sessionId) throw new SessionGoalOperationError("session-rebound", "Session changed after this Goal operation; refresh before trying again."); const result = safeParseJson(row.result_json); if (!validateReceipt(result) || result.operationId !== operation.operationId || result.action !== operation.action || result.sessionId !== sessionId || "goalId" in operation && result.goalId !== operation.goalId || result.goal !== void 0 && result.goal.id !== result.goalId) throw new SessionGoalOperationError("receipt-invalid", "Stored Goal operation receipt is invalid; inspect the session before retrying."); return result; } /** Apply the same policy used by text commands to the fresh row inside the commit section. */ function applySessionGoalOperation(entry, operation, now) { try { if (operation.action === "start") return buildCreatedSessionGoal(entry, operation, now); if (!entry.goal || entry.goal.id !== operation.goalId) throw new SessionGoalOperationError("goal-rebound", "Goal changed or was cleared; refresh before trying again."); if (operation.action === "clear") return; if (operation.action === "edit") return buildUpdatedSessionGoalObjective(entry, operation.objective, now); return buildUpdatedSessionGoalStatus(entry, { status: operation.action === "resume" ? "active" : operation.action === "pause" ? "paused" : operation.action === "block" ? "blocked" : "complete", note: operation.note }, now); } catch (error) { if (error instanceof SessionGoalTransitionError) throw new SessionGoalOperationError("invalid", error.message); throw error; } } /** Called only after every Goal/turn/lifecycle write succeeds, in that same transaction. */ function writeSessionGoalOperationReceipt(db, sessionKey, sessionId, operation, goal, runId) { const now = Date.now(); assertSessionGoalOperationTime(operation, now); const kysely = getSessionKysely(db); executeSqliteQuerySync(db, kysely.deleteFrom(SESSION_GOAL_OPERATIONS_TABLE).where("expires_at", "<=", now)); if ((executeSqliteQueryTakeFirstSync(db, kysely.selectFrom("session_goal_operations").select(({ fn }) => fn.countAll().as("count")).where("session_key", "=", sessionKey))?.count ?? 0) >= MAX_SESSION_RECEIPTS) throw new SessionGoalOperationError("capacity", "Too many recent Goal operations; wait for older requests to expire before trying again."); const goalId = goal?.id ?? ("goalId" in operation ? operation.goalId : void 0); if (!goalId) throw new Error("Goal creation did not produce a Goal identity."); const result = { operationId: operation.operationId, action: operation.action, sessionId, goalId, status: runId ? "started" : operation.action === "clear" ? "cleared" : "updated", ...goal ? { goal } : {}, ...runId ? { runId } : {} }; executeSqliteQuerySync(db, kysely.insertInto(SESSION_GOAL_OPERATIONS_TABLE).values({ session_key: sessionKey, operation_id: operation.operationId, session_id: sessionId, request_fingerprint: operationFingerprint(operation), result_json: JSON.stringify(result), expires_at: operation.issuedAtMs + OPERATION_VALIDITY_MS })); return result; } /** Management-only Goal actions do not enter chat or fabricate user turns. */ async function mutateSessionGoal(options) { const incognito = captureIncognitoSessionOperation(options); if (incognito) { const { actor, admissionSignal } = incognito; const input = structuredClone({ sessionKey: resolveSqliteScope({ ...options, agentId: actor.agentId, storePath: actor.path }).sessionKey, expectedSessionId: options.expectedSessionId, operation: options.operation }); return actor.sessions.withSharedState(async () => { const source = await prepareSessionSourceAuthority(options.assertCurrent); try { if (source.nativeSource || source.hasOpaqueCheck || source.checks.some(({ predicate }) => predicate.source.path !== actor.path || predicate.source.agentId !== actor.agentId || predicate.source.databaseIdentity !== actor.identity.incarnation)) throw new Error("Incognito Goal mutations require source authority prepared for their actor"); const authority = { assertCurrent() { incognito.authority.assertCurrent(); (source.assertPreparedCurrent ?? source.assertCurrent)(); } }; admissionSignal?.throwIfAborted(); const committed = await actor.sessions.transcript(authority, { type: "session.goal.mutate", input: { ...input, sessionId: input.expectedSessionId, fence: {}, sources: source.checks.map(({ predicate }) => predicate) } }, void 0, void 0, (result) => { if ("refusedOwnerSource" in result) return; const { previous, sessionEntry } = result; if (sessionEntry) publishIncognitoSessionEntry(actor, input.sessionKey, previous, sessionEntry); }, void 0, (_refused, validation) => { acceptSessionSourceValidation(source, validation); authority.assertCurrent(); }); if ("refusedOwnerSource" in committed) { const refused = committed.refusedOwnerSource; source.checks[refused.index]?.refuse(refused.facts); throw new Error("Goal source refusal omitted its prepared assertion"); } const { previous: _previous, ...result } = committed; return result; } finally { await releaseSessionSourceAuthorities([source]); } }); } const resolved = captureLifecycleDatabaseScope(resolveSqliteScope(options)); const databaseOptions = toDatabaseOptions(resolved); const input = structuredClone({ sessionKey: resolved.sessionKey, expectedSessionId: options.expectedSessionId, operation: options.operation }); return runExclusiveSqliteSessionWrite(resolved, async () => { const native = async (assertCaptured) => { ensureSessionGoalOperationsSchema(openOpenClawAgentDatabase(databaseOptions).db); const { previous: _previous, ...result } = runOpenClawAgentWriteTransaction((database) => { assertCaptured?.(); options.assertCurrent?.(); return mutateSessionGoalInDatabase(database, input); }, databaseOptions, { operationLabel: "session.goal.mutate" }); return result; }; if (!isMainThread || !supportsOpenClawAgentDatabaseExecution(databaseOptions) || options.assertCurrent && !options.assertCurrent.prepareSessionSource) return native(); const execution = captureOpenClawAgentDatabaseExecution(databaseOptions); return (await import("./goals-management-COfVzPOu.mjs").catch(async (error) => { await execution.release(); throw error; })).mutateSessionGoalInWorker({ ...databaseOptions, path: resolved.path }, resolved.agentId, execution, input, composeSessionSourceAssertion([options.assertCurrent], (assertSources) => { assertSources(); assertSessionGoalOperationTime(input.operation, Date.now()); }), native); }, "session.goal.mutate"); } /** Receipt replay and the reducer share the current row and the caller's synchronous transaction. */ function mutateSessionGoalInDatabase(database, input) { const fresh = readSessionEntryRow(database, input.sessionKey); const replay = readSessionGoalOperationReceipt(database.db, input.sessionKey, input.expectedSessionId, input.operation); if (replay && fresh?.entry.sessionId === input.expectedSessionId) return { result: replay, replayed: true }; if (!fresh || fresh.entry.sessionId !== input.expectedSessionId) throw new SessionGoalOperationError("session-rebound", "Session changed; refresh before changing its Goal."); const goal = applySessionGoalOperation(fresh.entry, input.operation, Date.now()); const sessionEntry = writeSessionEntry(database, input.sessionKey, mergeSessionEntry(fresh.entry, { goal }), { canonicalPreviousEntry: fresh.entry }); return { result: writeSessionGoalOperationReceipt(database.db, input.sessionKey, input.expectedSessionId, input.operation, goal), replayed: false, sessionEntry, previous: fresh.entry }; } //#endregion export { readSessionGoalOperationInDatabase as a, mutateSessionGoalInDatabase as i, assertSessionGoalOperationTime as n, readSessionGoalOperationReceipt as o, mutateSessionGoal as r, writeSessionGoalOperationReceipt as s, applySessionGoalOperation as t };