UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

120 lines (119 loc) 5.96 kB
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js"; import { a as getNodeSqliteKysely, r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js"; import { g as runOpenClawAgentWriteTransaction } from "./openclaw-agent-db-CWtDoRbC.js"; import { c as resolveSqliteScope, m as toDatabaseOptions } from "./session-accessor.sqlite-scope-2KfMzb44.js"; //#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. */ function persistHeartbeatOutcome(params) { if (params.response.notify || params.response.outcome === "no_change") return; const taskNames = normalizeTaskNames(params.taskNames ?? []); const values = { 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() }; runOpenClawAgentWriteTransaction(({ db }) => { const agentDb = getNodeSqliteKysely(db); executeSqliteQuerySync(db, agentDb.insertInto("heartbeat_outcomes").values(values).onConflict((conflict) => conflict.column("session_key").doUpdateSet({ run_session_key: values.run_session_key, outcome: values.outcome, summary: values.summary, response_reason: values.response_reason, priority: values.priority, next_check: values.next_check, task_names_json: values.task_names_json, wake_source: values.wake_source, wake_reason: values.wake_reason, occurred_at: values.occurred_at, context_run_id: null, context_claimed_at: null, updated_at: values.updated_at }))); }, toDatabaseOptions(resolveSqliteScope(params)), { operationLabel: "heartbeat.outcome.persist" }); } /** Claims the latest outcome for one user run while allowing that run's retries. */ function claimHeartbeatOutcomeForRun(params) { return runOpenClawAgentWriteTransaction(({ db }) => { const agentDb = getNodeSqliteKysely(db); const row = executeSqliteQuerySync(db, agentDb.selectFrom("heartbeat_outcomes").selectAll().where("session_key", "=", params.sessionKey)).rows[0]; if (!row || row.context_run_id !== null && row.context_run_id !== params.runId) return; if (row.context_run_id === null) { if (executeSqliteQuerySync(db, agentDb.updateTable("heartbeat_outcomes").set({ context_run_id: params.runId, context_claimed_at: Date.now() }).where("session_key", "=", params.sessionKey).where("context_run_id", "is", null)).numAffectedRows !== 1n) return; } return rowToOutcome(row); }, toDatabaseOptions(resolveSqliteScope(params)), { operationLabel: "heartbeat.outcome.claim" }); } /** 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"); } //#endregion export { claimHeartbeatOutcomeForRun as n, persistHeartbeatOutcome as r, buildHeartbeatOutcomeContext as t };