openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
389 lines (388 loc) • 18 kB
JavaScript
import { a as getNodeSqliteKysely, i as executeSqliteQueryTakeFirstSync, r as executeSqliteQuerySync, u as sqliteStringSet } from "./kysely-sync-K7ftY-g4.mjs";
import { c as runSqlitePinnedReadSnapshotSync } from "./sqlite-schema-facts-CG0Tk84j.mjs";
import { i as requestSqliteWorkerOperationAdmission, s as takeSqliteWorkerOperationAdmissionAttachment } from "./sqlite-worker-operation-admission-cNGSH5v0.mjs";
import { l as withExistingOpenClawStateDatabaseArtifactPreservingReadOnly } from "./openclaw-state-db-readonly-CC57AD_q.mjs";
import { l as runOpenClawStateWriteTransaction } from "./openclaw-state-db-D1jWDzK3.mjs";
import { c as parseFailedPayload, n as baseRecord, o as decodeClaimColumns, r as claimedRecord, t as FAILED_NULL_PAYLOAD_SENTINEL } from "./ingress-queue.codec-BSYlAVif.mjs";
import { randomUUID } from "node:crypto";
//#region src/channels/message/ingress-queue.kernel.ts
const getQueue = (db) => getNodeSqliteKysely(db);
const affectedRows = (result) => Number(result.numAffectedRows ?? 0n);
const LIST_PENDING_BATCH_SIZE = 100;
function normalizeLimit(limit) {
return limit === "all" ? Number.MAX_SAFE_INTEGER : Math.max(1, Math.floor(limit ?? 100));
}
function selectRow(db, queueName, id) {
return executeSqliteQueryTakeFirstSync(db, getQueue(db).selectFrom("channel_ingress_events").selectAll().where("queue_name", "=", queueName).where("event_id", "=", id));
}
function readChannelIngressClaimSnapshotInDatabase(db, input) {
const base = getQueue(db).selectFrom("channel_ingress_events").selectAll().where("queue_name", "=", input.queueName);
const claimed = input.candidateIds?.length ? executeSqliteQuerySync(db, base.where("status", "=", "claimed").where("event_id", "in", input.candidateIds).orderBy("event_id", "asc")).rows : [];
if (input.candidateIds?.length === 0) return {
pending: [],
claimed
};
let pending = base.where("status", "=", "pending");
if (input.candidateIds) pending = pending.where("event_id", "in", input.candidateIds);
const blocked = [.../* @__PURE__ */ new Set([...input.blockedLaneKeys, ...claimed.flatMap((row) => row.lane_key ? [row.lane_key] : [])])];
if (!input.deriveLaneKey && blocked.length) pending = pending.where((eb) => eb.or([eb("lane_key", "is", null), eb("lane_key", "not in", blocked)]));
const ordered = input.orderBy === "id" ? pending.orderBy("event_id", "asc") : pending.orderBy("received_at", "asc").orderBy("event_id", "asc");
return {
claimed,
pending: executeSqliteQuerySync(db, ordered.limit(normalizeLimit(input.scanLimit) + 100)).rows
};
}
function tombstoneCorruptRow(db, row, now, reason) {
executeSqliteQuerySync(db, getQueue(db).updateTable("channel_ingress_events").set((eb) => ({
status: "failed",
failed_at: now,
failed_reason: reason,
last_error: null,
...reason === "corrupt_payload" ? {
payload_json: "null",
metadata_json: null
} : { payload_json: eb.case().when("payload_json", "=", "null").then(FAILED_NULL_PAYLOAD_SENTINEL).else(eb.ref("payload_json")).end() },
claim_token: null,
claim_owner: null,
claimed_at: null,
updated_at: now
})).where("queue_name", "=", row.queue_name).where("event_id", "=", row.event_id));
}
function enqueueChannelIngressInDatabase(db, input) {
const inserted = executeSqliteQuerySync(db, getQueue(db).insertInto("channel_ingress_events").values({
queue_name: input.queueName,
event_id: input.id,
channel_id: input.channelId,
account_id: input.accountId,
status: "pending",
lane_key: input.laneKey ?? null,
payload_json: input.payloadJson,
metadata_json: input.metadataJson,
received_at: input.receivedAt,
updated_at: input.now,
attempts: 0
}).onConflict((conflict) => conflict.columns(["queue_name", "event_id"]).doNothing()));
let row = selectRow(db, input.queueName, input.id);
if (!row) throw new Error(`Failed to read channel ingress event ${input.queueName}/${input.id}`);
const accepted = affectedRows(inserted) > 0;
if (accepted && !baseRecord(row)) throw new Error(`Corrupt payload_json in channel ingress event ${input.queueName}/${input.id}`);
if (row.status === "claimed" && !claimedRecord(row)) throw new Error(`Corrupt claimed channel ingress event ${input.queueName}/${input.id}`);
if (row.status === "pending" && !baseRecord(row)) {
tombstoneCorruptRow(db, row, input.now, "corrupt_payload");
row = selectRow(db, input.queueName, input.id);
if (!row) throw new Error(`Failed to read corrupt ingress tombstone ${input.queueName}/${input.id}`);
}
return {
accepted,
row
};
}
function claimChannelIngressInDatabase(db, input, now = Date.now) {
const transitionAt = now();
const row = selectRow(db, input.queueName, input.id);
if (!row || row.status !== "pending") return null;
if (!baseRecord(row)) {
tombstoneCorruptRow(db, row, transitionAt, "corrupt_payload");
return null;
}
executeSqliteQuerySync(db, getQueue(db).updateTable("channel_ingress_events").set({
status: "claimed",
claim_token: randomUUID(),
claim_owner: input.ownerId,
claimed_at: transitionAt,
updated_at: transitionAt,
...input.laneKey ? { lane_key: input.laneKey } : {}
}).where("queue_name", "=", input.queueName).where("event_id", "=", input.id).where("status", "=", "pending"));
return selectRow(db, input.queueName, input.id) ?? null;
}
function claimNextChannelIngressInDatabase(db, input, now = Date.now) {
const current = readChannelIngressClaimSnapshotInDatabase(db, input.request);
if (JSON.stringify(current) !== JSON.stringify(input.snapshot)) return { kind: "conflict" };
const transitionAt = now();
const corrupt = new Set(input.selection.corruptIds);
for (const row of current.pending) if (corrupt.has(row.event_id)) tombstoneCorruptRow(db, row, transitionAt, "corrupt_payload");
const selected = input.selection.selected;
return {
kind: "claimed",
row: selected ? claimChannelIngressInDatabase(db, {
queueName: input.request.queueName,
...selected,
ownerId: input.ownerId
}, () => transitionAt) : null
};
}
function recoverChannelIngressClaimInDatabase(db, input) {
const current = selectRow(db, input.row.queue_name, input.row.event_id);
if (!current || JSON.stringify(current) !== JSON.stringify(input.row)) return false;
const claim = decodeClaimColumns(current);
if (!claim || !claimedRecord(current)) {
tombstoneCorruptRow(db, current, input.now, claim ? "corrupt_payload" : "corrupt_claim");
return true;
}
return affectedRows(executeSqliteQuerySync(db, getQueue(db).updateTable("channel_ingress_events").set((eb) => ({
status: "pending",
claim_token: null,
claim_owner: null,
claimed_at: null,
attempts: eb("attempts", "+", 1),
last_attempt_at: input.now,
updated_at: input.now
})).where("queue_name", "=", current.queue_name).where("event_id", "=", current.event_id).where("status", "=", "claimed").where("claim_token", "=", claim.token).where("claimed_at", "<=", input.cutoff))) > 0;
}
function selectedMutation(db, input) {
const base = getQueue(db).updateTable("channel_ingress_events").where("queue_name", "=", input.queueName).where("event_id", "=", input.id);
return input.token === null ? base.where("status", "=", "pending") : base.where("status", "=", "claimed").where("claim_token", "=", input.token);
}
function refreshChannelIngressClaimInDatabase(db, input) {
return affectedRows(executeSqliteQuerySync(db, selectedMutation(db, input).set({
claimed_at: input.now,
updated_at: input.now
}))) > 0;
}
function completeChannelIngressInDatabase(db, input) {
const update = executeSqliteQuerySync(db, selectedMutation(db, input).set({
status: "completed",
completed_at: input.now,
completed_metadata_json: input.metadataJson,
payload_json: "null",
metadata_json: null,
claim_token: null,
claim_owner: null,
claimed_at: null,
last_attempt_at: null,
last_error: null,
updated_at: input.now
}));
if (affectedRows(update) > 0) return true;
if (input.token !== null) return false;
return affectedRows(executeSqliteQuerySync(db, getQueue(db).insertInto("channel_ingress_events").values({
queue_name: input.queueName,
event_id: input.id,
channel_id: input.channelId,
account_id: input.accountId,
status: "completed",
lane_key: null,
payload_json: "null",
metadata_json: null,
received_at: input.now,
updated_at: input.now,
attempts: 0,
completed_at: input.now,
completed_metadata_json: input.metadataJson
}).onConflict((conflict) => conflict.columns(["queue_name", "event_id"]).doNothing()))) > 0;
}
function releaseChannelIngressInDatabase(db, input) {
return affectedRows(executeSqliteQuerySync(db, selectedMutation(db, input).set((eb) => ({
status: "pending",
claim_token: null,
claim_owner: null,
claimed_at: null,
...input.recordAttempt === false ? {} : {
attempts: eb("attempts", "+", 1),
last_attempt_at: input.now
},
...input.lastError === void 0 ? {} : { last_error: input.lastError },
updated_at: input.now
})))) > 0;
}
function failChannelIngressInDatabase(db, input) {
return affectedRows(executeSqliteQuerySync(db, selectedMutation(db, input).set((eb) => ({
status: "failed",
failed_at: input.now,
failed_reason: input.reason,
last_error: input.message ?? null,
payload_json: eb.case().when("payload_json", "=", "null").then(FAILED_NULL_PAYLOAD_SENTINEL).else(eb.ref("payload_json")).end(),
claim_token: null,
claim_owner: null,
claimed_at: null,
updated_at: input.now
})))) > 0;
}
function pruneChannelIngressInDatabase(db, input) {
const kysely = getQueue(db);
const protectedIds = (input.options.protectIds ?? []).map((id) => id.trim()).filter(Boolean);
const protectedMaxIds = protectedIds.filter((id) => id.isWellFormed());
let deleted = 0;
const policies = [
{
status: "pending",
column: "updated_at",
ttl: input.options.pendingTtlMs,
max: input.options.pendingMaxEntries
},
{
status: "completed",
column: "completed_at",
ttl: input.options.completedTtlMs,
max: input.options.completedMaxEntries
},
{
status: "failed",
column: "failed_at",
ttl: input.options.failedTtlMs,
max: input.options.failedMaxEntries
}
];
for (const policy of policies) {
if (policy.ttl !== void 0) {
let query = kysely.deleteFrom("channel_ingress_events").where("queue_name", "=", input.queueName).where("status", "=", policy.status).where(policy.column, "<", input.now - policy.ttl);
if (protectedIds.length) query = query.where("event_id", "not in", protectedIds);
deleted += affectedRows(executeSqliteQuerySync(db, query));
}
if (policy.max === void 0) continue;
const overflow = kysely.selectFrom("channel_ingress_events").select("event_id").where("queue_name", "=", input.queueName).where("status", "=", policy.status).orderBy("updated_at", "desc").orderBy("event_id", "desc").limit(-1).offset(Math.max(0, Math.floor(policy.max)));
let candidates = kysely.selectFrom(overflow.as("overflow")).select("event_id").limit(500);
if (protectedMaxIds.length) candidates = candidates.where("event_id", "not in", sqliteStringSet(protectedMaxIds));
const query = kysely.deleteFrom("channel_ingress_events").where("queue_name", "=", input.queueName).where("status", "=", policy.status).where("event_id", "in", candidates);
while (true) {
const removed = affectedRows(executeSqliteQuerySync(db, query));
if (!removed) break;
deleted += removed;
}
}
return deleted;
}
function listStaleChannelIngressClaimsInDatabase(db, input) {
return executeSqliteQuerySync(db, getQueue(db).selectFrom("channel_ingress_events").selectAll().where("queue_name", "=", input.queueName).where("status", "=", "claimed").where((eb) => eb.or([
eb("claimed_at", "<=", input.cutoff),
eb("claimed_at", "is", null),
eb("claim_token", "is", null),
eb("claim_owner", "is", null),
eb("claim_token", "=", ""),
eb("claim_owner", "=", "")
]))).rows;
}
function deleteChannelIngressInDatabase(db, input) {
const base = getQueue(db).deleteFrom("channel_ingress_events").where("queue_name", "=", input.queueName).where("event_id", "=", input.id);
return affectedRows(executeSqliteQuerySync(db, input.token === null ? base.where("status", "=", "pending") : base.where("status", "=", "claimed").where("claim_token", "=", input.token))) > 0;
}
function resubmitChannelIngressInDatabase(db, input) {
const row = selectRow(db, input.queueName, input.id);
if (!row) return { kind: "not-found" };
if (row.status === "completed") return {
kind: "completed",
row
};
if (row.status !== "failed") return {
kind: "active",
status: row.status === "claimed" ? "claimed" : "pending"
};
if (row.payload_json === "null" || !parseFailedPayload(row.payload_json).ok) return {
kind: "unrecoverable",
row
};
executeSqliteQuerySync(db, getQueue(db).updateTable("channel_ingress_events").set({
status: "pending",
payload_json: row.payload_json === "OPENCLAW_CHANNEL_INGRESS_FAILED_NULL_V1" ? "null" : row.payload_json,
received_at: input.now,
updated_at: input.now,
attempts: 0,
last_attempt_at: null,
last_error: null,
failed_at: null,
failed_reason: null,
claim_token: null,
claim_owner: null,
claimed_at: null,
completed_at: null,
completed_metadata_json: null
}).where("queue_name", "=", input.queueName).where("event_id", "=", input.id).where("status", "=", "failed"));
const updated = selectRow(db, input.queueName, input.id);
if (!updated) throw new Error(`Failed to read resubmitted channel ingress event ${input.queueName}/${input.id}`);
return {
kind: "resubmitted",
row: updated,
previous: row
};
}
function purgeChannelIngressInDatabase(db, input) {
return affectedRows(executeSqliteQuerySync(db, getQueue(db).deleteFrom("channel_ingress_events").where("queue_name", "=", input.queueName)));
}
function listChannelIngressRowsInDatabase(db, input) {
const select = getQueue(db).selectFrom("channel_ingress_events").selectAll().where("queue_name", "=", input.queueName).where("status", "in", input.status === "unsettled" ? ["pending", "claimed"] : [input.status]);
if (input.status === "claimed") return executeSqliteQuerySync(db, select.orderBy("claimed_at", "asc").orderBy("received_at", "asc").orderBy("event_id", "asc")).rows;
if (input.status === "failed") return executeSqliteQuerySync(db, select.orderBy("failed_at", "asc").orderBy("event_id", "asc").limit(normalizeLimit(input.limit))).rows;
const ordered = input.orderBy === "id" ? select.orderBy("event_id", "asc") : select.orderBy("received_at", "asc").orderBy("event_id", "asc");
if (input.status === "unsettled") return executeSqliteQuerySync(db, ordered).rows;
const limit = normalizeLimit(input.limit);
const result = [];
let last;
while (result.length < limit) {
let page = ordered;
if (last) {
const cursor = last;
page = input.orderBy === "id" ? page.where("event_id", ">", cursor.event_id) : page.where((eb) => eb.or([eb("received_at", ">", cursor.received_at), eb.and([eb("received_at", "=", cursor.received_at), eb("event_id", ">", cursor.event_id)])]));
}
const rows = executeSqliteQuerySync(db, page.limit(LIST_PENDING_BATCH_SIZE)).rows;
for (const row of rows) if (baseRecord(row)) {
result.push(row);
if (result.length === limit) break;
}
if (rows.length < LIST_PENDING_BATCH_SIZE) break;
last = rows.at(-1);
}
return result;
}
//#endregion
//#region src/channels/message/ingress-queue.worker.ts
function write(apply, claim = false) {
return (input, { open, stateOptions }) => {
const options = stateOptions();
const database = open();
let transitionClock;
if (claim && input.customClock) {
const attachment = takeSqliteWorkerOperationAdmissionAttachment();
if (!(attachment instanceof Float64Array) || !(attachment.buffer instanceof SharedArrayBuffer) || attachment.length !== 1) throw new Error("Channel ingress claim requires its admitted clock attachment");
transitionClock = attachment;
}
return runOpenClawStateWriteTransaction(({ db }) => {
let admitted = false;
const admitTransaction = () => {
requestSqliteWorkerOperationAdmission({
stage: "transaction",
facts: void 0
});
admitted = true;
};
if (!claim) admitTransaction();
const result = apply(db, input, () => {
admitTransaction();
return transitionClock?.[0] ?? Date.now();
});
if (!admitted) admitTransaction();
requestSqliteWorkerOperationAdmission({
stage: "commit",
facts: void 0
});
return result;
}, {
path: database.path,
env: options.env
});
};
}
const channelIngressOperations = {
"channelIngress.list": (input, { open, stateOptions }) => {
const read = ({ db }) => runSqlitePinnedReadSnapshotSync(db, () => listChannelIngressRowsInDatabase(db, input));
return input.readOnly ? withExistingOpenClawStateDatabaseArtifactPreservingReadOnly(read, stateOptions()) ?? [] : read(open());
},
"channelIngress.claimSnapshot": (input, { open }) => {
const { db } = open();
return runSqlitePinnedReadSnapshotSync(db, () => readChannelIngressClaimSnapshotInDatabase(db, input));
},
"channelIngress.staleClaims": (input, { open }) => listStaleChannelIngressClaimsInDatabase(open().db, input),
"channelIngress.enqueue": write(enqueueChannelIngressInDatabase),
"channelIngress.claim": write(claimChannelIngressInDatabase, true),
"channelIngress.claimNext": write(claimNextChannelIngressInDatabase, true),
"channelIngress.recover": write(recoverChannelIngressClaimInDatabase),
"channelIngress.refresh": write(refreshChannelIngressClaimInDatabase),
"channelIngress.complete": write(completeChannelIngressInDatabase),
"channelIngress.release": write(releaseChannelIngressInDatabase),
"channelIngress.fail": write(failChannelIngressInDatabase),
"channelIngress.delete": write(deleteChannelIngressInDatabase),
"channelIngress.resubmit": write(resubmitChannelIngressInDatabase),
"channelIngress.prune": write(pruneChannelIngressInDatabase),
"channelIngress.purge": write(purgeChannelIngressInDatabase)
};
//#endregion
export { channelIngressOperations };