UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

673 lines (672 loc) 39.8 kB
import { a as getNodeSqliteKysely, i as executeSqliteQueryTakeFirstSync, r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js"; import { E as tableExists, S as OPENCLAW_STATE_SCHEMA_SQL, T as ensureColumn } from "./openclaw-state-db-cache-C7ljO0xP.js"; import { i as openOpenClawStateDatabase, s as runOpenClawStateWriteTransaction } from "./openclaw-state-db-BRTnL-D8.js"; import { i as generateSecureToken } from "./secure-random-Ds4AFLgz.js"; import "./worker-protocol-primitives-Dh40okcI.js"; import { a as projectWorkspaceResultConflict } from "./workspace-conflicts-CQVfTI04.js"; import { a as isCurrentPlacementTurnClaim, c as nextGeneration, d as normalizeIdentity, g as projectWorkerSessionTurnClaim, h as placementTurnOwner, i as assertRecordShape, o as isForceAbandonedWorkerPlacement, p as normalizeWorkerPlacementExecutionMode, u as normalizeEpoch, v as required, y as resolvePlacementTurnEnvironment } from "./placement-record-BOfTzlFO.js"; import { t as boundedWorkerError } from "./worker-error-BQ2GkQ1M.js"; import { a as query, i as getRequired, n as find, o as transitionValues, r as fromRow$1, s as canTransitionWorkerSessionPlacement, t as ensureLocal } from "./placement-row-codec-CuXIyKoc.js"; import { a as createPlacementWorkspaceResultOps, c as isCurrentWorkerWorkspacePendingResultOwner, i as createPlacementWorkspaceReservationOps, l as clearWorkerWorkspaceReconciliation, n as createPlacementTurnClaimOps, o as hasCurrentWorkspaceResultClaim, r as assertSessionWorkspaceUnreserved, s as hasWorkerWorkspacePendingResult, u as createPlacementWorkspaceJournalOps } from "./placement-turn-claims-BI_dYzS0.js"; import { n as clearWorkerTurnToolState, t as assertNoRunningWorkerSessionToolOperations } from "./placement-session-tool-operations-C2F1Fg6L.js"; import { a as registerWorkerTurnClaimClosedHandler, l as signalWorkerTurnClaimClosed, t as attachWorkerTurnExecutionIdentityStore } from "./placement-turn-claim-events-Baq1Tn78.js"; import { isDeepStrictEqual } from "node:util"; import { randomUUID } from "node:crypto"; //#region src/gateway/worker-environments/placement-drain.ts function drainWorkerSessionPlacement(db, input, nowMs) { const sessionId = required(input.sessionId, "session id"); const environmentId = required(input.environmentId, "environment id"); const ownerEpoch = normalizeEpoch(input.ownerEpoch, "active owner epoch"); const current = getRequired(db, sessionId); if (current.state !== "active" || current.generation !== input.expectedGeneration || current.environmentId !== environmentId || current.activeOwnerEpoch !== ownerEpoch) throw new Error(`Cannot drain stale worker placement for session ${sessionId}`); if (!input.allowPendingWorkspaceResult && hasWorkerWorkspacePendingResult(db, sessionId)) throw new Error(`Cannot drain session ${sessionId} with a pending cloud workspace result`); const values = transitionValues(current, "draining", input.workspaceBaseManifestRef === void 0 ? {} : { workspaceBaseManifestRef: input.workspaceBaseManifestRef }, nowMs); const turnClaim = current.turnClaim; if (turnClaim) { values.turn_claim_owner = turnClaim.owner; values.turn_claim_id = turnClaim.claimId; values.turn_claim_run_id = turnClaim.runId; values.turn_claim_generation = turnClaim.generation; values.turn_claim_owner_epoch = turnClaim.ownerEpoch; } assertRecordShape({ state: "draining", executionMode: current.executionMode, environmentId, activeOwnerEpoch: ownerEpoch, workspaceBaseManifestRef: values.workspace_base_manifest_ref, remoteWorkspaceDir: values.remote_workspace_dir, workerBundleHash: values.worker_bundle_hash, lastTranscriptAckCursor: values.last_transcript_ack_cursor, lastLiveEventAckCursor: values.last_live_event_ack_cursor, recoveryError: values.recovery_error, terminalReason: values.terminal_reason, terminalAtMs: values.terminal_at_ms, turnClaim }); if (executeSqliteQuerySync(db, query(db).updateTable("worker_session_placements").set(values).where("session_id", "=", sessionId).where("state", "=", "active").where("transition_generation", "=", current.generation).where("environment_id", "=", environmentId).where("active_owner_epoch", "=", ownerEpoch)).numAffectedRows !== 1n) throw new Error(`Worker session placement ${sessionId} changed during drain`); if (input.workspaceBaseManifestRef !== void 0) clearWorkerWorkspaceReconciliation(db, sessionId, input.workspaceBaseManifestRef); return getRequired(db, sessionId); } //#endregion //#region src/gateway/worker-environments/placement-move-intent.ts const MOVE_SCHEMA_START = "CREATE TABLE IF NOT EXISTS worker_session_placement_moves ("; const MOVE_SCHEMA_END = "\n) STRICT;"; const MOVE_OPERATION_PREFIX = "move:v1:"; const MOVE_MACHINE_CLASS_MAX_LENGTH = 128; const moveQuery = (db) => getNodeSqliteKysely(db); function moveSchemaSql() { const start = OPENCLAW_STATE_SCHEMA_SQL.indexOf(MOVE_SCHEMA_START); const endMarkerStart = OPENCLAW_STATE_SCHEMA_SQL.indexOf(MOVE_SCHEMA_END, start); if (start < 0 || endMarkerStart < start) throw new Error("Worker placement move schema marker is missing"); return OPENCLAW_STATE_SCHEMA_SQL.slice(start, endMarkerStart + 10); } const ensuredMoveSchemaHandles = /* @__PURE__ */ new WeakSet(); function ensureWorkerPlacementMoveSchema(db) { if (ensuredMoveSchemaHandles.has(db)) return; db.exec(moveSchemaSql()); ensureColumn(db, "worker_session_placement_moves", "target_machine_class TEXT"); ensureColumn(db, "worker_session_placement_moves", "abandon_source INTEGER"); ensuredMoveSchemaHandles.add(db); } function ensureExistingWorkerPlacementMoveSchema(db) { if (ensuredMoveSchemaHandles.has(db)) return true; if (!tableExists(db, "worker_session_placement_moves")) return false; ensureWorkerPlacementMoveSchema(db); return true; } function normalizeGeneration(value) { if (!Number.isSafeInteger(value) || value < 0) throw new Error("Worker placement move source generation must be a non-negative integer"); return value; } function boundedIdentifier(value, field, maximumLength = 256) { const normalized = required(value, field); if (normalized.length > maximumLength) throw new Error(`Worker session placement ${field} exceeds ${maximumLength} characters`); return normalized; } function normalizeOperationId(value) { const operationId = required(value, "move operation id"); if (!operationId.startsWith(MOVE_OPERATION_PREFIX) || operationId.length > 128) throw new Error("Worker session placement move operation id is invalid"); return operationId; } function normalizeWorkerPlacementMoveTarget(target) { switch (target.kind) { case "gateway": return { kind: "gateway" }; case "profile": { const machineClass = target.machineClass; return { kind: "profile", profileId: boundedIdentifier(target.profileId, "move profile id"), ...machineClass === void 0 ? {} : { machineClass: boundedIdentifier(machineClass, "move machine class", MOVE_MACHINE_CLASS_MAX_LENGTH) } }; } case "device": return { kind: "device", deviceId: boundedIdentifier(target.deviceId, "move device id") }; } throw new Error("Worker placement move target is invalid"); } function normalizeWorkerPlacementMoveSource(source) { return { generation: normalizeGeneration(source.generation), environmentId: boundedIdentifier(source.environmentId, "move source environment id"), ownerEpoch: normalizeEpoch(source.ownerEpoch, "move source owner epoch") }; } function targetValues(target) { switch (target.kind) { case "gateway": return { target_kind: target.kind, target_id: null, target_machine_class: null }; case "profile": return { target_kind: target.kind, target_id: target.profileId, target_machine_class: target.machineClass ?? null }; case "device": return { target_kind: target.kind, target_id: target.deviceId, target_machine_class: null }; } throw new Error("Worker placement move target is invalid"); } function normalizeAbandonSource(value) { if (value === null) return false; if (value === 1) return true; throw new Error("Invalid worker placement move source abandonment value"); } function abandonSourceValue(abandonSource) { return abandonSource ? 1 : null; } function fromRow(row) { const source = normalizeWorkerPlacementMoveSource({ generation: row.source_generation, environmentId: row.source_environment_id, ownerEpoch: row.source_owner_epoch }); let target; if (row.target_kind !== "profile" && row.target_machine_class !== null) throw new Error(`Invalid worker placement move target: ${row.target_kind}`); if (row.target_kind === "gateway" && row.target_id === null) target = { kind: "gateway" }; else if (row.target_kind === "profile" && row.target_id !== null) target = { kind: "profile", profileId: boundedIdentifier(row.target_id, "move profile id"), ...row.target_machine_class === null ? {} : { machineClass: boundedIdentifier(row.target_machine_class, "move machine class", MOVE_MACHINE_CLASS_MAX_LENGTH) } }; else if (row.target_kind === "device" && row.target_id !== null) target = { kind: "device", deviceId: boundedIdentifier(row.target_id, "move device id") }; else throw new Error(`Invalid worker placement move target: ${row.target_kind}`); const abandonSource = normalizeAbandonSource(row.abandon_source); if (abandonSource && target.kind !== "gateway") throw new Error("Worker placement move source abandonment requires a Gateway target"); return { operationId: normalizeOperationId(row.operation_id), sessionId: required(row.session_id, "move session id"), source, target, abandonSource, lastError: row.last_error, createdAtMs: row.created_at_ms, updatedAtMs: row.updated_at_ms }; } function findMoveRowBySession(db, sessionId) { if (!ensureExistingWorkerPlacementMoveSchema(db)) return; return executeSqliteQueryTakeFirstSync(db, moveQuery(db).selectFrom("worker_session_placement_moves").selectAll().where("session_id", "=", sessionId)); } function findMoveRowByOperation(db, operationId) { if (!ensureExistingWorkerPlacementMoveSchema(db)) return; return executeSqliteQueryTakeFirstSync(db, moveQuery(db).selectFrom("worker_session_placement_moves").selectAll().where("operation_id", "=", operationId)); } function requireExactMove(db, input) { const operationId = normalizeOperationId(input.operationId); const sessionId = required(input.sessionId, "move session id"); const row = findMoveRowByOperation(db, operationId); if (!row || row.session_id !== sessionId) throw new Error(`Session ${sessionId} placement move changed before completion`); return fromRow(row); } function deleteExactMove(db, intent) { const values = targetValues(intent.target); let statement = moveQuery(db).deleteFrom("worker_session_placement_moves").where("operation_id", "=", intent.operationId).where("session_id", "=", intent.sessionId).where("source_generation", "=", intent.source.generation).where("source_environment_id", "=", intent.source.environmentId).where("source_owner_epoch", "=", intent.source.ownerEpoch).where("target_kind", "=", values.target_kind); statement = intent.abandonSource ? statement.where("abandon_source", "=", 1) : statement.where("abandon_source", "is", null); statement = values.target_id === null ? statement.where("target_id", "is", null) : statement.where("target_id", "=", values.target_id); statement = values.target_machine_class === null ? statement.where("target_machine_class", "is", null) : statement.where("target_machine_class", "=", values.target_machine_class); if (executeSqliteQuerySync(db, statement).numAffectedRows !== 1n) throw new Error(`Session ${intent.sessionId} placement move changed before completion`); } function requireExactAttachedEnvironment(db, input) { const row = executeSqliteQueryTakeFirstSync(db, moveQuery(db).selectFrom("worker_environments").select([ "state", "owner_epoch", "profile_id", "attached_session_ids_json" ]).where("environment_id", "=", input.environmentId)); let attachedSessionIds; try { attachedSessionIds = row ? JSON.parse(row.attached_session_ids_json) : void 0; } catch { attachedSessionIds = void 0; } if (!row || row.state !== "attached" || row.owner_epoch !== input.ownerEpoch || input.profileId !== void 0 && row.profile_id !== input.profileId || !Array.isArray(attachedSessionIds) || attachedSessionIds.length !== 1 || attachedSessionIds[0] !== input.sessionId) throw new Error(`Cannot move stale worker environment for session ${input.sessionId}`); } function createPlacementMoveOps(runtime) { const { read, write, now } = runtime; return { getPlacementMove(sessionId) { const row = findMoveRowBySession(read(), required(sessionId, "move session id")); return row ? fromRow(row) : void 0; }, getPlacementMoves(sessionIds) { const normalizedIds = [...new Set(sessionIds.map((sessionId) => required(sessionId, "move session id")))]; const results = /* @__PURE__ */ new Map(); const db = read(); if (!ensureExistingWorkerPlacementMoveSchema(db)) return results; for (let offset = 0; offset < normalizedIds.length; offset += 250) { const chunk = normalizedIds.slice(offset, offset + 250); for (const row of executeSqliteQuerySync(db, moveQuery(db).selectFrom("worker_session_placement_moves").selectAll().where("session_id", "in", chunk)).rows) { const intent = fromRow(row); results.set(intent.sessionId, intent); } } return results; }, listPlacementMoves() { const db = read(); if (!ensureExistingWorkerPlacementMoveSchema(db)) return []; return executeSqliteQuerySync(db, moveQuery(db).selectFrom("worker_session_placement_moves").selectAll().orderBy("created_at_ms").orderBy("session_id")).rows.map(fromRow); }, beginPlacementMove(input) { const sessionId = required(input.sessionId, "move session id"); const source = normalizeWorkerPlacementMoveSource(input.source); const target = normalizeWorkerPlacementMoveTarget(input.target); const abandonSource = input.abandonSource === true; if (abandonSource && target.kind !== "gateway") throw new Error("Worker placement move source abandonment requires a Gateway target"); const operationId = `${MOVE_OPERATION_PREFIX}${generateSecureToken(32)}`; return write((db) => { const existingRow = findMoveRowBySession(db, sessionId); if (existingRow) { const existing = fromRow(existingRow); if (!isDeepStrictEqual(existing.source, source) || !isDeepStrictEqual(existing.target, target) || existing.abandonSource !== abandonSource) throw new Error(`Session ${sessionId} already has a conflicting placement move`); return { intent: existing, placement: getRequired(db, sessionId), joined: true }; } const current = getRequired(db, sessionId); if (current.state !== "active" && !(abandonSource && isForceAbandonedWorkerPlacement(current)) || current.generation !== source.generation || current.environmentId !== source.environmentId || current.activeOwnerEpoch !== source.ownerEpoch) throw new Error(`Cannot move stale worker placement for session ${sessionId}`); if (current.state === "active") requireExactAttachedEnvironment(db, { sessionId, ...source }); ensureWorkerPlacementMoveSchema(db); const timestamp = now(); const row = { operation_id: operationId, session_id: sessionId, source_generation: source.generation, source_environment_id: source.environmentId, source_owner_epoch: source.ownerEpoch, ...targetValues(target), abandon_source: abandonSourceValue(abandonSource), last_error: null, created_at_ms: timestamp, updated_at_ms: timestamp }; executeSqliteQuerySync(db, moveQuery(db).insertInto("worker_session_placement_moves").values(row)); const placement = current.state === "failed" ? current : drainWorkerSessionPlacement(db, { sessionId, ...source, expectedGeneration: source.generation }, timestamp); return { intent: fromRow(row), placement, joined: false }; }); }, recordPlacementMoveError(input) { return write((db) => { const row = findMoveRowByOperation(db, normalizeOperationId(input.operationId)); if (!row || row.session_id !== required(input.sessionId, "move session id")) return false; const intent = fromRow(row); const values = targetValues(intent.target); let statement = moveQuery(db).updateTable("worker_session_placement_moves").set({ last_error: boundedWorkerError(input.error), updated_at_ms: now() }).where("operation_id", "=", intent.operationId).where("session_id", "=", intent.sessionId).where("source_generation", "=", intent.source.generation).where("source_environment_id", "=", intent.source.environmentId).where("source_owner_epoch", "=", intent.source.ownerEpoch).where("target_kind", "=", values.target_kind); statement = intent.abandonSource ? statement.where("abandon_source", "=", 1) : statement.where("abandon_source", "is", null); statement = values.target_id === null ? statement.where("target_id", "is", null) : statement.where("target_id", "=", values.target_id); statement = values.target_machine_class === null ? statement.where("target_machine_class", "is", null) : statement.where("target_machine_class", "=", values.target_machine_class); return executeSqliteQuerySync(db, statement).numAffectedRows === 1n; }); }, cancelPlacementMove(input) { write((db) => { deleteExactMove(db, requireExactMove(db, input)); }); }, completePlacementMoveSourceToLocal(input) { return write((db) => { const intent = requireExactMove(db, input); const current = getRequired(db, intent.sessionId); if (current.state !== "reconciling" || current.generation !== input.expectedGeneration || current.environmentId !== intent.source.environmentId || current.activeOwnerEpoch !== intent.source.ownerEpoch) throw new Error(`Cannot complete stale Gateway placement move for session ${intent.sessionId}`); const values = transitionValues(current, "local", {}, now()); if (executeSqliteQuerySync(db, query(db).updateTable("worker_session_placements").set(values).where("session_id", "=", intent.sessionId).where("state", "=", "reconciling").where("transition_generation", "=", current.generation).where("environment_id", "=", intent.source.environmentId).where("active_owner_epoch", "=", intent.source.ownerEpoch).where("turn_claim_owner", "is", null)).numAffectedRows !== 1n) throw new Error(`Session ${intent.sessionId} changed during Gateway placement move`); if (intent.target.kind === "gateway") deleteExactMove(db, intent); return getRequired(db, intent.sessionId); }); }, completeAbandonedPlacementMoveSourceToLocal(input) { return write((db) => { const intent = requireExactMove(db, input); if (!intent.abandonSource || intent.target.kind !== "gateway") throw new Error(`Session ${intent.sessionId} placement move is not an abandonment`); const current = getRequired(db, intent.sessionId); if (current.state !== "failed" || current.generation !== input.expectedGeneration || current.environmentId !== intent.source.environmentId || current.activeOwnerEpoch !== intent.source.ownerEpoch || current.recoveryError !== input.expectedRecoveryError || current.turnClaim !== null) throw new Error(`Cannot complete stale abandoned placement move for session ${intent.sessionId}`); const values = transitionValues(current, "local", {}, now()); if (executeSqliteQuerySync(db, query(db).updateTable("worker_session_placements").set(values).where("session_id", "=", intent.sessionId).where("state", "=", "failed").where("transition_generation", "=", current.generation).where("environment_id", "=", intent.source.environmentId).where("active_owner_epoch", "=", intent.source.ownerEpoch).where("recovery_error", "=", input.expectedRecoveryError).where("turn_claim_owner", "is", null)).numAffectedRows !== 1n) throw new Error(`Session ${intent.sessionId} changed during abandoned placement move`); deleteExactMove(db, intent); return getRequired(db, intent.sessionId); }); }, completePlacementMoveToWorker(input) { const environmentId = boundedIdentifier(input.environmentId, "move destination environment id"); const ownerEpoch = normalizeEpoch(input.ownerEpoch, "move destination owner epoch"); return write((db) => { const intent = requireExactMove(db, input); if (intent.target.kind === "gateway") throw new Error(`Session ${intent.sessionId} placement move target is not a worker`); const current = getRequired(db, intent.sessionId); if (current.state !== "active" || current.generation !== input.expectedGeneration || current.environmentId !== environmentId || current.activeOwnerEpoch !== ownerEpoch) throw new Error(`Cannot complete stale worker placement move for session ${intent.sessionId}`); const profileId = intent.target.kind === "profile" ? intent.target.profileId : `device:${intent.target.deviceId}`; requireExactAttachedEnvironment(db, { sessionId: intent.sessionId, environmentId, ownerEpoch, profileId }); deleteExactMove(db, intent); return current; }); } }; } //#endregion //#region src/gateway/worker-environments/placement-pending-failure.ts function createPlacementPendingFailureOps(runtime) { const { now, path, write } = runtime; return { failWorkspaceResultAndReleaseTurn(pending, error) { const sessionId = required(pending.sessionId, "session id"); const recoveryError = boundedWorkerError(error); const outcome = write((db) => { const current = getRequired(db, sessionId); if (!isCurrentWorkerWorkspacePendingResultOwner(current, pending)) throw new Error(`Session ${sessionId} workspace result owner changed before failure`); const persisted = current.turnClaim; const releasedClaim = persisted ? { sessionId, claimId: persisted.claimId, runId: persisted.runId, placementGeneration: persisted.generation, owner: placementTurnOwner(current) } : null; const pendingQuery = getNodeSqliteKysely(db); if (!executeSqliteQuerySync(db, pendingQuery.selectFrom("worker_workspace_pending_results").select("session_id").where("session_id", "=", sessionId).where("environment_id", "=", pending.environmentId).where("owner_epoch", "=", pending.ownerEpoch).where("placement_generation", "=", pending.placementGeneration).where("claim_id", "=", pending.claimId).where("run_id", "=", pending.runId)).rows[0]) throw new Error(`Session ${sessionId} workspace result changed before failure`); const terminalAtMs = now(); let transitioning = current; if (transitioning.state === "active") { const values = transitionValues(transitioning, "draining", {}, terminalAtMs); if (persisted) { values.turn_claim_owner = persisted.owner; values.turn_claim_id = persisted.claimId; values.turn_claim_run_id = persisted.runId; values.turn_claim_generation = persisted.generation; values.turn_claim_owner_epoch = persisted.ownerEpoch; } if (executeSqliteQuerySync(db, query(db).updateTable("worker_session_placements").set(values).where("session_id", "=", sessionId).where("state", "=", "active").where("transition_generation", "=", transitioning.generation)).numAffectedRows !== 1n) throw new Error(`Session ${sessionId} workspace result changed during drain`); transitioning = getRequired(db, sessionId); } if (transitioning.state !== "draining") throw new Error(`Session ${sessionId} workspace result did not reach draining`); if (persisted) { assertNoRunningWorkerSessionToolOperations(db, { sessionId, claimId: persisted.claimId }); clearWorkerTurnToolState(db, { sessionId, claimId: persisted.claimId }); } const reconcilingValues = transitionValues(transitioning, "reconciling", {}, terminalAtMs); if (executeSqliteQuerySync(db, query(db).updateTable("worker_session_placements").set(reconcilingValues).where("session_id", "=", sessionId).where("state", "=", "draining").where("transition_generation", "=", transitioning.generation)).numAffectedRows !== 1n) throw new Error(`Session ${sessionId} workspace result changed during reconcile`); transitioning = getRequired(db, sessionId); const failedValues = transitionValues(transitioning, "failed", { recoveryError, terminalReason: recoveryError }, terminalAtMs); if (executeSqliteQuerySync(db, query(db).updateTable("worker_session_placements").set(failedValues).where("session_id", "=", sessionId).where("state", "=", "reconciling").where("transition_generation", "=", transitioning.generation).where("turn_claim_owner", "is", null)).numAffectedRows !== 1n) throw new Error(`Session ${sessionId} workspace result changed during failure`); if (executeSqliteQuerySync(db, pendingQuery.deleteFrom("worker_workspace_pending_results").where("session_id", "=", sessionId).where("environment_id", "=", pending.environmentId).where("owner_epoch", "=", pending.ownerEpoch).where("placement_generation", "=", pending.placementGeneration).where("claim_id", "=", pending.claimId).where("run_id", "=", pending.runId)).numAffectedRows !== 1n) throw new Error(`Session ${sessionId} workspace result changed during failure`); return { record: getRequired(db, sessionId), releasedClaim }; }); if (outcome.releasedClaim) signalWorkerTurnClaimClosed(path, outcome.releasedClaim); return outcome.record; } }; } //#endregion //#region src/gateway/worker-environments/placement-store.ts const RETIRABLE_PLACEMENT_STATES = [ "local", "requested", "reclaimed", "failed" ]; function exactConflictPath(value) { if (typeof value !== "string" || value.length === 0) throw new Error("Worker placement conflict path is required"); return value; } function updateTransition(db, current, to, patch, nowMs) { const values = transitionValues(current, to, patch, nowMs); if (executeSqliteQuerySync(db, query(db).updateTable("worker_session_placements").set(values).where("session_id", "=", current.sessionId).where("state", "=", current.state).where("transition_generation", "=", current.generation).where("turn_claim_owner", "is", null)).numAffectedRows !== 1n) throw new Error(`Worker session placement ${current.sessionId} changed during transition`); return getRequired(db, current.sessionId); } function createWorkerSessionPlacementStore(options = {}) { const path = (options.database ?? openOpenClawStateDatabase()).path; const now = options.now ?? Date.now; const runtime = { path, instanceId: randomUUID(), now, read: () => openOpenClawStateDatabase({ path }).db, write: (operation) => runOpenClawStateWriteTransaction(({ db }) => operation(db), { path }) }; const { read, write } = runtime; const workspaceResultConflicts = /* @__PURE__ */ new Map(); const withWorkspaceResultConflict = (record) => { if (!record) return; const conflict = workspaceResultConflicts.get(record.sessionId); return conflict ? { ...record, workspaceResultConflict: conflict } : record; }; const requireClaimOwner = (claim) => { const db = read(); const current = find(db, required(claim.sessionId, "session id")); if (!current || !isCurrentPlacementTurnClaim(current, claim) && !hasCurrentWorkspaceResultClaim(db, claim)) throw new Error(`Session ${claim.sessionId} workspace result conflict owner changed`); }; const store = { ...createPlacementWorkspaceReservationOps(runtime), ...createPlacementTurnClaimOps(runtime), ...createPlacementPendingFailureOps(runtime), ...createPlacementMoveOps(runtime), ...createPlacementWorkspaceJournalOps(runtime), ...createPlacementWorkspaceResultOps(runtime), registerTurnClaimClosedHandler(handler) { return registerWorkerTurnClaimClosedHandler(path, handler); }, get(sessionId) { return withWorkspaceResultConflict(find(read(), required(sessionId, "session id"))); }, getMany(sessionIds) { const normalizedIds = [...new Set(sessionIds.map((sessionId) => required(sessionId, "session id")))]; const records = /* @__PURE__ */ new Map(); const db = read(); for (let offset = 0; offset < normalizedIds.length; offset += 250) { const chunk = normalizedIds.slice(offset, offset + 250); for (const row of executeSqliteQuerySync(db, query(db).selectFrom("worker_session_placements").selectAll().where("session_id", "in", chunk)).rows) { const record = fromRow$1(row); records.set(record.sessionId, withWorkspaceResultConflict(record)); } } return records; }, retireSessionPlacement(input) { const sessionId = required(input.sessionId, "session id"); if (!RETIRABLE_PLACEMENT_STATES.includes(input.expectedState)) throw new Error(`Cannot retire worker session placement from ${input.expectedState}`); write((db) => { if (executeSqliteQuerySync(db, query(db).deleteFrom("worker_session_placements").where("session_id", "=", sessionId).where("state", "=", input.expectedState).where("transition_generation", "=", input.expectedGeneration).where("turn_claim_owner", "is", null).where("turn_claim_id", "is", null).where("turn_claim_run_id", "is", null).where("turn_claim_generation", "is", null).where("turn_claim_owner_epoch", "is", null)).numAffectedRows !== 1n) throw new Error(`Worker session placement ${sessionId} changed before retirement`); }); workspaceResultConflicts.delete(sessionId); }, recordWorkspaceResultConflict(claim, conflict) { requireClaimOwner(claim); if (!conflict) { workspaceResultConflicts.delete(claim.sessionId); return; } const paths = conflict.paths.map(exactConflictPath); const stagedResultRef = required(conflict.stagedResultRef, "staged result ref"); if (paths.length === 0 || !/^refs\/openclaw\/worker-results\/[A-Za-z0-9-]+$/u.test(stagedResultRef)) throw new Error("Cloud workspace result conflict projection is invalid"); workspaceResultConflicts.set(claim.sessionId, projectWorkspaceResultConflict(paths, stagedResultRef, conflict.totalCount)); }, startDispatch(input) { const identity = normalizeIdentity(input); const executionMode = normalizeWorkerPlacementExecutionMode(input.executionMode); return write((db) => { const current = ensureLocal(db, identity, now()); assertSessionWorkspaceUnreserved(db, identity.sessionId); if (current.state !== "local" && current.state !== "reclaimed" && current.state !== "failed") throw new Error(`Cannot dispatch session ${identity.sessionId} from placement ${current.state}`); const updatedAtMs = now(); if (executeSqliteQuerySync(db, query(db).updateTable("worker_session_placements").set({ state: "requested", execution_mode: executionMode, environment_id: null, transition_generation: nextGeneration(current.generation), active_owner_epoch: null, workspace_base_manifest_ref: null, remote_workspace_dir: null, worker_bundle_hash: null, last_transcript_ack_cursor: null, last_live_event_ack_cursor: null, recovery_error: null, terminal_reason: null, terminal_at_ms: null, updated_at_ms: updatedAtMs, state_changed_at_ms: updatedAtMs }).where("session_id", "=", current.sessionId).where("state", "=", current.state).where("transition_generation", "=", current.generation)).numAffectedRows !== 1n) throw new Error(`Session ${identity.sessionId} placement changed during dispatch barrier`); return getRequired(db, identity.sessionId); }); }, transition(input) { if (!canTransitionWorkerSessionPlacement(input.from, input.to)) throw new Error(`Illegal worker session placement transition: ${input.from} -> ${input.to}`); if (input.from === "draining" && input.to === "reconciling") throw new Error("Use startReconcile after fencing the drained worker environment"); if (input.to === "failed") throw new Error("Use fail to record terminal worker placement diagnostics"); const sessionId = required(input.sessionId, "session id"); return write((db) => { const current = getRequired(db, sessionId); if (current.state !== input.from || current.generation !== input.expectedGeneration) throw new Error(`Worker session placement ${sessionId} changed: expected ${input.from}@${input.expectedGeneration}, found ${current.state}@${current.generation}`); if (current.turnClaim) throw new Error(`Cannot transition session ${sessionId} during an active turn`); return updateTransition(db, current, input.to, input.patch ?? {}, now()); }); }, startDrain(input) { return write((db) => drainWorkerSessionPlacement(db, input, now())); }, startWorkspaceResultDrain(claim) { return write((db) => { const current = getRequired(db, required(claim.sessionId, "session id")); const ownsWorkspaceResult = hasCurrentWorkspaceResultClaim(db, claim); const owner = resolvePlacementTurnEnvironment(current, claim) ?? (ownsWorkspaceResult && current.state === "active" && current.environmentId && current.activeOwnerEpoch !== null ? { environmentId: current.environmentId, ownerEpoch: current.activeOwnerEpoch } : void 0); if (current.state !== "active" || !owner || !ownsWorkspaceResult) throw new Error(`Cannot drain stale workspace result for session ${claim.sessionId}`); return drainWorkerSessionPlacement(db, { sessionId: current.sessionId, environmentId: owner.environmentId, ownerEpoch: owner.ownerEpoch, expectedGeneration: current.generation, allowPendingWorkspaceResult: true }, now()); }); }, startReconcile(input) { const sessionId = required(input.sessionId, "session id"); const environmentId = required(input.environmentId, "environment id"); const ownerEpoch = normalizeEpoch(input.ownerEpoch, "active owner epoch"); const outcome = write((db) => { const current = getRequired(db, sessionId); if (current.state !== "draining" || current.generation !== input.expectedGeneration || current.environmentId !== environmentId || current.activeOwnerEpoch !== ownerEpoch) throw new Error(`Cannot reconcile stale worker placement for session ${sessionId}`); if (hasWorkerWorkspacePendingResult(db, sessionId)) throw new Error(`Cannot reconcile session ${sessionId} with a pending cloud workspace result`); const claim = current.turnClaim; if (claim?.owner === "local" && input.forceLocalClaim !== true) throw new Error(`Cannot reconcile session ${sessionId} while its local turn is active`); if (claim) { assertNoRunningWorkerSessionToolOperations(db, { sessionId, claimId: claim.claimId }); clearWorkerTurnToolState(db, { sessionId, claimId: claim.claimId }); } const values = transitionValues(current, "reconciling", {}, now()); const update = query(db).updateTable("worker_session_placements").set(values).where("session_id", "=", sessionId).where("state", "=", "draining").where("transition_generation", "=", current.generation).where("environment_id", "=", environmentId).where("active_owner_epoch", "=", ownerEpoch); const guardedUpdate = claim ? update.where("turn_claim_owner", "=", claim.owner).where("turn_claim_id", "=", claim.claimId).where("turn_claim_run_id", "=", claim.runId).where("turn_claim_generation", "=", claim.generation).where("turn_claim_owner_epoch", claim.owner === "worker" ? "=" : "is", claim.ownerEpoch) : update.where("turn_claim_owner", "is", null); if (executeSqliteQuerySync(db, guardedUpdate).numAffectedRows !== 1n) throw new Error(`Worker session placement ${sessionId} changed during reconcile`); return { record: getRequired(db, sessionId), releasedClaim: claim ? { sessionId, claimId: claim.claimId, runId: claim.runId, placementGeneration: claim.generation, owner: placementTurnOwner(current) } : void 0 }; }); if (outcome.releasedClaim) signalWorkerTurnClaimClosed(path, outcome.releasedClaim); return outcome.record; }, validateWorkerOwner(input) { const current = find(read(), required(input.sessionId, "session id")); return current?.state === "active" && current.environmentId === required(input.environmentId, "environment id") && current.activeOwnerEpoch === normalizeEpoch(input.ownerEpoch, "active owner epoch"); }, fail(input) { const sessionId = required(input.sessionId, "session id"); const recoveryError = boundedWorkerError(input.recoveryError); const outcome = write((db) => { const current = getRequired(db, sessionId); if (input.expectedGeneration !== void 0 && current.generation !== input.expectedGeneration) throw new Error(`Worker session placement ${sessionId} changed before failure`); if (current.state === "failed") { if (executeSqliteQuerySync(db, query(db).updateTable("worker_session_placements").set({ recovery_error: recoveryError, updated_at_ms: now() }).where("session_id", "=", sessionId).where("state", "=", "failed").where("transition_generation", "=", current.generation)).numAffectedRows !== 1n) throw new Error(`Worker session placement ${sessionId} changed during failure update`); return { record: getRequired(db, sessionId), releasedClaim: void 0 }; } if (!canTransitionWorkerSessionPlacement(current.state, "failed")) throw new Error(`Cannot fail worker session placement from ${current.state}`); const localClaim = current.turnClaim?.owner === "local" ? current.turnClaim : null; const updatedAtMs = now(); if (executeSqliteQuerySync(db, query(db).updateTable("worker_session_placements").set({ state: "failed", transition_generation: nextGeneration(current.generation), recovery_error: recoveryError, terminal_reason: recoveryError, terminal_at_ms: updatedAtMs, turn_claim_owner: localClaim ? "local" : null, turn_claim_id: localClaim?.claimId ?? null, turn_claim_run_id: localClaim?.runId ?? null, turn_claim_generation: localClaim?.generation ?? null, turn_claim_owner_epoch: null, updated_at_ms: updatedAtMs, state_changed_at_ms: updatedAtMs }).where("session_id", "=", sessionId).where("state", "=", current.state).where("transition_generation", "=", current.generation)).numAffectedRows !== 1n) throw new Error(`Worker session placement ${sessionId} changed during failure`); return { record: getRequired(db, sessionId), releasedClaim: projectWorkerSessionTurnClaim(current) }; }); if (outcome.releasedClaim) signalWorkerTurnClaimClosed(path, outcome.releasedClaim); return outcome.record; }, adoptActive(input) { const sessionId = required(input.sessionId, "session id"); const environmentId = required(input.environmentId, "environment id"); const ownerEpoch = normalizeEpoch(input.ownerEpoch, "active owner epoch"); const current = getRequired(read(), sessionId); if (current.state !== "active" || current.environmentId !== environmentId || current.activeOwnerEpoch !== ownerEpoch || input.expectedGeneration !== void 0 && current.generation !== input.expectedGeneration) throw new Error(`Cannot adopt stale worker placement for session ${sessionId}`); return current; }, listForReconcile() { const db = read(); return executeSqliteQuerySync(db, query(db).selectFrom("worker_session_placements").selectAll().where("state", "not in", ["local", "reclaimed"]).orderBy("updated_at_ms").orderBy("session_id")).rows.map((row) => withWorkspaceResultConflict(fromRow$1(row))); }, list() { const db = read(); return executeSqliteQuerySync(db, query(db).selectFrom("worker_session_placements").selectAll().orderBy("session_id")).rows.map((row) => withWorkspaceResultConflict(fromRow$1(row))); } }; attachWorkerTurnExecutionIdentityStore(store, path); return store; } //#endregion export { createWorkerSessionPlacementStore as t };