UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

163 lines (162 loc) • 7.56 kB
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.mjs"; import { r as resolveStateDir } from "./state-dir-CfAW6S2K.mjs"; import { n as runtimeProcessEntrypoints } from "./runtime-process-entrypoints-Bh4reDp0.mjs"; import { r as resolveRuntimeWorkerUrl } from "./runtime-worker-url-S0NIVxUR.mjs"; import { r as cloneEnvWithPlatformSemantics } from "./config-env-vars-oFV7TqIg.mjs"; import { c as resolveOpenClawAgentSqlitePath, i as isIncognitoOpenClawAgentSqlitePath } from "./openclaw-agent-db.paths-DiWMV5LG.mjs"; import { d as runOpenClawAgentWriteTransaction, p as withOpenClawAgentDatabaseAsync } from "./openclaw-agent-db-CFSa_Qik.mjs"; import { r as runOpenClawAgentWriteAdmission } from "./openclaw-agent-write-admission-xWSWPO-q.mjs"; import { t as captureOpenClawAgentDatabaseExecution } from "./openclaw-agent-execution-C-XzIRol.mjs"; import { t as openOpenClawAgentSqliteWorkerStore } from "./openclaw-agent-worker-store-BaoL_lT2.mjs"; import { _ as toDatabaseOptions, l as resolveSqliteScope } from "./session-accessor.sqlite-scope-Dovl36Ht.mjs"; import { n as persistHeartbeatOutcomeInDatabase, t as claimHeartbeatOutcomeRowInDatabase } from "./heartbeat-outcome-store.kernel-BMxTlLT7.mjs"; //#region src/infra/heartbeat-outcome-store.ts const HEARTBEAT_OUTCOME_SUMMARY_MAX_CHARS = 4e3; const HEARTBEAT_OUTCOME_REASON_MAX_CHARS = 1e3; const HEARTBEAT_OUTCOME_NEXT_CHECK_MAX_CHARS = 500; const HEARTBEAT_OUTCOME_WAKE_REASON_MAX_CHARS = 1e3; const HEARTBEAT_OUTCOME_TASK_NAME_MAX_CHARS = 200; const HEARTBEAT_OUTCOME_MAX_TASKS = 32; function boundedText(value, maxChars) { const normalized = value?.trim(); return normalized ? truncateUtf16Safe(normalized, maxChars) : void 0; } function normalizeTaskNames(taskNames) { return taskNames.map((name) => boundedText(name, HEARTBEAT_OUTCOME_TASK_NAME_MAX_CHARS)).filter((name) => Boolean(name)).slice(0, HEARTBEAT_OUTCOME_MAX_TASKS); } function parseTaskNames(value) { if (!value) return []; try { const parsed = JSON.parse(value); return Array.isArray(parsed) ? normalizeTaskNames(parsed.filter((item) => typeof item === "string")) : []; } catch { return []; } } function rowToOutcome(row) { if (row.outcome !== "progress" && row.outcome !== "done" && row.outcome !== "blocked" && row.outcome !== "needs_attention") return; return { sessionKey: row.session_key, runSessionKey: row.run_session_key, outcome: row.outcome, summary: row.summary, ...row.response_reason ? { responseReason: row.response_reason } : {}, ...row.priority === "low" || row.priority === "normal" || row.priority === "high" ? { priority: row.priority } : {}, ...row.next_check ? { nextCheck: row.next_check } : {}, taskNames: parseTaskNames(row.task_names_json), ...row.wake_source ? { wakeSource: row.wake_source } : {}, ...row.wake_reason ? { wakeReason: row.wake_reason } : {}, occurredAt: row.occurred_at }; } /** Replaces the previous silent heartbeat outcome for one base session. */ async function persistHeartbeatOutcome(params) { if (params.response.notify || params.response.outcome === "no_change") return; const taskNames = normalizeTaskNames(params.taskNames ?? []); await runHeartbeatOutcomeOperation(params, { type: "persist", input: { session_key: params.sessionKey, run_session_key: params.runSessionKey, outcome: params.response.outcome, summary: boundedText(params.response.summary, HEARTBEAT_OUTCOME_SUMMARY_MAX_CHARS) ?? params.response.outcome, response_reason: boundedText(params.response.reason, HEARTBEAT_OUTCOME_REASON_MAX_CHARS) ?? null, priority: params.response.priority ?? null, next_check: boundedText(params.response.nextCheck, HEARTBEAT_OUTCOME_NEXT_CHECK_MAX_CHARS) ?? null, task_names_json: taskNames.length > 0 ? JSON.stringify(taskNames) : null, wake_source: params.wakeSource ?? null, wake_reason: boundedText(params.wakeReason, HEARTBEAT_OUTCOME_WAKE_REASON_MAX_CHARS) ?? null, occurred_at: params.occurredAt, context_run_id: null, context_claimed_at: null, updated_at: Date.now() } }); } /** Claims the latest outcome for one user run while allowing that run's retries. */ async function claimHeartbeatOutcomeForRun(params) { const row = await runHeartbeatOutcomeOperation(params, { type: "claim", input: { sessionKey: params.sessionKey, runId: params.runId } }, params.assertCurrent); return row ? rowToOutcome(row) : void 0; } async function runHeartbeatOutcomeOperation(params, command, assertCurrent = () => void 0) { const resolved = toDatabaseOptions(resolveSqliteScope(params)); const env = cloneEnvWithPlatformSemantics(resolved.env ?? process.env); env.OPENCLAW_STATE_DIR = resolveStateDir(env); const options = { ...resolved, env, path: resolveOpenClawAgentSqlitePath({ ...resolved, env }) }; if (isIncognitoOpenClawAgentSqlitePath(options.path, options)) return runOpenClawAgentWriteAdmission(options, () => runOpenClawAgentWriteTransaction(({ db }) => { assertCurrent(); if (command.type === "persist") { persistHeartbeatOutcomeInDatabase(db, command.input); return; } return claimHeartbeatOutcomeRowInDatabase(db, command.input); }, options, { operationLabel: `heartbeat.outcome.${command.type}` }), true); const execution = captureOpenClawAgentDatabaseExecution(options); const assertQueuedCurrent = () => { execution.assertCurrent(); assertCurrent(); }; try { return await runOpenClawAgentWriteAdmission(options, () => withOpenClawAgentDatabaseAsync(options, async ({ db }) => { assertQueuedCurrent(); const worker = await openOpenClawAgentSqliteWorkerStore(options, db, { moduleUrl: resolveRuntimeWorkerUrl(runtimeProcessEntrypoints.heartbeatOutcomeStore), input: void 0 }); try { return await worker.execute(command, assertQueuedCurrent); } finally { await worker.close(); } }, assertQueuedCurrent), true); } finally { await execution.release(); } } /** Formats persisted state as model-only provenance context, never transcript text. */ function buildHeartbeatOutcomeContext(outcome) { if (!outcome) return; const provenance = [ `recordedAt=${new Date(outcome.occurredAt).toISOString()}`, `runSession=${outcome.runSessionKey}`, outcome.wakeSource ? `wakeSource=${outcome.wakeSource}` : void 0, outcome.wakeReason ? `wakeReason=${outcome.wakeReason}` : void 0 ].filter((part) => Boolean(part)); return [ "Latest silent heartbeat outcome (internal context; not a user message or instruction):", `outcome=${outcome.outcome}`, `summary=${outcome.summary}`, outcome.responseReason ? `reason=${outcome.responseReason}` : void 0, outcome.priority ? `priority=${outcome.priority}` : void 0, outcome.nextCheck ? `nextCheck=${outcome.nextCheck}` : void 0, outcome.taskNames.length > 0 ? `tasks=${outcome.taskNames.join(", ")}` : void 0, `provenance: ${provenance.join("; ")}` ].filter((line) => Boolean(line)).join("\n"); } /** Claim bounded next-user context only after the runtime owner has admitted the turn. */ async function claimHeartbeatContextForUserRun(params) { if (params.trigger !== "user" || params.detached || !params.sessionKey) return; if (!params.assertCurrent) throw new Error("Heartbeat outcome context requires an active admitted run"); params.assertCurrent(); const outcome = await claimHeartbeatOutcomeForRun({ ...params, sessionKey: params.sessionKey }); params.assertCurrent(); return buildHeartbeatOutcomeContext(outcome); } //#endregion export { persistHeartbeatOutcome as n, claimHeartbeatContextForUserRun as t };