UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

634 lines (633 loc) • 33.3 kB
import { c as isRecord } from "./record-coerce-TIsquoVR.mjs"; import { a as getNodeSqliteKysely, i as executeSqliteQueryTakeFirstSync, r as executeSqliteQuerySync } from "./kysely-sync-K7ftY-g4.mjs"; import { n as getAdmittedSqliteSchemaFacts } from "./sqlite-schema-facts-CG0Tk84j.mjs"; import { t as extractSqliteTableSchema } from "./sqlite-schema-sql-CKIqAlrf.mjs"; import { i as tableExists } from "./openclaw-state-db-schema-helpers-BlZefrPy.mjs"; import { o as isGatewayExternallySupervised } from "./gateway-supervision-CUdBruRD.mjs"; import { r as createSqliteSchemaEnsurer } from "./openclaw-agent-session-transcript-archive-schema-DeVxJLCy.mjs"; import { i as requestSqliteWorkerOperationAdmission } from "./sqlite-worker-operation-admission-cNGSH5v0.mjs"; import { f as OPENCLAW_STATE_SCHEMA_SQL } from "./openclaw-state-db-legacy-backfills-BxTnDU05.mjs"; import { l as runOpenClawStateWriteTransaction } from "./openclaw-state-db-D1jWDzK3.mjs"; import { i as nodeWorkerTurnMatchesIdentity } from "./node-supervisor-protocol-dVsS52lI.mjs"; import { t as NodeWorkerPreparedWorkspaceKernel } from "./node-worker-prepared-workspace-store.kernel-BMRIueFO.mjs"; //#region src/node-host/node-worker-launch-receipt.ts function isNodeWorkerTerminalState(value) { return value === "completed" || value === "failed" || value === "interrupted" || value === "cancelled"; } function validateNodeWorkerPlanHash(value) { if (!/^[a-f0-9]{64}$/u.test(value)) throw new Error("node worker plan hash must be 64 lowercase hexadecimal characters"); } function validateNodeWorkerProcessIdentity(identity) { if (!Number.isSafeInteger(identity.pid) || identity.pid <= 0 || identity.pid > 2147483647 || !Number.isSafeInteger(identity.startTime) || identity.startTime < 0) throw new Error("node worker process identity must contain a bounded pid and start time"); } function validateNodeWorkerContainerIdentity(identity) { if (identity.engine !== "docker" && identity.engine !== "podman") throw new Error("node worker container engine must be docker or podman"); if (!/^[a-f0-9]{64}$/u.test(identity.containerId)) throw new Error("node worker container id must contain exactly 64 lowercase hexadecimal digits"); if (!/^[a-f0-9]{64}$/u.test(identity.engineTarget)) throw new Error("node worker container engine target must contain exactly 64 lowercase hexadecimal digits"); } function containerIdentity(value) { if (value == null) return null; let parsed; try { parsed = JSON.parse(value); } catch { throw new Error("invalid node worker container identity"); } if (!isRecord(parsed) || Object.keys(parsed).length !== 3 || parsed.engine !== "docker" && parsed.engine !== "podman" || typeof parsed.containerId !== "string" || typeof parsed.engineTarget !== "string") throw new Error("invalid node worker container identity"); const identity = { engine: parsed.engine, containerId: parsed.containerId, engineTarget: parsed.engineTarget }; validateNodeWorkerContainerIdentity(identity); return identity; } function nodeWorkerLaunchReceiptFromRow(row) { if (row.state !== "pending" && row.state !== "running" && !isNodeWorkerTerminalState(row.state)) throw new Error(`invalid node worker launch state ${row.state}`); const container = containerIdentity(row.container_json); const cleanupMode = row.cleanup_mode ?? null; if (row.scope_kind != null && (row.scope_kind !== "linux-subreaper" || cleanupMode !== "owned-anchor")) throw new Error("invalid node worker process scope"); if (cleanupMode !== null && cleanupMode !== "process-group" && cleanupMode !== "owned-anchor") throw new Error("invalid node worker cleanup mode"); return { launchId: row.launch_id, planHash: row.plan_hash, gatewayNamespace: row.gateway_namespace, environmentId: row.environment_id, sessionId: row.session_id, ownerEpoch: row.owner_epoch, placementGeneration: row.placement_generation, runId: row.run_id, state: row.state, supervisor: { pid: row.supervisor_pid, startTime: row.supervisor_start_time }, worker: row.worker_pid === null || row.worker_start_time === null ? null : { pid: row.worker_pid, startTime: row.worker_start_time }, workerCleanupMode: row.scope_kind === "linux-subreaper" ? "linux-subreaper" : cleanupMode, ...row.scope_kind === "linux-subreaper" ? { workerDescendantsReaped: row.descendants_reaped === 1 } : {}, workerLineageSettled: row.lineage_settled === 1, ...container ? { container } : {}, resultJson: row.result_json, errorText: row.error_text, completedAtMs: row.completed_at_ms, createdAtMs: row.created_at_ms, updatedAtMs: row.updated_at_ms }; } //#endregion //#region src/node-host/node-worker-launch-store.kernel.ts const NODE_WORKER_LAUNCH_SCHEMA_END = "\n WHERE completed_at_ms IS NOT NULL;"; const initializedDatabases = /* @__PURE__ */ new WeakSet(); const TERMINAL_RECEIPT_RETENTION_MS$1 = 864e5; const TERMINAL_PRUNE_BATCH_LIMIT$1 = 256; function ensureNodeWorkerLaunchSchema(database, table) { database.exec(extractSqliteTableSchema(OPENCLAW_STATE_SCHEMA_SQL, table, { endMarker: table === "node_worker_launches" ? NODE_WORKER_LAUNCH_SCHEMA_END : void 0 })); } function query$1(database) { return getNodeSqliteKysely(database); } function hasLaunchTable(database, schema, table) { return schema ? schema.tables.has(table) : tableExists(database, table); } function selectLaunchRows(database, schema) { return query$1(database).selectFrom("node_worker_launches").selectAll("node_worker_launches").$if(hasLaunchTable(database, schema, "node_worker_launch_containers"), (selection) => selection.leftJoin("node_worker_launch_containers", "node_worker_launch_containers.launch_id", "node_worker_launches.launch_id").select("node_worker_launch_containers.container_json")).$if(hasLaunchTable(database, schema, "node_worker_launch_cleanup"), (selection) => selection.leftJoin("node_worker_launch_cleanup", "node_worker_launch_cleanup.launch_id", "node_worker_launches.launch_id").select(["node_worker_launch_cleanup.cleanup_mode", "node_worker_launch_cleanup.lineage_settled"])).$if(hasLaunchTable(database, schema, "node_worker_launch_process_scopes"), (selection) => selection.leftJoin("node_worker_launch_process_scopes", "node_worker_launch_process_scopes.launch_id", "node_worker_launches.launch_id").select(["node_worker_launch_process_scopes.scope_kind", "node_worker_launch_process_scopes.descendants_reaped"])); } function readRow$1(database, launchId, schema) { return executeSqliteQueryTakeFirstSync(database, selectLaunchRows(database, schema).where("node_worker_launches.launch_id", "=", launchId)); } function readNonterminalCount(database) { return executeSqliteQueryTakeFirstSync(database, query$1(database).selectFrom("node_worker_launches").select((expression) => expression.fn.countAll().as("count")).where("state", "in", ["pending", "running"]))?.count ?? 0; } function readNonterminalRows(database, schema) { return executeSqliteQuerySync(database, selectLaunchRows(database, schema).where("node_worker_launches.state", "in", ["pending", "running"]).orderBy("node_worker_launches.launch_id", "asc")).rows; } function pruneTerminalRows(params) { let candidates = query$1(params.database).selectFrom("node_worker_launches").select("launch_id").where("state", "in", [ "completed", "failed", "interrupted", "cancelled" ]).where("completed_at_ms", "<=", params.cutoffMs).orderBy("completed_at_ms", "asc").orderBy("launch_id", "asc").limit(params.limit); if (params.excludeLaunchId) candidates = candidates.where("launch_id", "!=", params.excludeLaunchId); const launchIds = executeSqliteQuerySync(params.database, candidates).rows.map((row) => row.launch_id); if (launchIds.length === 0) return 0; if (hasLaunchTable(params.database, params.schema, "node_worker_launch_containers")) executeSqliteQuerySync(params.database, query$1(params.database).deleteFrom("node_worker_launch_containers").where("launch_id", "in", launchIds)); const result = executeSqliteQuerySync(params.database, query$1(params.database).deleteFrom("node_worker_launches").where("launch_id", "in", launchIds).where("state", "in", [ "completed", "failed", "interrupted", "cancelled" ]).where("completed_at_ms", "<=", params.cutoffMs)); return Number(result.numAffectedRows ?? 0n); } /** Read the authoritative physical owner within an already-open journal transaction. */ function readNodeWorkerLaunchReceipt(database, launchId) { const schema = getAdmittedSqliteSchemaFacts(database); if (!hasLaunchTable(database, schema, "node_worker_launches")) return; const row = readRow$1(database, launchId, schema); return row ? nodeWorkerLaunchReceiptFromRow(row) : void 0; } /** Physical extinction closes unfinished turns, never a result already recorded by the worker. */ function settleNodeWorkerActiveTurns(database, owner, schema) { if (owner.state === "pending" || owner.state === "running" || !hasLaunchTable(database, schema ?? getAdmittedSqliteSchemaFacts(database), "node_worker_turns")) return; executeSqliteQuerySync(database, query$1(database).updateTable("node_worker_turns").set((expression) => { const completedAt = expression.fn("max", [ "created_at_ms", "updated_at_ms", expression.val(owner.updatedAtMs) ]); return { state: owner.state === "completed" ? "interrupted" : owner.state, result_json: null, error_text: owner.errorText ?? "node worker stopped before its turn completed", completed_at_ms: completedAt, updated_at_ms: completedAt }; }).where("owner_launch_id", "=", owner.launchId).where("state", "=", "running")); } function validateIdentifier(value, label) { if (!value || value.trim() !== value || value.length > 256 || value.includes("\0")) throw new Error(`${label} must be a bounded non-empty identifier`); } function validateTimestamp(value) { if (!Number.isSafeInteger(value) || value < 0) throw new Error("node worker launch timestamp must be a non-negative safe integer"); } function validatePruneLimit(limit) { if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e3) throw new Error("node worker launch prune limit must be between 1 and 1000"); } function requireMatchingRow(database, identity, schema) { const { launchId, planHash } = identity; const row = readRow$1(database, launchId, schema); if (!row) throw new Error(`node worker launch ${launchId} does not exist`); if (row.plan_hash !== planHash) throw new Error(`node worker launch ${launchId} was replayed with a different plan`); return row; } function rowHasSupervisor(row, identity) { return row.supervisor_pid === identity.pid && row.supervisor_start_time === identity.startTime; } function rowHasWorker(row, identity) { return identity === null ? row.worker_pid === null && row.worker_start_time === null : row.worker_pid === identity.pid && row.worker_start_time === identity.startTime; } function sameObservedOwner(current, observed) { return current.state === observed.state && current.supervisor_pid === observed.supervisor_pid && current.supervisor_start_time === observed.supervisor_start_time && current.worker_pid === observed.worker_pid && current.worker_start_time === observed.worker_start_time; } function rowMatchesImmutableIdentity(row, expected) { return row.launch_id === expected.launchId && row.plan_hash === expected.planHash && row.environment_id === expected.environmentId && row.session_id === expected.sessionId && row.owner_epoch === expected.ownerEpoch && row.placement_generation === expected.placementGeneration && row.run_id === expected.runId; } function finishOwnedRow(database, current, params, nowMs, expected) { if (isNodeWorkerTerminalState(current.state) || !rowHasSupervisor(current, params.supervisor) || !rowHasWorker(current, params.worker)) return; const completedAtMs = Math.max(nowMs, current.created_at_ms, current.updated_at_ms); let update = query$1(database).updateTable("node_worker_launches").set({ state: params.state, result_json: params.state === "completed" ? params.resultJson ?? null : null, error_text: params.state === "completed" ? null : params.errorText ?? null, completed_at_ms: completedAtMs, updated_at_ms: completedAtMs }).where("launch_id", "=", current.launch_id).where("plan_hash", "=", current.plan_hash); if (expected) update = update.where("environment_id", "=", expected.environmentId).where("session_id", "=", expected.sessionId).where("owner_epoch", "=", expected.ownerEpoch).where("placement_generation", "=", expected.placementGeneration).where("run_id", "=", expected.runId); update = update.where("state", "in", ["pending", "running"]).where("supervisor_pid", "=", params.supervisor.pid).where("supervisor_start_time", "=", params.supervisor.startTime); update = params.worker ? update.where("worker_pid", "=", params.worker.pid).where("worker_start_time", "=", params.worker.startTime) : update.where("worker_pid", "is", null).where("worker_start_time", "is", null); const updated = executeSqliteQueryTakeFirstSync(database, update.returning([ "state", "result_json", "error_text", "completed_at_ms", "updated_at_ms" ])); return updated ? { ...current, ...updated } : void 0; } /** Connection-bound launch journal; every operation retains its original write transaction. */ var NodeWorkerLaunchKernel = class { constructor(databaseOptions) { this.databaseOptions = databaseOptions; } write(operationLabel, operation) { let initializedDatabase; const result = runOpenClawStateWriteTransaction(({ db, path }) => { requestSqliteWorkerOperationAdmission({ stage: "transaction", facts: { kind: "node-worker-journal" } }); if (!initializedDatabases.has(db)) { ensureNodeWorkerLaunchSchema(db, "node_worker_launches"); initializedDatabase = db; } return operation(db, getAdmittedSqliteSchemaFacts(db), path); }, this.databaseOptions, { operationLabel }); if (initializedDatabase) initializedDatabases.add(initializedDatabase); return result; } claimObservation(claim, supervisor, capacity, nowMs = Date.now()) { validateIdentifier(claim.launchId, "node worker launch id"); validateNodeWorkerPlanHash(claim.planHash); validateTimestamp(nowMs); validateNodeWorkerProcessIdentity(supervisor); if (!Number.isSafeInteger(capacity) || capacity < 1) throw new Error("node worker capacity must be a positive safe integer"); return this.write("node-worker-launch.claim-inspect", (database, schema) => { const observed = readRow$1(database, claim.launchId, schema); if (!observed) return; return { plan_hash: observed.plan_hash, state: observed.state, supervisor_pid: observed.supervisor_pid, supervisor_start_time: observed.supervisor_start_time, worker_pid: observed.worker_pid, worker_start_time: observed.worker_start_time }; }); } claim(claim, supervisor, capacity, nowMs, observed, observedSupervisorState) { return this.write("node-worker-launch.claim", (database, schema) => { const finalize = (result) => { pruneTerminalRows({ database, schema, cutoffMs: Math.max(0, nowMs - TERMINAL_RECEIPT_RETENTION_MS$1), limit: TERMINAL_PRUNE_BATCH_LIMIT$1, excludeLaunchId: claim.launchId }); return result; }; let current = readRow$1(database, claim.launchId, schema); let action = "replay"; if (!current) { const nonterminalCount = readNonterminalCount(database); if (nonterminalCount >= capacity) return finalize({ action: "at-capacity", nonterminalCount }); executeSqliteQuerySync(database, query$1(database).insertInto("node_worker_launches").values({ launch_id: claim.launchId, plan_hash: claim.planHash, gateway_namespace: claim.gatewayNamespace, environment_id: claim.environmentId, session_id: claim.sessionId, owner_epoch: claim.ownerEpoch, placement_generation: claim.placementGeneration, run_id: claim.runId, state: "pending", supervisor_pid: supervisor.pid, supervisor_start_time: supervisor.startTime, worker_pid: null, worker_start_time: null, result_json: null, error_text: null, completed_at_ms: null, created_at_ms: nowMs, updated_at_ms: nowMs })); current = requireMatchingRow(database, claim, schema); action = "start"; } else if (current.plan_hash !== claim.planHash) throw new Error(`node worker launch ${claim.launchId} was replayed with a different plan`); else if (observed && sameObservedOwner(current, observed) && (observedSupervisorState === "dead" || observedSupervisorState === "reused")) { if (current.state === "pending") { const updatedAtMs = Math.max(nowMs, current.created_at_ms, current.updated_at_ms); executeSqliteQuerySync(database, query$1(database).updateTable("node_worker_launches").set({ supervisor_pid: supervisor.pid, supervisor_start_time: supervisor.startTime, updated_at_ms: updatedAtMs }).where("launch_id", "=", claim.launchId).where("plan_hash", "=", claim.planHash).where("state", "=", "pending").where("supervisor_pid", "=", observed.supervisor_pid).where("supervisor_start_time", "=", observed.supervisor_start_time).where("worker_pid", "is", null).where("worker_start_time", "is", null)); current = requireMatchingRow(database, claim, schema); action = rowHasSupervisor(current, supervisor) ? "start" : "replay"; } else if (current.state === "running") action = "recover"; } return finalize({ action, receipt: nodeWorkerLaunchReceiptFromRow(current), nonterminalCount: readNonterminalCount(database) }); }); } listNonterminal() { return this.write("node-worker-launch.list-nonterminal", (database, schema) => readNonterminalRows(database, schema).map(nodeWorkerLaunchReceiptFromRow)); } nonterminalCount() { return this.write("node-worker-launch.count-nonterminal", readNonterminalCount); } pruneExpiredTerminal(params = {}) { const nowMs = params.nowMs ?? Date.now(); const limit = params.limit ?? TERMINAL_PRUNE_BATCH_LIMIT$1; validateTimestamp(nowMs); validatePruneLimit(limit); return this.write("node-worker-launch.prune-terminal", (database, schema) => pruneTerminalRows({ database, schema, cutoffMs: Math.max(0, nowMs - TERMINAL_RECEIPT_RETENTION_MS$1), limit })); } get(launchId) { validateIdentifier(launchId, "node worker launch id"); return this.write("node-worker-launch.get", (database, schema) => { const row = readRow$1(database, launchId, schema); return row ? nodeWorkerLaunchReceiptFromRow(row) : void 0; }); } getMatching(expected) { validateIdentifier(expected.launchId, "node worker launch id"); validateNodeWorkerPlanHash(expected.planHash); return this.write("node-worker-launch.get-matching", (database, schema) => { const row = readRow$1(database, expected.launchId, schema); return row && rowMatchesImmutableIdentity(row, expected) ? nodeWorkerLaunchReceiptFromRow(row) : void 0; }); } cleanupBinding(params) { return this.write("node-worker-launch.cleanup-binding", (database, schema, databasePath) => { const current = requireMatchingRow(database, params, schema); if (isNodeWorkerTerminalState(current.state) || !rowHasSupervisor(current, params.supervisor)) throw new Error("node worker cleanup binding no longer owns its launch"); return { databasePath, externallySupervised: isGatewayExternallySupervised(this.databaseOptions.env), launchId: params.launchId, planHash: params.planHash, supervisor: { ...params.supervisor } }; }); } finishCancelled(params) { const nowMs = params.nowMs ?? Date.now(); validateTimestamp(nowMs); validateNodeWorkerProcessIdentity(params.supervisor); if (params.worker) validateNodeWorkerProcessIdentity(params.worker); return this.write("node-worker-launch.finish-cancelled", (database, schema) => { const current = readRow$1(database, params.expected.launchId, schema); if (!current || !rowMatchesImmutableIdentity(current, params.expected)) return; const settled = finishOwnedRow(database, current, { supervisor: params.supervisor, worker: params.worker, state: "cancelled", errorText: "node worker launch cancelled" }, nowMs, params.expected) ?? current; if (!rowMatchesImmutableIdentity(settled, params.expected)) return; const receipt = nodeWorkerLaunchReceiptFromRow(settled); settleNodeWorkerActiveTurns(database, receipt, schema); return receipt; }); } markRunning(params) { const nowMs = params.nowMs ?? Date.now(); validateTimestamp(nowMs); validateNodeWorkerProcessIdentity(params.supervisor); validateNodeWorkerProcessIdentity(params.worker); if (params.container) validateNodeWorkerContainerIdentity(params.container); return this.write("node-worker-launch.mark-running", (database, schema) => { const current = requireMatchingRow(database, params, schema); if (isNodeWorkerTerminalState(current.state) || current.state === "running") return nodeWorkerLaunchReceiptFromRow(current); if (!rowHasSupervisor(current, params.supervisor) || !rowHasWorker(current, null)) return nodeWorkerLaunchReceiptFromRow(current); if (params.container) { ensureNodeWorkerLaunchSchema(database, "node_worker_launch_containers"); executeSqliteQuerySync(database, query$1(database).insertInto("node_worker_launch_containers").values({ launch_id: params.launchId, container_json: JSON.stringify({ engine: params.container.engine, containerId: params.container.containerId, engineTarget: params.container.engineTarget }) })); } if (params.cleanupMode !== null) { ensureNodeWorkerLaunchSchema(database, "node_worker_launch_cleanup"); executeSqliteQuerySync(database, query$1(database).insertInto("node_worker_launch_cleanup").values({ launch_id: params.launchId, cleanup_mode: params.cleanupMode === "linux-subreaper" ? "owned-anchor" : params.cleanupMode, lineage_settled: null })); } if (params.cleanupMode === "linux-subreaper") { ensureNodeWorkerLaunchSchema(database, "node_worker_launch_process_scopes"); executeSqliteQuerySync(database, query$1(database).insertInto("node_worker_launch_process_scopes").values({ launch_id: params.launchId, scope_kind: "linux-subreaper", descendants_reaped: null })); } const updatedAtMs = Math.max(nowMs, current.created_at_ms, current.updated_at_ms); executeSqliteQuerySync(database, query$1(database).updateTable("node_worker_launches").set({ state: "running", worker_pid: params.worker.pid, worker_start_time: params.worker.startTime, updated_at_ms: updatedAtMs }).where("launch_id", "=", params.launchId).where("plan_hash", "=", params.planHash).where("state", "=", "pending").where("supervisor_pid", "=", params.supervisor.pid).where("supervisor_start_time", "=", params.supervisor.startTime).where("worker_pid", "is", null).where("worker_start_time", "is", null)); return nodeWorkerLaunchReceiptFromRow(requireMatchingRow(database, params, getAdmittedSqliteSchemaFacts(database))); }); } finish(params) { const nowMs = params.nowMs ?? Date.now(); validateTimestamp(nowMs); validateNodeWorkerProcessIdentity(params.supervisor); if (params.worker) validateNodeWorkerProcessIdentity(params.worker); return this.write("node-worker-launch.finish", (database, schema) => { const current = requireMatchingRow(database, params, schema); const receipt = nodeWorkerLaunchReceiptFromRow(finishOwnedRow(database, current, params, nowMs) ?? current); settleNodeWorkerActiveTurns(database, receipt, schema); return receipt; }); } }; //#endregion //#region src/node-host/node-worker-turn-store.kernel.ts const TERMINAL_RECEIPT_RETENTION_MS = 864e5; const TERMINAL_PRUNE_BATCH_LIMIT = 256; function query(database) { return getNodeSqliteKysely(database); } const turnSchema = createSqliteSchemaEnsurer(() => extractSqliteTableSchema(OPENCLAW_STATE_SCHEMA_SQL, "node_worker_turns", { endMarker: "\n WHERE state = 'running';", errorMessage: "OpenClaw node worker turn schema marker is missing." })); function readRow(database, turnId) { return executeSqliteQueryTakeFirstSync(database, query(database).selectFrom("node_worker_turns").selectAll().where("turn_id", "=", turnId)); } function readReceipt(database, turnId) { let turn = readRow(database, turnId); if (!turn) return; const owner = readNodeWorkerLaunchReceipt(database, turn.owner_launch_id); if (!owner) throw new Error(`node worker turn ${turnId} has no physical owner`); if (turn.state === "running" && owner.state !== "pending" && owner.state !== "running") { settleNodeWorkerActiveTurns(database, owner); turn = readRow(database, turnId); } return turnReceiptFromRow(turn, owner); } function turnReceiptFromRow(turn, owner) { const state = turn.state === "running" && owner.state === "pending" ? "pending" : turn.state; if (state !== "pending" && state !== "running" && !isNodeWorkerTerminalState(state)) throw new Error(`invalid node worker turn state ${state}`); return { ...owner, ownerLaunchId: turn.owner_launch_id, launchId: turn.turn_id, planHash: turn.plan_hash, runId: turn.run_id, state, resultJson: turn.result_json, errorText: turn.error_text, completedAtMs: turn.completed_at_ms, createdAtMs: turn.created_at_ms, updatedAtMs: turn.updated_at_ms }; } function matchesProcess(receipt, supervisor, worker) { return receipt.supervisor.pid === supervisor.pid && receipt.supervisor.startTime === supervisor.startTime && receipt.worker?.pid === worker?.pid && receipt.worker?.startTime === worker?.startTime; } function pruneTerminal(database, nowMs, excludeTurnId) { executeSqliteQuerySync(database, query(database).deleteFrom("node_worker_turns").where("turn_id", "in", query(database).selectFrom("node_worker_turns").select("turn_id").where("completed_at_ms", "<=", Math.max(0, nowMs - TERMINAL_RECEIPT_RETENTION_MS)).where("turn_id", "!=", excludeTurnId).orderBy("completed_at_ms", "asc").orderBy("turn_id", "asc").limit(TERMINAL_PRUNE_BATCH_LIMIT))); } /** Immutable turn outcomes attached to a separately supervised physical worker. */ var NodeWorkerTurnKernel = class { constructor(databaseOptions) { this.databaseOptions = databaseOptions; } write(operationLabel, operation) { let initialized; const result = runOpenClawStateWriteTransaction(({ db }) => { requestSqliteWorkerOperationAdmission({ stage: "transaction", facts: { kind: "node-worker-journal" } }); if (turnSchema.ensure(db)) initialized = db; return operation(db); }, this.databaseOptions, { operationLabel }); if (initialized) turnSchema.recordCommitted(initialized); return result; } claim(params) { const { claim, ownerLaunchId, supervisor } = params; const nowMs = params.nowMs ?? Date.now(); return this.write("node-worker-turn.claim", (database) => { const existing = readReceipt(database, claim.launchId); if (existing) { if (!nodeWorkerTurnMatchesIdentity(existing, claim) || existing.gatewayNamespace !== claim.gatewayNamespace || existing.ownerLaunchId !== ownerLaunchId) throw new Error(`node worker turn ${claim.launchId} was replayed with a different plan or owner`); pruneTerminal(database, nowMs, claim.launchId); return { action: "replay", receipt: existing }; } const owner = readNodeWorkerLaunchReceipt(database, ownerLaunchId); if (!owner || owner.state !== "pending" && owner.state !== "running" || !matchesProcess(owner, supervisor, params.worker ?? null) || owner.gatewayNamespace !== claim.gatewayNamespace || owner.environmentId !== claim.environmentId || owner.sessionId !== claim.sessionId || owner.ownerEpoch !== claim.ownerEpoch || owner.placementGeneration !== claim.placementGeneration || owner.state === "pending" && owner.launchId !== claim.launchId || owner.launchId === claim.launchId && (owner.state !== "pending" || owner.planHash !== claim.planHash || owner.runId !== claim.runId)) throw new Error(`node worker turn ${claim.launchId} does not match its live physical owner`); const turn = executeSqliteQueryTakeFirstSync(database, query(database).insertInto("node_worker_turns").values({ turn_id: claim.launchId, owner_launch_id: ownerLaunchId, plan_hash: claim.planHash, run_id: claim.runId, state: "running", result_json: null, error_text: null, completed_at_ms: null, created_at_ms: nowMs, updated_at_ms: nowMs }).returningAll()); pruneTerminal(database, nowMs, claim.launchId); return { action: "start", receipt: turnReceiptFromRow(turn, owner) }; }); } get(turnId) { return this.write("node-worker-turn.get", (database) => readReceipt(database, turnId)); } finish(params) { return this.write("node-worker-turn.finish", (database) => { const receipt = readReceipt(database, params.expected.launchId); if (!receipt || !nodeWorkerTurnMatchesIdentity(receipt, params.expected) || receipt.ownerLaunchId !== params.ownerLaunchId) return; if (receipt.state !== "pending" && receipt.state !== "running" || !matchesProcess(receipt, params.supervisor, params.worker)) return receipt; const nowMs = params.nowMs ?? Date.now(); const completedAtMs = Math.max(nowMs, receipt.createdAtMs, receipt.updatedAtMs); const turn = executeSqliteQueryTakeFirstSync(database, query(database).updateTable("node_worker_turns").set({ state: params.state, result_json: params.state === "completed" ? params.resultJson ?? null : null, error_text: params.state === "completed" ? null : params.errorText ?? null, completed_at_ms: completedAtMs, updated_at_ms: completedAtMs }).where("turn_id", "=", receipt.launchId).where("state", "=", "running").returningAll()); pruneTerminal(database, nowMs, receipt.launchId); return turnReceiptFromRow(turn, receipt); }); } }; //#endregion //#region src/node-host/node-worker-journal.worker.ts const nodeWorkerJournalOperations = { "nodeWorker.prepared.find": (input, { stateOptions }) => new NodeWorkerPreparedWorkspaceKernel(stateOptions()).find(...input), "nodeWorker.prepared.list": (input, { stateOptions }) => new NodeWorkerPreparedWorkspaceKernel(stateOptions()).list(...input), "nodeWorker.prepared.register": (input, { stateOptions, open }) => new NodeWorkerPreparedWorkspaceKernel({ ...stateOptions(), database: open() }).register(...input), "nodeWorker.prepared.bind": (input, { stateOptions, open }) => new NodeWorkerPreparedWorkspaceKernel({ ...stateOptions(), database: open() }).bind(...input), "nodeWorker.prepared.retire": (input, { stateOptions, open }) => new NodeWorkerPreparedWorkspaceKernel({ ...stateOptions(), database: open() }).retire(...input), "nodeWorker.prepared.completeMutation": (input, { stateOptions, open }) => new NodeWorkerPreparedWorkspaceKernel({ ...stateOptions(), database: open() }).completeMutation(...input), "nodeWorker.launch.claimObservation": (input, { stateOptions, open }) => new NodeWorkerLaunchKernel({ ...stateOptions(), database: open() }).claimObservation(...input), "nodeWorker.launch.claim": (input, { stateOptions, open }) => new NodeWorkerLaunchKernel({ ...stateOptions(), database: open() }).claim(...input), "nodeWorker.launch.listNonterminal": (input, { stateOptions, open }) => new NodeWorkerLaunchKernel({ ...stateOptions(), database: open() }).listNonterminal(...input), "nodeWorker.launch.nonterminalCount": (input, { stateOptions, open }) => new NodeWorkerLaunchKernel({ ...stateOptions(), database: open() }).nonterminalCount(...input), "nodeWorker.launch.pruneExpiredTerminal": (input, { stateOptions, open }) => new NodeWorkerLaunchKernel({ ...stateOptions(), database: open() }).pruneExpiredTerminal(...input), "nodeWorker.launch.get": (input, { stateOptions, open }) => new NodeWorkerLaunchKernel({ ...stateOptions(), database: open() }).get(...input), "nodeWorker.launch.getMatching": (input, { stateOptions, open }) => new NodeWorkerLaunchKernel({ ...stateOptions(), database: open() }).getMatching(...input), "nodeWorker.launch.cleanupBinding": (input, { stateOptions, open }) => new NodeWorkerLaunchKernel({ ...stateOptions(), database: open() }).cleanupBinding(...input), "nodeWorker.launch.finishCancelled": (input, { stateOptions, open }) => new NodeWorkerLaunchKernel({ ...stateOptions(), database: open() }).finishCancelled(...input), "nodeWorker.launch.markRunning": (input, { stateOptions, open }) => new NodeWorkerLaunchKernel({ ...stateOptions(), database: open() }).markRunning(...input), "nodeWorker.launch.finish": (input, { stateOptions, open }) => new NodeWorkerLaunchKernel({ ...stateOptions(), database: open() }).finish(...input), "nodeWorker.turn.claim": (input, { stateOptions, open }) => new NodeWorkerTurnKernel({ ...stateOptions(), database: open() }).claim(...input), "nodeWorker.turn.get": (input, { stateOptions, open }) => new NodeWorkerTurnKernel({ ...stateOptions(), database: open() }).get(...input), "nodeWorker.turn.finish": (input, { stateOptions, open }) => new NodeWorkerTurnKernel({ ...stateOptions(), database: open() }).finish(...input) }; //#endregion export { nodeWorkerJournalOperations };