openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
211 lines (210 loc) • 13 kB
JavaScript
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 };