UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

570 lines (569 loc) 23.8 kB
import { a as asOptionalRecord, u as readStringField } from "./record-coerce-DItp3I4t.js"; import { O as parseAgentSessionKey } from "./session-key-BnWWjqNc.js"; import { a as getNodeSqliteKysely, i as executeSqliteQueryTakeFirstSync, r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js"; import "./artifacts-DMDO9PHy.js"; import "./paths-CXdaYWF_.js"; import { g as runOpenClawAgentWriteTransaction, p as openOpenClawAgentDatabase } from "./openclaw-agent-db-CWtDoRbC.js"; import { G as accountSessionGoalUsage, J as buildUpdatedSessionGoalStatus, K as buildCreatedSessionGoal, Wt as applySessionEntryLifecycleMutation, q as buildUpdatedSessionGoalObjective } from "./session-accessor-YsytfDtG.js"; import { n as withOpenClawAgentDatabaseReadOnly } from "./openclaw-agent-db-readonly-CHjf8FxN.js"; import { d as patchSessionEntryCore, u as loadSessionEntryReadOnly } from "./session-accessor.sqlite-entry-CWk3jL7s.js"; import "./group-DlTf9maP.js"; import "./session-accessor.sqlite-entry-store-BxYl0nro.js"; import { r as resolveSqliteTargetFromSessionStorePath } from "./session-sqlite-target-Dp-kgpcT.js"; import "./store-entry-CzRELcpv.js"; import { c as resolveSqliteScope, i as getSessionKysely, m as toDatabaseOptions } from "./session-accessor.sqlite-scope-2KfMzb44.js"; import { S as collectActiveSessionWorkAdmissionKeys, h as pruneStaleEntries } from "./disk-budget-BsDSCUGD.js"; import "./types-BHV0IaPK.js"; import "./main-session-Br0F9dzh.js"; import { t as listSessionEntriesCore } from "./session-accessor.entry-CQ2WPpfn.js"; import "./targets-Cknmo7YZ.js"; import { d as recordSessionGoalChanged } from "./session-state-events-DNCKmH78.js"; import "./combined-store-gateway-DGm4nZAJ.js"; import { t as formatTokenCount } from "./token-format-D942KbWN.js"; import "./main-session.runtime.js"; import { a as SessionWorkStartInvalidatedError } from "./lifecycle-BaroCBMc.js"; import "./reset-DkY1cXo4.js"; import "./session-key-CwhWkrL5.js"; import "./transcript-Dv8-hmIo.js"; import "./delivery-info-CfG8kavj.js"; import "./cleanup-service-DoQBUGSQ.js"; import fs from "node:fs"; import { randomUUID } from "node:crypto"; //#region src/config/sessions/goals.ts const MODEL_UPDATABLE_SESSION_GOAL_STATUSES = ["complete", "blocked"]; function nowMs(value) { return typeof value === "number" && Number.isFinite(value) ? value : Date.now(); } function cloneGoal(goal) { return { ...goal }; } function recordGoalChange(options, entry, summary) { recordSessionGoalChanged({ sessionKey: options.sessionKey, entry, actor: options.actor, agentId: options.agentId, summary }); } function resolveSessionGoalDisplayState(entry, now, options) { return accountSessionGoalUsage(entry, nowMs(now), options); } function goalsEqual(a, b) { return JSON.stringify(a) === JSON.stringify(b); } function formatSessionGoalStatus(goal) { if (!goal) return "No goal for this session.\nStart one with /goal start <objective>."; const budget = goal.tokenBudget === void 0 ? "" : `\nToken budget: ${formatTokenCount(goal.tokensUsed)}/${formatTokenCount(goal.tokenBudget)}`; const note = goal.lastStatusNote ? `\nNote: ${goal.lastStatusNote}` : ""; const commands = resolveGoalCommandHint(goal.status); return [ "Goal", `Status: ${goal.status}`, `Objective: ${goal.objective}`, `Tokens used: ${formatTokenCount(goal.tokensUsed)}`, ...budget ? [budget.slice(1)] : [], ...note ? [note.slice(1)] : [], "", `Commands: ${commands}` ].join("\n"); } function resolveGoalCommandHint(status) { switch (status) { case "active": return "/goal edit <objective>, /goal pause, /goal complete, /goal clear"; case "paused": case "blocked": case "usage_limited": case "budget_limited": return "/goal resume, /goal edit <objective>, /goal clear"; case "complete": return "/goal clear"; } return "/goal"; } async function getSessionGoal(options) { const now = nowMs(options.now); if (options.persist === false) { const entry = loadSessionEntryReadOnly({ sessionKey: options.sessionKey, storePath: options.storePath }) ?? options.fallbackEntry; const projected = entry ? resolveSessionGoalDisplayState(entry, now, { adoptFreshBaseline: false }) : void 0; return projected ? { status: "found", goal: projected } : { status: "missing" }; } let goal; if (!await patchSessionEntryCore({ sessionKey: options.sessionKey, storePath: options.storePath }, (entry) => { const accounted = accountSessionGoalUsage(entry, now); goal = accounted ? cloneGoal(accounted) : void 0; if (!accounted || goalsEqual(accounted, entry.goal)) return null; return { goal: accounted }; }, { fallbackEntry: options.fallbackEntry }) || !goal) return { status: "missing" }; return { status: "found", goal }; } async function createSessionGoal(options) { const objective = options.objective.trim(); if (!objective) throw new Error("objective required"); const now = nowMs(options.now); let created; const result = await patchSessionEntryCore({ sessionKey: options.sessionKey, storePath: options.storePath }, (entry) => { created = buildCreatedSessionGoal(entry, { objective, tokenBudget: options.tokenBudget }, now); return { goal: created }; }, { fallbackEntry: options.fallbackEntry }); if (!result || !created) throw new Error("session not found"); recordGoalChange(options, result, "goal created"); return cloneGoal(created); } async function updateSessionGoalStatus(options) { const now = nowMs(options.now); let updated; let foundSession = false; const result = await patchSessionEntryCore({ sessionKey: options.sessionKey, storePath: options.storePath }, (entry) => { foundSession = true; updated = buildUpdatedSessionGoalStatus(entry, options, now); return { goal: updated }; }); if (!result || !updated) throw new Error(foundSession ? "goal not found" : "session not found"); recordGoalChange(options, result, `goal status changed to ${updated.status}`); return cloneGoal(updated); } async function updateSessionGoalObjective(options) { const objective = options.objective.trim(); if (!objective) throw new Error("objective required"); const now = nowMs(options.now); let updated; let foundSession = false; const result = await patchSessionEntryCore({ sessionKey: options.sessionKey, storePath: options.storePath }, (entry) => { foundSession = true; updated = buildUpdatedSessionGoalObjective(entry, objective, now); return { goal: updated }; }); if (!result || !updated) throw new Error(foundSession ? "goal not found" : "session not found"); recordGoalChange(options, result, "goal objective changed"); return cloneGoal(updated); } async function clearSessionGoal(options) { let removed = false; const result = await patchSessionEntryCore({ sessionKey: options.sessionKey, storePath: options.storePath }, (entry) => { if (!entry.goal) return null; removed = true; return { goal: void 0 }; }); if (result && removed) recordGoalChange(options, result, "goal cleared"); return Boolean(result && removed); } //#endregion //#region src/config/sessions/session-registry-maintenance.ts function parseCronRunSessionJobId(sessionKey) { const parsed = parseAgentSessionKey(sessionKey); if (!parsed) return; return /^cron:([^:]+):run:[^:]+(?:$|:)/u.exec(parsed.rest)?.[1]; } function buildSessionRegistryPreserveKeys(params) { const preserveKeys = collectActiveSessionWorkAdmissionKeys({ storePath: params.storePath, store: params.store }) ?? /* @__PURE__ */ new Set(); let preservedRunning = 0; for (const key of Object.keys(params.store)) { const jobId = parseCronRunSessionJobId(key); if (!jobId) { preserveKeys.add(key); continue; } if (params.runningCronJobIds.has(jobId)) { preserveKeys.add(key); preservedRunning += 1; } } return { preserveKeys, preservedRunning }; } function pruneSessionRegistryStore(params) { const { preserveKeys, preservedRunning } = buildSessionRegistryPreserveKeys({ runningCronJobIds: params.runningCronJobIds, storePath: params.storePath, store: params.store }); const pruned = pruneStaleEntries(params.store, params.retentionMs, { log: false, onPruned: params.removals ? ({ key, entry }) => { params.removals?.push({ sessionKey: key, expectedEntry: entry, archiveRemovedTranscript: true }); } : void 0, preserveKeys }); return { afterCount: Object.keys(params.store).length, preservedRunning, pruned }; } /** * Runs task session-registry maintenance for one resolved agent store. * Preview prunes a clone; apply uses one store-sized write transaction and * skips generic session maintenance so non-cron rows stay outside this sweep. */ async function runSessionRegistryMaintenanceForStore(params) { const { agentId, storePath } = params; const sqliteTarget = resolveSqliteTargetFromSessionStorePath(storePath, { agentId }); if (sqliteTarget.path && !fs.existsSync(sqliteTarget.path)) return { beforeCount: 0, afterCount: 0, preservedRunning: 0, pruned: 0 }; const beforeStore = Object.fromEntries(listSessionEntriesCore({ agentId, storePath }).map(({ sessionKey, entry }) => [sessionKey, entry])); const beforeCount = Object.keys(beforeStore).length; if (!params.apply) { const previewStore = structuredClone(beforeStore); return { beforeCount, ...pruneSessionRegistryStore({ retentionMs: params.retentionMs, runningCronJobIds: params.runningCronJobIds, storePath, store: previewStore }) }; } const applyStore = structuredClone(beforeStore); const removals = []; const applied = pruneSessionRegistryStore({ retentionMs: params.retentionMs, removals, runningCronJobIds: params.runningCronJobIds, storePath, store: applyStore }); if (removals.length > 0) { const mutation = await applySessionEntryLifecycleMutation({ agentId, storePath, removals, skipMaintenance: true }); return { afterCount: mutation.afterCount, beforeCount, preservedRunning: applied.preservedRunning, pruned: mutation.removedEntries }; } return { beforeCount, ...applied }; } //#endregion //#region src/config/sessions/session-accessor.sqlite-entry-identity.ts function readSessionEntryInstanceId(database, sessionKey) { const row = executeSqliteQueryTakeFirstSync(database.db, getSessionKysely(database.db).selectFrom("session_nodes").select(["current_session_id", "entry_json"]).where("session_key", "=", sessionKey)); if (!row?.entry_json) return; try { const sessionId = readStringField(asOptionalRecord(JSON.parse(row.entry_json)), "sessionId"); return sessionId === row.current_session_id ? sessionId : void 0; } catch { return; } } //#endregion //#region src/config/sessions/session-sharing-store.ts const SESSION_MEMBERSHIP_QUERY_CHUNK_SIZE = 400; function resolveDatabaseOptions$1(scope) { return toDatabaseOptions(resolveSqliteScope(scope)); } function getSessionMemberKysely(database) { return getNodeSqliteKysely(database.db); } function readSessionMembers(scope, fallback, operation) { const result = withOpenClawAgentDatabaseReadOnly(operation, resolveDatabaseOptions$1(scope), { throwOnMissingTable: true }); return result.found ? result.value : fallback; } function listSessionMembers(scope) { return readSessionMembers(scope, [], (database) => { const db = getSessionMemberKysely(database); return executeSqliteQuerySync(database.db, db.selectFrom("session_members").select([ "identity_id", "added_by", "added_at" ]).where("session_key", "=", resolveSqliteScope(scope).sessionKey).orderBy("identity_id")).rows.map((row) => ({ identityId: row.identity_id, addedBy: row.added_by, addedAt: row.added_at })); }); } function listSessionMembershipKeys(scope, sessionKeys, identityId) { const normalizedIdentityId = identityId.trim(); const normalizedSessionKeys = [...new Set(sessionKeys.map((key) => key.trim()).filter(Boolean))]; if (!normalizedIdentityId || normalizedSessionKeys.length === 0) return /* @__PURE__ */ new Set(); return readSessionMembers(scope, /* @__PURE__ */ new Set(), (database) => { const db = getSessionMemberKysely(database); const memberships = /* @__PURE__ */ new Set(); for (let offset = 0; offset < normalizedSessionKeys.length; offset += SESSION_MEMBERSHIP_QUERY_CHUNK_SIZE) { const chunk = normalizedSessionKeys.slice(offset, offset + SESSION_MEMBERSHIP_QUERY_CHUNK_SIZE); const rows = executeSqliteQuerySync(database.db, db.selectFrom("session_members").select("session_key").where("identity_id", "=", normalizedIdentityId).where("session_key", "in", chunk)).rows; for (const row of rows) memberships.add(row.session_key); } return memberships; }); } function isSessionMember(scope, identityId) { const normalizedIdentityId = identityId.trim(); if (!normalizedIdentityId) return false; return readSessionMembers(scope, false, (database) => { const db = getSessionMemberKysely(database); return Boolean(executeSqliteQueryTakeFirstSync(database.db, db.selectFrom("session_members").select("identity_id").where("session_key", "=", resolveSqliteScope(scope).sessionKey).where("identity_id", "=", normalizedIdentityId))); }); } function assertAuthorizedSessionInstance(database, sessionKey, expectedSessionId) { const sessionId = readSessionEntryInstanceId(database, sessionKey); if (sessionId === void 0 || expectedSessionId !== void 0 && sessionId !== expectedSessionId) throw new Error("session changed before sharing mutation"); } function addSessionMember(scope, params) { const identityId = params.identityId.trim(); const addedBy = params.addedBy.trim(); if (!identityId || !addedBy) throw new Error("session member identity and actor are required"); const options = resolveDatabaseOptions$1(scope); const addedAt = params.addedAt ?? Date.now(); const inserted = runOpenClawAgentWriteTransaction((database) => { assertAuthorizedSessionInstance(database, resolveSqliteScope(scope).sessionKey, params.expectedSessionId); const db = getSessionMemberKysely(database); return (executeSqliteQuerySync(database.db, db.insertInto("session_members").values({ session_key: resolveSqliteScope(scope).sessionKey, identity_id: identityId, added_by: addedBy, added_at: addedAt }).onConflict((conflict) => conflict.columns(["session_key", "identity_id"]).doNothing())).numAffectedRows ?? 0n) > 0n; }, options); return { member: { identityId, addedBy, addedAt }, inserted }; } function removeSessionMember(scope, identityId, expected, expectedSessionId) { const normalizedIdentityId = identityId.trim(); if (!normalizedIdentityId) return null; const options = resolveDatabaseOptions$1(scope); return runOpenClawAgentWriteTransaction((database) => { assertAuthorizedSessionInstance(database, resolveSqliteScope(scope).sessionKey, expectedSessionId); const db = getSessionMemberKysely(database); const row = executeSqliteQueryTakeFirstSync(database.db, db.selectFrom("session_members").select([ "identity_id", "added_by", "added_at" ]).where("session_key", "=", resolveSqliteScope(scope).sessionKey).where("identity_id", "=", normalizedIdentityId)); if (!row || expected && (row.added_by !== expected.addedBy || row.added_at !== expected.addedAt)) return null; executeSqliteQuerySync(database.db, db.deleteFrom("session_members").where("session_key", "=", resolveSqliteScope(scope).sessionKey).where("identity_id", "=", normalizedIdentityId)); return { identityId: row.identity_id, addedBy: row.added_by, addedAt: row.added_at }; }, options); } //#endregion //#region src/config/sessions/session-suggestion-store.ts const MAX_PENDING_SESSION_SUGGESTIONS_PER_AUTHOR = 20; const MAX_PENDING_SESSION_SUGGESTIONS_PER_SESSION = 100; const MAX_RETAINED_RESOLVED_SESSION_SUGGESTIONS = 200; const SESSION_SUGGESTION_DISPATCH_CLAIM_TTL_MS = 3e4; function resolveDatabaseOptions(scope) { return toDatabaseOptions(resolveSqliteScope(scope)); } function suggestionDb(database) { return getNodeSqliteKysely(database.db); } function toSuggestion(row) { return { id: row.id, authorId: row.author_id, ...row.author_label ? { authorLabel: row.author_label } : {}, text: row.text, createdAt: row.created_at, state: row.state }; } function assertSessionInstance(database, sessionKey, expectedSessionId) { if (expectedSessionId === void 0) return; if (readSessionEntryInstanceId(database, sessionKey) !== expectedSessionId) throw new SessionWorkStartInvalidatedError("session changed before suggestion mutation"); } function pruneResolvedSessionSuggestions(database, sessionKey) { const db = suggestionDb(database); const resolvedRows = executeSqliteQuerySync(database.db, db.selectFrom("session_suggestions").select("id").where("session_key", "=", sessionKey).where("state", "!=", "pending").orderBy("created_at", "desc").orderBy("id", "desc").limit((eb) => eb.lit(-1)).offset((eb) => eb.lit(MAX_RETAINED_RESOLVED_SESSION_SUGGESTIONS))).rows; if (resolvedRows.length === 0) return; executeSqliteQuerySync(database.db, db.deleteFrom("session_suggestions").where("id", "in", resolvedRows.map((row) => row.id))); } function addSessionSuggestion(scope, params) { const authorId = params.authorId.trim(); const authorLabel = params.authorLabel?.trim() || void 0; const text = params.text; if (!authorId || !text.trim()) throw new Error("suggestion author and text are required"); const options = resolveDatabaseOptions(scope); const sessionKey = resolveSqliteScope(scope).sessionKey; const suggestion = { id: params.id ?? randomUUID(), authorId, ...authorLabel ? { authorLabel } : {}, text, createdAt: params.createdAt ?? Date.now(), state: "pending" }; runOpenClawAgentWriteTransaction((database) => { assertSessionInstance(database, sessionKey, params.expectedSessionId); const db = suggestionDb(database); pruneResolvedSessionSuggestions(database, sessionKey); const pendingCounts = executeSqliteQuerySync(database.db, db.selectFrom("session_suggestions").select((eb) => ["author_id", eb.fn.countAll().as("count")]).where("session_key", "=", sessionKey).where("state", "=", "pending").groupBy("author_id")).rows.reduce((counts, row) => ({ session: counts.session + row.count, author: counts.author + (row.author_id === suggestion.authorId ? row.count : 0) }), { session: 0, author: 0 }); if (pendingCounts.session >= MAX_PENDING_SESSION_SUGGESTIONS_PER_SESSION) throw new Error("session pending suggestion limit reached"); if (pendingCounts.author >= MAX_PENDING_SESSION_SUGGESTIONS_PER_AUTHOR) throw new Error("author pending suggestion limit reached"); executeSqliteQuerySync(database.db, db.insertInto("session_suggestions").values({ id: suggestion.id, session_key: sessionKey, author_id: suggestion.authorId, author_label: suggestion.authorLabel ?? null, text: suggestion.text, created_at: suggestion.createdAt, state: suggestion.state, dispatch_token: null, dispatch_started_at: null, dispatch_resolution: null })); }, options); return suggestion; } function listSessionSuggestions(scope, params = {}) { const options = resolveDatabaseOptions(scope); const database = openOpenClawAgentDatabase(options); const sessionKey = resolveSqliteScope(scope).sessionKey; let query = suggestionDb(database).selectFrom("session_suggestions").select([ "id", "author_id", "author_label", "text", "created_at", "state" ]).where("session_key", "=", sessionKey); if (params.authorId?.trim()) query = query.where("author_id", "=", params.authorId.trim()); if (params.pendingOnly) query = query.where("state", "=", "pending"); return executeSqliteQuerySync(database.db, query.orderBy("created_at", "asc").orderBy("id", "asc")).rows.map(toSuggestion); } function claimSessionSuggestionDispatch(scope, params) { const options = resolveDatabaseOptions(scope); const sessionKey = resolveSqliteScope(scope).sessionKey; return runOpenClawAgentWriteTransaction((database) => { assertSessionInstance(database, sessionKey, params.expectedSessionId); const db = suggestionDb(database); const row = executeSqliteQueryTakeFirstSync(database.db, db.selectFrom("session_suggestions").select([ "id", "author_id", "author_label", "text", "created_at", "state", "dispatch_token", "dispatch_started_at", "dispatch_resolution" ]).where("session_key", "=", sessionKey).where("id", "=", params.id).where("state", "=", "pending")); if (!row) return null; const now = params.now ?? Date.now(); const claimTtlMs = params.claimTtlMs ?? 3e4; if (row.dispatch_token && row.dispatch_started_at !== null && now - row.dispatch_started_at < claimTtlMs) return { kind: "busy" }; if (row.dispatch_resolution && row.dispatch_resolution !== params.resolution) return { kind: "mismatch", resolution: row.dispatch_resolution }; const token = randomUUID(); executeSqliteQuerySync(database.db, db.updateTable("session_suggestions").set({ dispatch_token: token, dispatch_started_at: now, dispatch_resolution: params.resolution }).where("session_key", "=", sessionKey).where("id", "=", params.id).where("state", "=", "pending")); return { kind: "claimed", suggestion: toSuggestion(row), token }; }, options); } function releaseSessionSuggestionDispatch(scope, params) { const options = resolveDatabaseOptions(scope); const sessionKey = resolveSqliteScope(scope).sessionKey; return runOpenClawAgentWriteTransaction((database) => { assertSessionInstance(database, sessionKey, params.expectedSessionId); return (executeSqliteQuerySync(database.db, suggestionDb(database).updateTable("session_suggestions").set({ dispatch_token: null, dispatch_started_at: null, dispatch_resolution: null }).where("session_key", "=", sessionKey).where("id", "=", params.id).where("state", "=", "pending").where("dispatch_token", "=", params.token)).numAffectedRows ?? 0n) > 0n; }, options); } function finalizeSessionSuggestionClaim(scope, params) { const options = resolveDatabaseOptions(scope); const sessionKey = resolveSqliteScope(scope).sessionKey; return runOpenClawAgentWriteTransaction((database) => { assertSessionInstance(database, sessionKey, params.expectedSessionId); const db = suggestionDb(database); const row = executeSqliteQueryTakeFirstSync(database.db, db.selectFrom("session_suggestions").select([ "id", "author_id", "author_label", "text", "created_at", "state" ]).where("session_key", "=", sessionKey).where("id", "=", params.id).where("state", "=", "pending").where("dispatch_token", "=", params.token)); if (!row) return null; if ((executeSqliteQuerySync(database.db, db.updateTable("session_suggestions").set({ state: params.state, dispatch_token: null, dispatch_started_at: null, dispatch_resolution: null }).where("session_key", "=", sessionKey).where("id", "=", params.id).where("state", "=", "pending").where("dispatch_token", "=", params.token)).numAffectedRows ?? 0n) === 0n) return null; pruneResolvedSessionSuggestions(database, sessionKey); return { ...toSuggestion(row), state: params.state }; }, options); } //#endregion export { getSessionGoal as _, listSessionSuggestions as a, updateSessionGoalStatus as b, isSessionMember as c, removeSessionMember as d, runSessionRegistryMaintenanceForStore as f, formatSessionGoalStatus as g, createSessionGoal as h, finalizeSessionSuggestionClaim as i, listSessionMembers as l, clearSessionGoal as m, addSessionSuggestion as n, releaseSessionSuggestionDispatch as o, MODEL_UPDATABLE_SESSION_GOAL_STATUSES as p, claimSessionSuggestionDispatch as r, addSessionMember as s, SESSION_SUGGESTION_DISPATCH_CLAIM_TTL_MS as t, listSessionMembershipKeys as u, resolveSessionGoalDisplayState as v, updateSessionGoalObjective as y };