UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

244 lines (243 loc) • 10.6 kB
import { a as getNodeSqliteKysely, i as executeSqliteQueryTakeFirstSync, r as executeSqliteQuerySync } from "./kysely-sync-K7ftY-g4.mjs"; import { i as requestSqliteWorkerOperationAdmission } from "./sqlite-worker-operation-admission-cNGSH5v0.mjs"; import { l as runOpenClawStateWriteTransaction } from "./openclaw-state-db-D1jWDzK3.mjs"; import { _ as validateWorkerInferenceTerminalOutcome } from "./worker-inference-B7U4TRc2.mjs"; //#region src/gateway/worker-environments/inference-store.kernel.ts const REQUEST_HASH_PATTERN = /^[a-f0-9]{64}$/u; const DEFAULT_RETENTION = { maxAgeMs: 864e5, maxRows: 256, maxBytes: 67108864 }; function required(value, field) { if (typeof value !== "string" || !value.trim()) throw new Error(`Worker inference turn ${field} must be a non-empty string`); return value.trim(); } function nonNegativeInteger(value, field) { if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error(`Worker inference turn ${field} must be a non-negative integer`); return value; } function normalizeRequestHash(value) { if (typeof value !== "string" || !REQUEST_HASH_PATTERN.test(value)) throw new Error("Worker inference turn request hash must be lowercase SHA-256 hex"); return value; } function normalizeInput(input, nowMs) { return { environmentId: required(input.environmentId, "environment id"), sessionId: required(input.sessionId, "session id"), runEpoch: nonNegativeInteger(input.runEpoch, "run epoch"), runId: required(input.runId, "run id"), turnId: required(input.turnId, "turn id"), requestHash: normalizeRequestHash(input.requestHash), nowMs: nonNegativeInteger(nowMs, "timestamp") }; } function parseTerminalJson(value) { let parsed; try { parsed = JSON.parse(value); } catch (error) { throw new Error("Worker inference cached terminal outcome is invalid", { cause: error }); } if (!validateWorkerInferenceTerminalOutcome(parsed)) throw new Error("Worker inference cached terminal outcome is invalid"); return parsed; } function serializeTerminalOutcome(outcome) { if (!validateWorkerInferenceTerminalOutcome(outcome)) throw new Error("Worker inference terminal outcome is invalid"); const serialized = JSON.stringify(outcome); if (!serialized) throw new Error("Worker inference terminal outcome is not serializable"); return serialized; } const query = (db) => getNodeSqliteKysely(db); function findTurn(db, input) { return executeSqliteQueryTakeFirstSync(db, query(db).selectFrom("worker_inference_turns").selectAll().where("session_id", "=", input.sessionId).where("run_epoch", "=", input.runEpoch).where("run_id", "=", input.runId).where("turn_id", "=", input.turnId)); } function findPendingTurn(db, input) { return executeSqliteQueryTakeFirstSync(db, query(db).selectFrom("worker_inference_turns").selectAll().where("session_id", "=", input.sessionId).where("run_epoch", "=", input.runEpoch).where("run_id", "=", input.runId).where("state", "=", "pending")); } function classifyExistingTurn(row, input) { if (!row) return; if (row.environment_id !== input.environmentId || row.request_hash !== input.requestHash) return { kind: "rejected", reason: "conflict" }; if (row.state === "pending" && row.terminal_json === null) return { kind: "recover" }; if (row.state === "terminal" && row.terminal_json !== null) return { kind: "replay", outcome: parseTerminalJson(row.terminal_json) }; throw new Error("Worker inference turn row has invalid terminal state"); } function insertPendingTurn(db, input) { const turn = { session_id: input.sessionId, run_epoch: input.runEpoch, run_id: input.runId, turn_id: input.turnId, environment_id: input.environmentId, request_hash: input.requestHash, state: "pending", terminal_json: null, created_at_ms: input.nowMs, updated_at_ms: input.nowMs }; executeSqliteQuerySync(db, query(db).insertInto("worker_inference_turns").values(turn)); } function deleteTurn(db, row) { executeSqliteQuerySync(db, query(db).deleteFrom("worker_inference_turns").where("session_id", "=", row.session_id).where("run_epoch", "=", row.run_epoch).where("run_id", "=", row.run_id).where("turn_id", "=", row.turn_id).where("state", "=", "terminal")); } function pruneTerminalTurns(params) { const db = query(params.db); const utf8 = executeSqliteQueryTakeFirstSync(params.db, db.selectFrom("pragma_encoding").select("encoding"))?.encoding === "UTF-8"; const retained = db.selectFrom("worker_inference_turns").select([ "session_id", "run_epoch", "run_id", "turn_id", "updated_at_ms" ]).where("state", "=", "terminal").orderBy("updated_at_ms", "desc").orderBy("session_id", "asc").orderBy("run_epoch", "desc").orderBy("run_id", "asc").orderBy("turn_id", "asc"); const rows = utf8 ? executeSqliteQuerySync(params.db, retained.select((eb) => eb.fn("octet_length", ["terminal_json"]).as("terminal_bytes"))).rows : executeSqliteQuerySync(params.db, retained.select("terminal_json")).rows.map((row) => ({ session_id: row.session_id, run_epoch: row.run_epoch, run_id: row.run_id, turn_id: row.turn_id, updated_at_ms: row.updated_at_ms, terminal_bytes: Buffer.byteLength(row.terminal_json ?? "", "utf8") })); const isPreserved = (row) => params.preserve !== void 0 && row.session_id === params.preserve.sessionId && row.run_epoch === params.preserve.runEpoch && row.run_id === params.preserve.runId && row.turn_id === params.preserve.turnId; rows.sort((left, right) => Number(isPreserved(right)) - Number(isPreserved(left))); const cutoffMs = Math.max(0, params.nowMs - params.policy.maxAgeMs); let retainedRows = 0; let retainedBytes = 0; for (const row of rows) { const terminalBytes = row.terminal_bytes; const preserve = isPreserved(row); const expired = row.updated_at_ms < cutoffMs; const exceedsRows = retainedRows >= params.policy.maxRows; const exceedsBytes = retainedRows > 0 && retainedBytes + terminalBytes > params.policy.maxBytes; if (!preserve && (expired || exceedsRows || exceedsBytes)) { deleteTurn(params.db, row); continue; } retainedRows += 1; retainedBytes += terminalBytes; } } /** The shared-state worker supplies the already-admitted transaction connection. */ function createWorkerInferenceStoreKernel(options) { const now = options.now; const retention = { ...DEFAULT_RETENTION, ...options.retention }; const begin = (rawInput) => { const input = normalizeInput(rawInput, now()); const db = options.db; pruneTerminalTurns({ db, nowMs: input.nowMs, policy: retention }); const existing = classifyExistingTurn(findTurn(db, input), input); if (existing) return existing; if (findPendingTurn(db, input)) return { kind: "rejected", reason: "conflict" }; insertPendingTurn(db, input); return { kind: "claimed" }; }; const complete = (rawInput) => { const input = normalizeInput(rawInput, now()); const terminalJson = serializeTerminalOutcome(rawInput.outcome); const db = options.db; const existing = classifyExistingTurn(findTurn(db, input), input); if (!existing) throw new Error("Worker inference turn must begin before terminal completion"); if (existing.kind === "rejected") throw new Error(`Worker inference terminal completion rejected: ${existing.reason}`); if (existing.kind === "replay") return existing.outcome; if (executeSqliteQuerySync(db, query(db).updateTable("worker_inference_turns").set({ state: "terminal", terminal_json: terminalJson, updated_at_ms: input.nowMs }).where("session_id", "=", input.sessionId).where("run_epoch", "=", input.runEpoch).where("run_id", "=", input.runId).where("turn_id", "=", input.turnId).where("environment_id", "=", input.environmentId).where("request_hash", "=", input.requestHash).where("state", "=", "pending")).numAffectedRows !== 1n) throw new Error("Worker inference turn changed during terminal completion"); pruneTerminalTurns({ db, nowMs: input.nowMs, policy: retention, preserve: input }); return rawInput.outcome; }; const cancelPending = (params) => { const nowMs = nonNegativeInteger(now(), "timestamp"); const terminalJson = serializeTerminalOutcome(params.outcome); const identity = { environmentId: required(params.environmentId, "environment id"), sessionId: required(params.sessionId, "session id"), runEpoch: nonNegativeInteger(params.runEpoch, "run epoch"), runId: required(params.runId, "run id"), turnId: required(params.turnId, "turn id") }; const db = options.db; executeSqliteQuerySync(db, query(db).updateTable("worker_inference_turns").set({ state: "terminal", terminal_json: terminalJson, updated_at_ms: nowMs }).where("session_id", "=", identity.sessionId).where("run_epoch", "=", identity.runEpoch).where("run_id", "=", identity.runId).where("turn_id", "=", identity.turnId).where("environment_id", "=", identity.environmentId).where("state", "=", "pending")); pruneTerminalTurns({ db, nowMs, policy: retention, preserve: identity }); }; const recoverPending = (outcome) => { const nowMs = nonNegativeInteger(now(), "timestamp"); const terminalJson = serializeTerminalOutcome(outcome); const db = options.db; executeSqliteQuerySync(db, query(db).updateTable("worker_inference_turns").set({ state: "terminal", terminal_json: terminalJson, updated_at_ms: nowMs }).where("state", "=", "pending")); pruneTerminalTurns({ db, nowMs, policy: retention }); }; return { begin, cancelPending, complete, recoverPending }; } //#endregion //#region src/gateway/worker-environments/inference-store.worker.ts function operation(type, select) { return (input, { open }) => runOpenClawStateWriteTransaction(({ db }) => { requestSqliteWorkerOperationAdmission({ stage: "transaction", facts: void 0 }); const result = select(createWorkerInferenceStoreKernel({ db, now: () => input.nowMs, retention: input.retention }))(input.input); requestSqliteWorkerOperationAdmission({ stage: "commit", facts: void 0 }); return result; }, { database: open() }, { operationLabel: type }); } const workerInferenceOperations = { "workerInference.begin": operation("workerInference.begin", (store) => store.begin), "workerInference.complete": operation("workerInference.complete", (store) => store.complete), "workerInference.cancelPending": operation("workerInference.cancelPending", (store) => store.cancelPending), "workerInference.recoverPending": operation("workerInference.recoverPending", (store) => store.recoverPending) }; //#endregion export { workerInferenceOperations };