UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

493 lines (492 loc) • 21.2 kB
import { a as extractErrorCode, l as readErrorName, r as collectErrorGraphCandidates } from "./error-coercion-W9BtigRm.mjs"; import { c as isRecord } from "./record-coerce-TIsquoVR.mjs"; import { i as resolveRuntimeWorkerUrl } from "./runtime-worker-url-CFqA9Dvi.mjs"; import { r as resolveStateDir } from "./state-dir-CYkBUd_G.mjs"; import { t as formatErrorMessage } from "./errors-BvOkmQM5.mjs"; import { d as readDatabasePathIdentitySync, r as assertExistingDatabaseIdentity } from "./sqlite-worker-identity-BRNRpRyc.mjs"; import { t as createSubsystemLogger } from "./subsystem-4NzR43uz.mjs"; import { n as runtimeProcessEntrypoints } from "./runtime-process-entrypoints-De68tE_W.mjs"; import { c as resolveSessionStorePathCore } from "./paths-Bonddxfk.mjs"; import { a as readOpenClawAgentDatabaseIdentity } from "./openclaw-agent-db-identity-BlmER9-X.mjs"; import { i as isIncognitoOpenClawAgentSqlitePath, l as resolveOpenClawAgentSqlitePath } from "./openclaw-agent-db.paths-BbmZlEP5.mjs"; import { n as withOpenClawAgentDatabaseReadOnly } from "./openclaw-agent-db-readonly-DAqV3tqr.mjs"; import { u as isSqliteWorkerError } from "./sqlite-worker-contract-C51cJHzg.mjs"; import { i as sessionChanges } from "./session-row-changes-Dj2yVWo-.mjs"; import { r as cloneEnvWithPlatformSemantics } from "./config-env-vars-BzUE_dOh.mjs"; import { c as runOpenClawAgentWriteTransaction, d as withOpenClawAgentDatabaseRuntime, i as getOpenClawAgentDatabaseIfOpen, u as withOpenClawAgentDatabaseAsync } from "./openclaw-agent-db-DWI9Bgur.mjs"; import { n as runOpenClawAgentWriteAdmission, t as runOpenClawAgentWorkerWrite } from "./openclaw-agent-write-admission-CXJiE5VB.mjs"; import { t as captureSessionTranscriptStorageEnvironment } from "./transcript-target-binding-DpcuSIFy.mjs"; import { o as resolveSqliteTargetFromSessionStorePath } from "./session-sqlite-target-Dd3VSz82.mjs"; import { i as captureExternalSessionCommitGuard, l as releaseSessionSourceAuthorities, s as prepareSessionSourceAuthority, t as acceptSessionSourceValidation } from "./session-source-authority-h1UO3u0I.mjs"; import { r as getRuntimeConfig } from "./io.runtime-B134MOMz.mjs"; import "./io-BGddrCVX.mjs"; import { n as openOpenClawAgentSqliteWorkerStore } from "./openclaw-agent-worker-store-BE9hhRoh.mjs"; import { r as BoardValidationError } from "./board-layout-6DyK3jbx.mjs"; import { r as captureIncognitoSessionOperation } from "./session-incognito-binding-Dp6RTei2.mjs"; import { h as targetDiscoveryLane } from "./session-transcript-worker-resources-cnBmyJ1M.mjs"; import { c as withSessionHistoryWorkerDatabase } from "./session-transcript-worker-runtime-BdmdicNg.mjs"; import { a as putBoardWidgetInDatabase, c as readBoardWidgetDocument, i as hasBoardSession, l as normalizeBoardWidgetPutParams, n as ensureBoardSchema, r as grantBoardWidgetInDatabase, s as readBoardSnapshotWithHtmlViewMetadata, t as applyBoardOpsToDatabase } from "./sqlite-board-store.kernel-DgDGNaxo.mjs"; import { t as captureSessionEntryNativeMutationWitness } from "./session-entry-read-ordered-D14zdHVq.mjs"; import { i as resolveSessionStoreIdentity } from "./session-store-key-Dvf3CKvN.mjs"; import { AsyncLocalStorage } from "node:async_hooks"; import { randomBytes } from "node:crypto"; import { isPromise } from "node:util/types"; //#region src/boards/sqlite-board-authority.ts const log = createSubsystemLogger("boards/store"); function reportBoardCleanupFailure(error) { try { log.warn(`Board publication completed before cleanup failed: ${formatErrorMessage(error)}`); } catch {} } async function prepareBoardSourceAuthority(assertion, identity) { const source = await prepareSessionSourceAuthority(captureExternalSessionCommitGuard(assertion)); return { ...source, nativeSource: source.nativeSource || source.checks.some(({ predicate }) => typeof predicate.source.databaseIdentity !== "string" || `file:${predicate.source.databaseIdentity}` !== identity.key), assertAdmission(request) { const validation = isRecord(request.facts) && request.facts.boardSourceValidation; if (validation) acceptSessionSourceValidation(source, validation); return request; } }; } //#endregion //#region src/boards/sqlite-board-store.ts function restoreBoardError(error) { if (error instanceof Error && error.name === "BoardValidationError" && "code" in error && (error.code === "conflict" || error.code === "invalid_operation" || error.code === "not_found")) return new BoardValidationError(error.code, error.message); return error; } /** Invalidation never grants retries; transported post-execution failures are plain Errors. */ function hasUnknownBoardWriteOutcome(error) { return collectErrorGraphCandidates(error, (current) => current instanceof AggregateError ? [current.cause] : []).some((current) => isSqliteWorkerError(current, "outcome-unknown") || current instanceof Error && readErrorName(current) === "SqliteWorkerError" && extractErrorCode(current) === "outcome-unknown"); } var SqliteBoardStore = class { constructor(options) { this.options = options; } assertTargetCurrent(target, resolved) { if (resolved.assertCurrent) { resolved.assertCurrent(); return; } const current = this.options.resolveSession(target); if (current.agentId !== resolved.agentId || current.path !== resolved.path || current.sessionKey !== resolved.sessionKey) throw new BoardValidationError("invalid_operation", "board session changed; retry"); } requireExistingSession(resolved, env) { const result = withOpenClawAgentDatabaseReadOnly((database) => hasBoardSession(database, resolved.sessionKey), { agentId: resolved.agentId, ...resolved.path ? { path: resolved.path } : {}, env }); if (!result.found || !result.value) throw new BoardValidationError("not_found", `board session not found: ${resolved.sessionKey}`); } write(target, options, operationLabel, native, worker, actorWrite, prepare) { const resolved = this.options.resolveSession(target); const env = cloneEnvWithPlatformSemantics(this.options.env ?? process.env); env.OPENCLAW_STATE_DIR = resolveStateDir(env); const databaseOptions = { agentId: resolved.agentId, env, path: resolveOpenClawAgentSqlitePath({ ...resolved, env }) }; const assertCurrent = () => { options?.assertCurrent?.(); this.assertTargetCurrent(target, resolved); }; if (resolved.incognito) { const { actor, authority } = resolved.incognito; if (actor.agentId !== resolved.agentId || actor.path !== databaseOptions.path) throw new Error("Board target differs from its captured incognito actor"); const currentAuthority = { assertCurrent() { assertCurrent(); authority.assertCurrent(); actor.assertCurrent(); }, authorize: (stage, facts) => authority.authorize?.(stage, facts) }; currentAuthority.assertCurrent(); const execute = async (writeAuthority) => { try { const committed = await actorWrite(actor, writeAuthority, resolved.sessionKey); sessionChanges.emitBatch(committed.changes); return committed.value; } catch (error) { sessionChanges.emit({ sessionKey: resolved.sessionKey, storePath: actor.path }); throw restoreBoardError(error); } }; return actor.sessions.withSharedState(async () => { if (!prepare) return execute(currentAuthority); const source = await actor.sessions.read(currentAuthority, { sessionKey: resolved.sessionKey }); if (!source.entry) throw new BoardValidationError("not_found", `board session not found: ${resolved.sessionKey}`); await prepare(); source.claim.assertCurrent(); const expected = source.entry; const writeAuthority = { ...currentAuthority, authorize(stage, facts) { if (facts.sharing?.entry?.sessionId !== expected.sessionId || facts.sharing.entry.lifecycleRevision !== expected.lifecycleRevision) throw new BoardValidationError("invalid_operation", "board session changed; retry"); return currentAuthority.authorize?.(stage, facts); } }; return execute(writeAuthority); }).then((result) => { currentAuthority.assertCurrent(); actor.assertReadable(); return result; }); } const incognito = isIncognitoOpenClawAgentSqlitePath(databaseOptions.path, databaseOptions); const assertOpenCurrent = () => { assertCurrent(); if (incognito) this.requireExistingSession({ ...resolved, path: databaseOptions.path }, env); }; assertOpenCurrent(); return runOpenClawAgentWriteAdmission(databaseOptions, async (identity, assertDatabaseCurrent) => { let expectedSession; if (!incognito) { const source = await withSessionHistoryWorkerDatabase(databaseOptions, (reader) => reader.readExactEntries({ env, sessionKeys: [resolved.sessionKey], projection: "exact", snapshotFields: [], expectedIdentity: identity }), targetDiscoveryLane); assertDatabaseCurrent(); assertOpenCurrent(); const entry = source.entries[0]?.entry; if (!entry) throw new BoardValidationError("not_found", `board session not found: ${resolved.sessionKey}`); expectedSession = { sessionId: entry.sessionId, lifecycleRevision: entry.lifecycleRevision }; } const authority = await prepareBoardSourceAuthority(options?.assertCurrent, identity); const nativeSource = incognito || authority.nativeSource; const assertPreparedCurrent = () => { assertDatabaseCurrent(); this.assertTargetCurrent(target, resolved); if (nativeSource) assertOpenCurrent(); else authority.assertCurrent(); }; return (nativeSource ? withOpenClawAgentDatabaseAsync : withOpenClawAgentDatabaseRuntime)(databaseOptions, async (database) => { if (prepare) await prepare(); assertPreparedCurrent(); if (prepare && getOpenClawAgentDatabaseIfOpen(databaseOptions) !== database) throw new BoardValidationError("invalid_operation", "board database closed or changed; retry"); if (nativeSource || typeof readOpenClawAgentDatabaseIdentity(database).identity === "symbol") { ensureBoardSchema(database); return runOpenClawAgentWriteTransaction((current) => { assertPreparedCurrent(); if (expectedSession && !hasBoardSession(current, resolved.sessionKey, expectedSession)) throw new BoardValidationError("invalid_operation", "board session changed; retry"); const value = native(current, resolved.sessionKey); assertPreparedCurrent(); return value; }, databaseOptions, { operationLabel }); } const publication = await openOpenClawAgentSqliteWorkerStore(databaseOptions, database.db, { moduleUrl: resolveRuntimeWorkerUrl(runtimeProcessEntrypoints.boardStore), input: expectedSession ? { agentId: databaseOptions.agentId, sessionKey: resolved.sessionKey, expectedSession, sources: authority.checks.map(({ predicate }) => predicate) } : void 0, assertAdmission: authority.assertAdmission }); let outcome; try { outcome = { ok: true, value: await publication.run(async (scope) => { let committed; try { committed = await worker(scope, resolved.sessionKey); } catch (error) { if (hasUnknownBoardWriteOutcome(error)) sessionChanges.emit({ sessionKey: resolved.sessionKey, storePath: database.path }); throw error; } sessionChanges.emitBatch(committed.changes); return committed.value; }, assertPreparedCurrent) }; } catch (error) { outcome = { ok: false, error: restoreBoardError(error) }; } let cleanup; try { await publication.close(); cleanup = { ok: true, value: void 0 }; } catch (error) { cleanup = { ok: false, error }; } if (!outcome.ok) { if (!cleanup.ok) throw new AggregateError([outcome.error, cleanup.error], "Board publication and cleanup failed", { cause: outcome.error }); throw outcome.error; } if (!cleanup.ok) reportBoardCleanupFailure(cleanup.error); return outcome.value; }, assertPreparedCurrent).then(async (value) => { try { await releaseSessionSourceAuthorities([authority]); } catch (error) { reportBoardCleanupFailure(error); } return value; }, async (error) => { await releaseSessionSourceAuthorities([authority], [error]); throw error; }); }, true); } async getSnapshot(target) { return this.useSnapshot(target, (snapshot) => snapshot); } async getSnapshotWithHtmlViewMetadata(target) { return this.consumeSnapshotWithHtmlViewMetadata(target, (snapshot) => snapshot); } async consumeRead(target, native, worker, actorRead, consume) { const capturedTarget = { ...target }; const resolved = this.options.resolveSession(capturedTarget); const env = captureSessionTranscriptStorageEnvironment(this.options.env ?? process.env); const captured = { agentId: resolved.agentId, sessionKey: resolved.sessionKey, env, path: resolveOpenClawAgentSqlitePath({ ...resolved, env }) }; const runInCallerContext = AsyncLocalStorage.snapshot(); const accept = (value) => { this.assertTargetCurrent(capturedTarget, resolved); const result = runInCallerContext(consume, value, captured.sessionKey); if (isPromise(result)) result.catch(() => {}); return { value: result }; }; if (resolved.incognito) { const { actor, authority } = resolved.incognito; if (actor.agentId !== captured.agentId || actor.path !== captured.path) throw new Error("Board target differs from its captured incognito actor"); const currentAuthority = { assertCurrent: () => { this.assertTargetCurrent(capturedTarget, resolved); authority.assertCurrent(); actor.assertCurrent(); }, authorize: (stage, facts) => authority.authorize?.(stage, facts) }; const result = await actor.sessions.withSharedState(async () => { const runInRetainedContext = AsyncLocalStorage.snapshot(); try { const value = await actorRead(actor, currentAuthority, captured.sessionKey); currentAuthority.assertCurrent(); actor.assertReadable(); return await runInRetainedContext(consume, value, captured.sessionKey); } catch (error) { throw restoreBoardError(error); } }); currentAuthority.assertCurrent(); actor.assertReadable(); return result; } if (isIncognitoOpenClawAgentSqlitePath(captured.path, captured)) return await (await runOpenClawAgentWorkerWrite(captured, async () => { this.assertTargetCurrent(capturedTarget, resolved); const read = withOpenClawAgentDatabaseReadOnly((database) => native(database, captured.sessionKey), captured); return accept(read.found ? read.value : void 0); })).value; const identity = readDatabasePathIdentitySync(captured.path); if (!identity.key.startsWith("file:")) return await (await runOpenClawAgentWorkerWrite(captured, async () => accept(void 0))).value; return await (await withSessionHistoryWorkerDatabase({ ...captured, path: identity.canonicalPath, requestedPaths: [captured.path] }, (reader) => runOpenClawAgentWriteAdmission(captured, async () => { this.assertTargetCurrent(capturedTarget, resolved); const assertNativeCurrent = captureSessionEntryNativeMutationWitness([captured]); const value = await worker(reader, captured.sessionKey, env, identity); assertExistingDatabaseIdentity(captured.path, identity.key, identity.birthtime); reader.assertCurrent(); assertNativeCurrent(); return accept(value); }, true), targetDiscoveryLane).catch((error) => { throw restoreBoardError(error); })).value; } async useSnapshot(target, consume) { return this.consumeSnapshotWithHtmlViewMetadata(target, ({ snapshot }) => consume(snapshot)); } async useWidgetDocument(target, name, consume) { return this.consumeWidgetDocument(target, name, consume); } consumeSnapshotWithHtmlViewMetadata(target, consume) { return this.consumeRead(target, readBoardSnapshotWithHtmlViewMetadata, (reader, sessionKey, env, expectedIdentity) => reader.readBoardSnapshot({ sessionKey, env, expectedIdentity }), (actor, authority, sessionKey) => actor.sessions.sideData(authority, { type: "session.boards.readSnapshot", input: { sessionKey } }), (stored, sessionKey) => consume(stored ?? { snapshot: { sessionKey, revision: 0, tabs: [], widgets: [] }, htmlViewMetadata: /* @__PURE__ */ new Map() })); } async applyOps(target, ops, options) { if (ops.length === 0) return this.getSnapshot(target); const capturedOps = structuredClone(ops); return this.write(target, options, "board.apply-ops", (database, sessionKey) => applyBoardOpsToDatabase(database, sessionKey, capturedOps), (scope, sessionKey) => scope.execute({ type: "boards.applyOps", input: { sessionKey, ops: capturedOps } }), (actor, authority, sessionKey) => actor.sessions.sideData(authority, { type: "session.boards.applyOps", input: { sessionKey, ops: capturedOps } })); } async putWidget(params, options) { const capturedParams = structuredClone(params); const viewGeneration = randomBytes(16).toString("hex"); let preparedParams = capturedParams; const content = capturedParams.content; const resolveInteraction = options?.resolveMcpAppInteraction; const prepare = content.kind === "mcp-app" && content.interactive && resolveInteraction ? async () => { if (!await resolveInteraction()) preparedParams = { ...capturedParams, content: { ...content, interactive: false }, declared: void 0 }; } : void 0; return this.write(params, options, "board.put-widget", (database, sessionKey) => putBoardWidgetInDatabase(database, sessionKey, normalizeBoardWidgetPutParams(preparedParams, sessionKey), viewGeneration), (scope, sessionKey) => scope.execute({ type: "boards.putWidget", input: { sessionKey, params: preparedParams, viewGeneration } }), (actor, authority, sessionKey) => actor.sessions.sideData(authority, { type: "session.boards.putWidget", input: { sessionKey, params: preparedParams, viewGeneration } }), prepare); } async grant(target, name, decision, revision, instanceId, options) { return this.write(target, options, "board.grant-widget", (database, sessionKey) => grantBoardWidgetInDatabase(database, sessionKey, name, decision, revision, instanceId), (scope, sessionKey) => scope.execute({ type: "boards.grant", input: { sessionKey, name, decision, revision, instanceId } }), (actor, authority, sessionKey) => actor.sessions.sideData(authority, { type: "session.boards.grant", input: { sessionKey, name, decision, revision, instanceId } })); } consumeWidgetDocument(target, name, consume, contentKind) { return this.consumeRead(target, (database, sessionKey) => readBoardWidgetDocument(database, sessionKey, name, contentKind), (reader, sessionKey, env, expectedIdentity) => reader.readBoardWidgetDocument({ sessionKey, name, contentKind, env, expectedIdentity }), (actor, authority, sessionKey) => actor.sessions.sideData(authority, { type: "session.boards.readWidgetDocument", input: { sessionKey, name, contentKind } }), consume); } async readWidgetMcpApp(target, name) { return this.consumeWidgetDocument(target, name, (document) => document && "descriptor" in document ? document : void 0, "mcp-app"); } }; //#endregion //#region src/gateway/board-store.ts function captureGatewaySessionStoreScope(sessionKey, explicitAgentId) { const cfg = getRuntimeConfig(); const { agentId, canonicalKey } = resolveSessionStoreIdentity({ cfg, sessionKey, agentId: explicitAgentId }); return { agentId, storePath: resolveSessionStorePathCore(cfg.session?.store, { agentId }), sessionKey: canonicalKey }; } function resolveGatewaySessionDatabase(sessionKey, explicitAgentId) { const { agentId, storePath, sessionKey: canonicalKey } = captureGatewaySessionStoreScope(sessionKey, explicitAgentId); const databaseTarget = resolveSqliteTargetFromSessionStorePath(storePath, { agentId }); return { agentId: databaseTarget.agentId ?? agentId, path: databaseTarget.path, sessionKey: canonicalKey }; } const boardStore = new SqliteBoardStore({ resolveSession: ({ sessionKey, agentId }) => { const scope = captureGatewaySessionStoreScope(sessionKey, agentId); const incognito = captureIncognitoSessionOperation(scope); return { ...incognito ? { agentId: incognito.actor.agentId, path: incognito.actor.path, sessionKey: scope.sessionKey } : resolveGatewaySessionDatabase(sessionKey, agentId), incognito, assertCurrent() { const current = captureGatewaySessionStoreScope(sessionKey, agentId); if (current.agentId !== scope.agentId || current.storePath !== scope.storePath || current.sessionKey !== scope.sessionKey) throw new BoardValidationError("invalid_operation", "board session changed; retry"); } }; } }); //#endregion export { captureGatewaySessionStoreScope as n, boardStore as t };