openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
200 lines (199 loc) • 9.31 kB
JavaScript
import { t as safeParseJson } from "./json-coercion-pfKbZnCJ.mjs";
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.mjs";
import { i as resolveRuntimeWorkerUrl } from "./runtime-worker-url-CFqA9Dvi.mjs";
import { r as resolveStateDir } from "./state-dir-CYkBUd_G.mjs";
import { n as runtimeProcessEntrypoints } from "./runtime-process-entrypoints-De68tE_W.mjs";
import { i as isIncognitoOpenClawAgentSqlitePath, l as resolveOpenClawAgentSqlitePath } from "./openclaw-agent-db.paths-BbmZlEP5.mjs";
import { r as cloneEnvWithPlatformSemantics } from "./config-env-vars-BzUE_dOh.mjs";
import { c as runOpenClawAgentWriteTransaction, d as withOpenClawAgentDatabaseRuntime } from "./openclaw-agent-db-DWI9Bgur.mjs";
import { n as runOpenClawAgentWriteAdmission } from "./openclaw-agent-write-admission-CXJiE5VB.mjs";
import { i as toDatabaseOptions } from "./session-accessor.sqlite-scope-helpers-CfDTAEsL.mjs";
import { n as captureOpenClawAgentDatabaseExecution } from "./openclaw-agent-execution-C2B2fucX.mjs";
import { n as openOpenClawAgentSqliteWorkerStore } from "./openclaw-agent-worker-store-BE9hhRoh.mjs";
import { s as resolveSqliteScope } from "./session-accessor.sqlite-scope-BkgwxatT.mjs";
import { r as captureIncognitoSessionOperation } from "./session-incognito-binding-Dp6RTei2.mjs";
import { n as persistHeartbeatOutcomeInDatabase, t as claimHeartbeatOutcomeRowInDatabase } from "./heartbeat-outcome-store.kernel-DXTdhUwk.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) {
const parsed = safeParseJson(value ?? "");
return Array.isArray(parsed) ? normalizeTaskNames(parsed.filter((item) => typeof item === "string")) : [];
}
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 { assertCurrent } = params;
const incognito = params.incognito ?? captureIncognitoSessionOperation(params);
const row = await runHeartbeatOutcomeOperation({
...params,
incognito
}, {
type: "claim",
input: {
sessionKey: params.sessionKey,
runId: params.runId
}
}, assertCurrent);
if (incognito) {
assertCurrent?.();
incognito.authority.assertCurrent();
incognito.actor.assertReadable();
}
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
})
};
const incognito = params.incognito ?? captureIncognitoSessionOperation(params);
if (incognito) {
const { actor, authority } = incognito;
if (actor.agentId !== options.agentId || actor.path !== options.path) throw new Error("Heartbeat outcome target differs from its captured incognito actor");
const claim = actor.sessions.captureCurrent(params.sessionKey);
const expected = actor.sessions.readSharing(params.sessionKey)?.entry;
const current = {
assertCurrent() {
assertCurrent();
authority.assertCurrent();
actor.assertCurrent();
},
authorize(stage, facts) {
if (facts.sharing?.entry?.sessionId !== expected?.sessionId || facts.sharing?.entry?.lifecycleRevision !== expected?.lifecycleRevision) throw new Error("Heartbeat outcome session changed; retry");
return authority.authorize?.(stage, facts);
}
};
current.assertCurrent();
return actor.sessions.withSharedState(async () => {
const result = await actor.sessions.sideData(current, {
type: `session.heartbeat.${command.type}`,
input: command.input
});
current.assertCurrent();
claim.assertCurrent();
return result;
});
}
if (isIncognitoOpenClawAgentSqlitePath(options.path, options)) return runOpenClawAgentWriteAdmission(options, () => runOpenClawAgentWriteTransaction(({ db }) => {
assertCurrent();
return command.type === "persist" ? persistHeartbeatOutcomeInDatabase(db, command.input) : 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, () => withOpenClawAgentDatabaseRuntime(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;
const { assertCurrent } = params;
const incognito = params.incognito ?? captureIncognitoSessionOperation(params);
if (!assertCurrent) throw new Error("Heartbeat outcome context requires an active admitted run");
assertCurrent();
const outcome = await claimHeartbeatOutcomeForRun({
...params,
sessionKey: params.sessionKey,
incognito
});
assertCurrent();
incognito?.authority.assertCurrent();
incognito?.actor.assertReadable();
return buildHeartbeatOutcomeContext(outcome);
}
//#endregion
export { persistHeartbeatOutcome as n, claimHeartbeatContextForUserRun as t };