diff --git a/CHANGELOG.md b/CHANGELOG.md index 7624c4e06ae..13a0c323b4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2229,7 +2229,7 @@ Docs: https://docs.openclaw.ai - Memory/active-memory: default QMD recall to search and surface better search-path telemetry so memory-backed recall works more predictably out of the box. (#65068) Thanks @Takhoffman. - Docs/providers: expand bundled provider docs with richer capability, env-var, and setup guidance across provider pages. - Docs/memory-wiki: add the recommended QMD + bridge-mode hybrid recipe plus zero-artifact troubleshooting guidance for `memory-wiki` bridge setups. (#63165) Thanks @sercada and @vincentkoc. -- Agents/commitments: add default-on inferred follow-up commitments with hidden batched extraction, per-agent/per-channel scoping, heartbeat delivery, CLI management, and heartbeat-interval due-time clamping so magical check-ins do not echo immediately. (#74189) Thanks @vignesh07. +- Agents/commitments: add opt-in inferred follow-up commitments with hidden batched extraction, per-agent/per-channel scoping, heartbeat delivery, CLI management, a simple `commitments.enabled`/`commitments.maxPerDay` config, and heartbeat-interval due-time clamping so magical check-ins do not echo immediately. (#74189) Thanks @vignesh07. ### Fixes diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index 4cdcfecdc71..09211943e6f 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -1,4 +1,4 @@ -bc53a2242782d03e6392671c154481fb4cd8dc5b35cc41a69b056d3ead28be97 config-baseline.json -861a230a4e66cb8986270a85f63e857077506a3bc75ec3754dfebd17a3ea9f0c config-baseline.core.json +0316c2ceef9a2da29a8860ba8c8e5218249bc561c5b44202ac78faf16b56029f config-baseline.json +d6f6410e05b623412f086ba59d8caea82e691e2f1367090ec2ddabfc189381ed config-baseline.core.json 9f5fad66a49fa618d64a963470aa69fed9fe4b4639cc4321f9ec04bfb2f8aa50 config-baseline.channel.json c4231c2194206547af8ad94342dc00aadb734f43cb49cc79d4c46bdbb80c3f95 config-baseline.plugin.json diff --git a/src/commands/commitments.test.ts b/src/commands/commitments.test.ts index f563872a49a..f06f06252fc 100644 --- a/src/commands/commitments.test.ts +++ b/src/commands/commitments.test.ts @@ -9,7 +9,7 @@ const mocks = vi.hoisted(() => ({ resolveCommitmentStorePath: vi.fn(() => "/tmp/openclaw-commitments.json"), getRuntimeConfig: vi.fn(() => ({ commitments: { - store: "/tmp/openclaw-commitments.json", + enabled: true, }, })), })); diff --git a/src/commands/commitments.ts b/src/commands/commitments.ts index c3e7b09cef8..a6734acef20 100644 --- a/src/commands/commitments.ts +++ b/src/commands/commitments.ts @@ -107,7 +107,7 @@ export async function commitmentsListCommand( count: commitments.length, status: status ?? (opts.all ? null : "pending"), agentId: normalizeOptionalString(opts.agent) ?? null, - store: resolveCommitmentStorePath(cfg.commitments?.store), + store: resolveCommitmentStorePath(), commitments, }, null, @@ -118,7 +118,7 @@ export async function commitmentsListCommand( } runtime.log(info(`Commitments: ${commitments.length}`)); - runtime.log(info(`Store: ${resolveCommitmentStorePath(cfg.commitments?.store)}`)); + runtime.log(info(`Store: ${resolveCommitmentStorePath()}`)); if (status) { runtime.log(info(`Status filter: ${status}`)); } diff --git a/src/commands/doctor-config-flow.test.ts b/src/commands/doctor-config-flow.test.ts index e7f961a85c1..110d80b16ac 100644 --- a/src/commands/doctor-config-flow.test.ts +++ b/src/commands/doctor-config-flow.test.ts @@ -1621,18 +1621,7 @@ describe("doctor config flow", () => { config: { commitments: { enabled: true, - categories: { - careCheckIns: "gentle", - eventCheckIns: false, - }, - extraction: { - enabled: true, - batchMaxItems: 4, - }, - delivery: { - maxPerHeartbeat: 2, - expireAfterHours: 48, - }, + maxPerDay: 2, }, }, run: loadAndMaybeMigrateDoctorConfig, @@ -1640,18 +1629,7 @@ describe("doctor config flow", () => { expect(result.cfg.commitments).toEqual({ enabled: true, - categories: { - careCheckIns: "gentle", - eventCheckIns: false, - }, - extraction: { - enabled: true, - batchMaxItems: 4, - }, - delivery: { - maxPerHeartbeat: 2, - expireAfterHours: 48, - }, + maxPerDay: 2, }); }); diff --git a/src/commitments/config.ts b/src/commitments/config.ts index 8fea8a202b8..6c54cd2e33b 100644 --- a/src/commitments/config.ts +++ b/src/commitments/config.ts @@ -1,6 +1,5 @@ import { resolveUserTimezone } from "../agents/date-time.js"; import type { OpenClawConfig } from "../config/config.js"; -import type { CommitmentsConfig } from "../config/types.commitments.js"; export const DEFAULT_COMMITMENT_EXTRACTION_DEBOUNCE_MS = 15_000; export const DEFAULT_COMMITMENT_BATCH_MAX_ITEMS = 8; @@ -9,29 +8,18 @@ export const DEFAULT_COMMITMENT_CARE_CONFIDENCE_THRESHOLD = 0.86; export const DEFAULT_COMMITMENT_EXTRACTION_TIMEOUT_SECONDS = 45; export const DEFAULT_COMMITMENT_MAX_PER_HEARTBEAT = 3; export const DEFAULT_COMMITMENT_EXPIRE_AFTER_HOURS = 72; +export const DEFAULT_COMMITMENT_MAX_PER_DAY = 3; export type ResolvedCommitmentsConfig = { enabled: boolean; - store?: string; - categories: { - eventCheckIns: boolean; - deadlineCheckIns: boolean; - openLoops: boolean; - careCheckIns: false | "gentle" | true; - }; + maxPerDay: number; extraction: { - enabled: boolean; - model?: string; debounceMs: number; batchMaxItems: number; confidenceThreshold: number; careConfidenceThreshold: number; timeoutSeconds: number; }; - delivery: { - maxPerHeartbeat: number; - expireAfterHours: number; - }; }; function positiveInt(value: unknown, fallback: number): number { @@ -40,63 +28,17 @@ function positiveInt(value: unknown, fallback: number): number { : fallback; } -function nonnegativeNumber(value: unknown, fallback: number): number { - return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback; -} - -function resolveCareCheckIns( - value: CommitmentsConfig["categories"] | undefined, -): false | "gentle" | true { - const raw = value?.careCheckIns; - if (raw === false) { - return false; - } - if (raw === true) { - return true; - } - return "gentle"; -} - export function resolveCommitmentsConfig(cfg?: OpenClawConfig): ResolvedCommitmentsConfig { const raw = cfg?.commitments; - const extraction = raw?.extraction; - const delivery = raw?.delivery; return { - enabled: raw?.enabled !== false, - store: raw?.store, - categories: { - eventCheckIns: raw?.categories?.eventCheckIns !== false, - deadlineCheckIns: raw?.categories?.deadlineCheckIns !== false, - openLoops: raw?.categories?.openLoops !== false, - careCheckIns: resolveCareCheckIns(raw?.categories), - }, + enabled: raw?.enabled === true, + maxPerDay: positiveInt(raw?.maxPerDay, DEFAULT_COMMITMENT_MAX_PER_DAY), extraction: { - enabled: extraction?.enabled !== false, - model: extraction?.model?.trim() || undefined, - debounceMs: nonnegativeNumber( - extraction?.debounceMs, - DEFAULT_COMMITMENT_EXTRACTION_DEBOUNCE_MS, - ), - batchMaxItems: positiveInt(extraction?.batchMaxItems, DEFAULT_COMMITMENT_BATCH_MAX_ITEMS), - confidenceThreshold: nonnegativeNumber( - extraction?.confidenceThreshold, - DEFAULT_COMMITMENT_CONFIDENCE_THRESHOLD, - ), - careConfidenceThreshold: nonnegativeNumber( - extraction?.careConfidenceThreshold, - DEFAULT_COMMITMENT_CARE_CONFIDENCE_THRESHOLD, - ), - timeoutSeconds: positiveInt( - extraction?.timeoutSeconds, - DEFAULT_COMMITMENT_EXTRACTION_TIMEOUT_SECONDS, - ), - }, - delivery: { - maxPerHeartbeat: positiveInt(delivery?.maxPerHeartbeat, DEFAULT_COMMITMENT_MAX_PER_HEARTBEAT), - expireAfterHours: positiveInt( - delivery?.expireAfterHours, - DEFAULT_COMMITMENT_EXPIRE_AFTER_HOURS, - ), + debounceMs: DEFAULT_COMMITMENT_EXTRACTION_DEBOUNCE_MS, + batchMaxItems: DEFAULT_COMMITMENT_BATCH_MAX_ITEMS, + confidenceThreshold: DEFAULT_COMMITMENT_CONFIDENCE_THRESHOLD, + careConfidenceThreshold: DEFAULT_COMMITMENT_CARE_CONFIDENCE_THRESHOLD, + timeoutSeconds: DEFAULT_COMMITMENT_EXTRACTION_TIMEOUT_SECONDS, }, }; } diff --git a/src/commitments/extraction.test.ts b/src/commitments/extraction.test.ts index 5505ff016f3..660b96a7d6b 100644 --- a/src/commitments/extraction.test.ts +++ b/src/commitments/extraction.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { buildCommitmentExtractionPrompt, @@ -17,6 +17,7 @@ describe("commitment extraction", () => { const nowMs = Date.parse("2026-04-29T16:00:00.000Z"); afterEach(async () => { + vi.unstubAllEnvs(); await Promise.all(tmpDirs.map((dir) => fs.rm(dir, { recursive: true, force: true }))); tmpDirs.length = 0; }); @@ -24,9 +25,10 @@ describe("commitment extraction", () => { async function createConfig(): Promise { const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-commitments-")); tmpDirs.push(tmpDir); + vi.stubEnv("OPENCLAW_STATE_DIR", tmpDir); return { commitments: { - store: path.join(tmpDir, "commitments.json"), + enabled: true, }, }; } @@ -103,26 +105,14 @@ describe("commitment extraction", () => { }); it("rejects disabled, low-confidence, and non-future candidates", () => { - const cfg: OpenClawConfig = { - commitments: { - categories: { careCheckIns: "gentle" }, - extraction: { confidenceThreshold: 0.8, careConfidenceThreshold: 0.9 }, - }, - }; + const cfg: OpenClawConfig = { commitments: { enabled: true } }; const valid = validateCommitmentCandidates({ cfg, items: [item()], result: { candidates: [ candidate(), - candidate({ - kind: "care_check_in", - sensitivity: "care", - reason: "The user said they were tired.", - suggestedText: "Hope you got some rest.", - dedupeKey: "sleep:2026-04-29", - confidence: 0.82, - }), + candidate({ dedupeKey: "low-confidence", confidence: 0.5 }), candidate({ dedupeKey: "past", dueWindow: { earliest: "2026-04-29T15:00:00.000Z" }, @@ -186,7 +176,7 @@ describe("commitment extraction", () => { }, nowMs: nowMs + 1_000, }); - const store = await loadCommitmentStore(cfg.commitments?.store); + const store = await loadCommitmentStore(); expect(created).toHaveLength(1); expect(deduped).toHaveLength(0); diff --git a/src/commitments/extraction.ts b/src/commitments/extraction.ts index 44d2c4d99b4..7a751baceb8 100644 --- a/src/commitments/extraction.ts +++ b/src/commitments/extraction.ts @@ -3,11 +3,7 @@ import type { OpenClawConfig } from "../config/config.js"; import { resolveHeartbeatIntervalMs } from "../infra/heartbeat-summary.js"; import { isRecord } from "../utils.js"; import { resolveCommitmentsConfig } from "./config.js"; -import { - isCommitmentKindEnabled, - listPendingCommitmentsForScope, - upsertInferredCommitments, -} from "./store.js"; +import { listPendingCommitmentsForScope, upsertInferredCommitments } from "./store.js"; import type { CommitmentCandidate, CommitmentExtractionBatchResult, @@ -198,8 +194,6 @@ export function buildCommitmentExtractionPrompt(params: { cfg?: OpenClawConfig; items: CommitmentExtractionItem[]; }): string { - const resolved = resolveCommitmentsConfig(params.cfg); - const categoryConfig = JSON.stringify(resolved.categories); const items = params.items.map((item) => ({ itemId: item.itemId, now: new Date(item.nowMs).toISOString(), @@ -212,7 +206,7 @@ export function buildCommitmentExtractionPrompt(params: { Create inferred follow-up commitments only. Exact user requests such as "remind me tomorrow", "schedule this", or "check in at 3" belong to cron/reminders and must be skipped. -Use these categories: ${categoryConfig} +Use these categories: event_check_in, deadline_check, care_check_in, open_loop. Create a candidate only when the latest exchange creates a useful future check-in opportunity that the user did not explicitly schedule. Prefer no candidate over weak candidates. @@ -279,7 +273,7 @@ export function validateCommitmentCandidates(params: { }> = []; for (const candidate of params.result.candidates) { const item = itemsById.get(candidate.itemId); - if (!item || !isCommitmentKindEnabled(candidate.kind, resolved.categories)) { + if (!item) { continue; } const threshold = diff --git a/src/commitments/runtime.test.ts b/src/commitments/runtime.test.ts index 69fde2608c8..e043fb430fa 100644 --- a/src/commitments/runtime.test.ts +++ b/src/commitments/runtime.test.ts @@ -18,6 +18,7 @@ describe("commitment extraction runtime", () => { afterEach(async () => { resetCommitmentExtractionRuntimeForTests(); + vi.unstubAllEnvs(); await Promise.all(tmpDirs.map((dir) => fs.rm(dir, { recursive: true, force: true }))); tmpDirs.length = 0; }); @@ -25,13 +26,10 @@ describe("commitment extraction runtime", () => { async function createConfig(): Promise { const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-commitment-runtime-")); tmpDirs.push(tmpDir); + vi.stubEnv("OPENCLAW_STATE_DIR", tmpDir); return { commitments: { - store: path.join(tmpDir, "commitments.json"), - extraction: { - debounceMs: 1_000, - batchMaxItems: 8, - }, + enabled: true, }, }; } @@ -52,6 +50,29 @@ describe("commitment extraction runtime", () => { ).toBe(false); }); + it("keeps hidden extraction opt-in by default", () => { + const cfg: OpenClawConfig = { + commitments: {}, + }; + configureCommitmentExtractionRuntime({ + forceInTests: true, + setTimer: () => ({ unref() {} }) as ReturnType, + clearTimer: () => undefined, + }); + + expect( + enqueueCommitmentExtraction({ + cfg, + nowMs, + agentId: "main", + sessionKey: "agent:main:telegram:user-1", + channel: "telegram", + userText: "Interview tomorrow.", + assistantText: "Good luck.", + }), + ).toBe(false); + }); + it("micro-batches queued turns into one extractor call", async () => { const cfg = await createConfig(); const extractBatch = vi.fn(async ({ items }: { items: CommitmentExtractionItem[] }) => ({ @@ -106,7 +127,7 @@ describe("commitment extraction runtime", () => { ).toBe(true); await expect(drainCommitmentExtractionQueue()).resolves.toBe(2); - const store = await loadCommitmentStore(cfg.commitments?.store); + const store = await loadCommitmentStore(); expect(extractBatch).toHaveBeenCalledTimes(1); const batchItems = extractBatch.mock.calls[0]?.[0].items; diff --git a/src/commitments/runtime.ts b/src/commitments/runtime.ts index d7b8e5e7797..14c07358d92 100644 --- a/src/commitments/runtime.ts +++ b/src/commitments/runtime.ts @@ -1,11 +1,6 @@ import { randomUUID } from "node:crypto"; import path from "node:path"; import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js"; -import { - buildModelAliasIndex, - resolveDefaultModelForAgent, - resolveModelRefFromString, -} from "../agents/model-selection.js"; import { runEmbeddedPiAgent, type EmbeddedPiRunResult } from "../agents/pi-embedded.js"; import type { OpenClawConfig } from "../config/config.js"; import { resolveStateDir } from "../config/paths.js"; @@ -100,7 +95,6 @@ export function enqueueCommitmentExtraction(input: CommitmentExtractionEnqueueIn const resolved = resolveCommitmentsConfig(input.cfg); if ( !resolved.enabled || - !resolved.extraction.enabled || shouldDisableBackgroundExtractionForTests() || !isUsefulText(input.userText) || !isUsefulText(input.assistantText) || @@ -170,18 +164,6 @@ async function defaultExtractBatch(params: { } const resolved = resolveCommitmentsConfig(cfg); const runId = `commitments-${randomUUID()}`; - const modelFallback = resolveDefaultModelForAgent({ cfg: cfg ?? {}, agentId: first.agentId }); - const aliasIndex = buildModelAliasIndex({ - cfg: cfg ?? {}, - defaultProvider: modelFallback.provider, - }); - const modelRef = resolved.extraction.model - ? resolveModelRefFromString({ - raw: resolved.extraction.model, - defaultProvider: modelFallback.provider, - aliasIndex, - })?.ref - : undefined; const result = await runEmbeddedPiAgent({ sessionId: runId, sessionKey: `agent:${first.agentId}:commitments:${runId}`, @@ -192,8 +174,6 @@ async function defaultExtractBatch(params: { config: cfg, prompt: buildCommitmentExtractionPrompt({ cfg, items: params.items }), disableTools: true, - provider: modelRef?.provider, - model: modelRef?.model, thinkLevel: "off", verboseLevel: "off", reasoningLevel: "off", diff --git a/src/commitments/store.test.ts b/src/commitments/store.test.ts new file mode 100644 index 00000000000..78ff05bfa61 --- /dev/null +++ b/src/commitments/store.test.ts @@ -0,0 +1,92 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { listDueCommitmentsForSession, loadCommitmentStore, saveCommitmentStore } from "./store.js"; +import type { CommitmentRecord } from "./types.js"; + +describe("commitment store delivery selection", () => { + const tmpDirs: string[] = []; + const nowMs = Date.parse("2026-04-29T17:00:00.000Z"); + const sessionKey = "agent:main:telegram:user-155462274"; + + afterEach(async () => { + vi.unstubAllEnvs(); + await Promise.all(tmpDirs.map((dir) => fs.rm(dir, { recursive: true, force: true }))); + tmpDirs.length = 0; + }); + + async function useTempStateDir(): Promise { + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-commitments-store-")); + tmpDirs.push(tmpDir); + vi.stubEnv("OPENCLAW_STATE_DIR", tmpDir); + } + + function commitment(overrides?: Partial): CommitmentRecord { + return { + id: "cm_interview", + agentId: "main", + sessionKey, + channel: "telegram", + to: "155462274", + kind: "event_check_in", + sensitivity: "routine", + source: "inferred_user_context", + status: "pending", + reason: "The user said they had an interview yesterday.", + suggestedText: "How did the interview go?", + dedupeKey: "interview:2026-04-28", + confidence: 0.92, + dueWindow: { + earliestMs: nowMs - 60_000, + latestMs: nowMs + 60 * 60_000, + timezone: "America/Los_Angeles", + }, + sourceUserText: "I have an interview tomorrow.", + createdAtMs: nowMs - 24 * 60 * 60_000, + updatedAtMs: nowMs - 24 * 60 * 60_000, + attempts: 0, + ...overrides, + }; + } + + it("does not surface due commitments unless inferred commitments are enabled", async () => { + await useTempStateDir(); + await saveCommitmentStore(undefined, { + version: 1, + commitments: [commitment()], + }); + + await expect( + listDueCommitmentsForSession({ + cfg: {}, + agentId: "main", + sessionKey, + nowMs, + }), + ).resolves.toEqual([]); + }); + + it("limits delivered commitments per agent session in a rolling day", async () => { + await useTempStateDir(); + await saveCommitmentStore(undefined, { + version: 1, + commitments: [ + commitment({ id: "cm_sent", status: "sent", sentAtMs: nowMs - 60_000 }), + commitment({ id: "cm_pending", dedupeKey: "interview:followup" }), + ], + }); + + await expect( + listDueCommitmentsForSession({ + cfg: { commitments: { enabled: true, maxPerDay: 1 } }, + agentId: "main", + sessionKey, + nowMs, + }), + ).resolves.toEqual([]); + + const store = await loadCommitmentStore(); + expect(store.commitments).toHaveLength(2); + }); +}); diff --git a/src/commitments/store.ts b/src/commitments/store.ts index 292df30c09b..380a5c487fb 100644 --- a/src/commitments/store.ts +++ b/src/commitments/store.ts @@ -4,11 +4,14 @@ import path from "node:path"; import type { OpenClawConfig } from "../config/config.js"; import { resolveStateDir } from "../config/paths.js"; import { expandHomePrefix } from "../infra/home-dir.js"; -import { resolveCommitmentsConfig } from "./config.js"; +import { + DEFAULT_COMMITMENT_EXPIRE_AFTER_HOURS, + DEFAULT_COMMITMENT_MAX_PER_HEARTBEAT, + resolveCommitmentsConfig, +} from "./config.js"; import type { CommitmentCandidate, CommitmentExtractionItem, - CommitmentKind, CommitmentRecord, CommitmentScope, CommitmentStatus, @@ -16,6 +19,7 @@ import type { } from "./types.js"; const STORE_VERSION = 1 as const; +const ROLLING_DAY_MS = 24 * 60 * 60 * 1000; function defaultCommitmentStorePath(): string { return path.join(resolveStateDir(), "commitments", "commitments.json"); @@ -192,8 +196,7 @@ export async function listPendingCommitmentsForScope(params: { nowMs?: number; limit?: number; }): Promise { - const resolved = resolveCommitmentsConfig(params.cfg); - const store = await loadCommitmentStore(resolved.store); + const store = await loadCommitmentStore(); const scopeKey = buildCommitmentScopeKey(params.scope); const nowMs = params.nowMs ?? Date.now(); const limit = params.limit ?? 20; @@ -224,8 +227,7 @@ export async function upsertInferredCommitments(params: { if (params.candidates.length === 0) { return []; } - const resolved = resolveCommitmentsConfig(params.cfg); - const store = await loadCommitmentStore(resolved.store); + const store = await loadCommitmentStore(); const nowMs = params.nowMs ?? Date.now(); const created: CommitmentRecord[] = []; const scopeKey = buildCommitmentScopeKey(params.item); @@ -265,10 +267,26 @@ export async function upsertInferredCommitments(params: { store.commitments.push(record); created.push(record); } - await saveCommitmentStore(resolved.store, store); + await saveCommitmentStore(undefined, store); return created; } +function countSentCommitmentsForSession(params: { + store: CommitmentStoreFile; + agentId: string; + sessionKey: string; + nowMs: number; +}): number { + const sinceMs = params.nowMs - ROLLING_DAY_MS; + return params.store.commitments.filter( + (commitment) => + commitment.agentId === params.agentId && + commitment.sessionKey === params.sessionKey && + commitment.status === "sent" && + (commitment.sentAtMs ?? 0) >= sinceMs, + ).length; +} + export async function listDueCommitmentsForSession(params: { cfg?: OpenClawConfig; agentId: string; @@ -280,10 +298,25 @@ export async function listDueCommitmentsForSession(params: { if (!resolved.enabled) { return []; } - const store = await loadCommitmentStore(resolved.store); + const store = await loadCommitmentStore(); const nowMs = params.nowMs ?? Date.now(); - const limit = params.limit ?? resolved.delivery.maxPerHeartbeat; - const expireAfterMs = resolved.delivery.expireAfterHours * 60 * 60 * 1000; + const remainingToday = + resolved.maxPerDay - + countSentCommitmentsForSession({ + store, + agentId: params.agentId, + sessionKey: params.sessionKey, + nowMs, + }); + if (remainingToday <= 0) { + return []; + } + const limit = Math.min( + params.limit ?? DEFAULT_COMMITMENT_MAX_PER_HEARTBEAT, + remainingToday, + DEFAULT_COMMITMENT_MAX_PER_HEARTBEAT, + ); + const expireAfterMs = DEFAULT_COMMITMENT_EXPIRE_AFTER_HOURS * 60 * 60 * 1000; return store.commitments .filter( (commitment) => @@ -310,9 +343,9 @@ export async function listDueCommitmentSessionKeys(params: { if (!resolved.enabled) { return []; } - const store = await loadCommitmentStore(resolved.store); + const store = await loadCommitmentStore(); const nowMs = params.nowMs ?? Date.now(); - const expireAfterMs = resolved.delivery.expireAfterHours * 60 * 60 * 1000; + const expireAfterMs = DEFAULT_COMMITMENT_EXPIRE_AFTER_HOURS * 60 * 60 * 1000; const keys = new Set(); for (const commitment of store.commitments) { if ( @@ -320,7 +353,13 @@ export async function listDueCommitmentSessionKeys(params: { isActiveStatus(commitment.status) && commitment.dueWindow.earliestMs <= nowMs && commitment.dueWindow.latestMs + expireAfterMs >= nowMs && - (commitment.status !== "snoozed" || (commitment.snoozedUntilMs ?? 0) <= nowMs) + (commitment.status !== "snoozed" || (commitment.snoozedUntilMs ?? 0) <= nowMs) && + countSentCommitmentsForSession({ + store, + agentId: params.agentId, + sessionKey: commitment.sessionKey, + nowMs, + }) < resolved.maxPerDay ) { keys.add(commitment.sessionKey); } @@ -339,10 +378,9 @@ export async function markCommitmentsAttempted(params: { if (params.ids.length === 0) { return; } - const resolved = resolveCommitmentsConfig(params.cfg); const idSet = new Set(params.ids); const nowMs = params.nowMs ?? Date.now(); - const store = await loadCommitmentStore(resolved.store); + const store = await loadCommitmentStore(); let changed = false; store.commitments = store.commitments.map((commitment) => { if (!idSet.has(commitment.id)) { @@ -357,7 +395,7 @@ export async function markCommitmentsAttempted(params: { }; }); if (changed) { - await saveCommitmentStore(resolved.store, store); + await saveCommitmentStore(undefined, store); } } @@ -370,10 +408,9 @@ export async function markCommitmentsStatus(params: { if (params.ids.length === 0) { return; } - const resolved = resolveCommitmentsConfig(params.cfg); const idSet = new Set(params.ids); const nowMs = params.nowMs ?? Date.now(); - const store = await loadCommitmentStore(resolved.store); + const store = await loadCommitmentStore(); let changed = false; store.commitments = store.commitments.map((commitment) => { if (!idSet.has(commitment.id) || !isActiveStatus(commitment.status)) { @@ -390,7 +427,7 @@ export async function markCommitmentsStatus(params: { }; }); if (changed) { - await saveCommitmentStore(resolved.store, store); + await saveCommitmentStore(undefined, store); } } @@ -399,8 +436,7 @@ export async function listCommitments(params?: { status?: CommitmentStatus; agentId?: string; }): Promise { - const resolved = resolveCommitmentsConfig(params?.cfg); - const store = await loadCommitmentStore(resolved.store); + const store = await loadCommitmentStore(); return store.commitments .filter( (commitment) => @@ -411,19 +447,3 @@ export async function listCommitments(params?: { (a, b) => a.dueWindow.earliestMs - b.dueWindow.earliestMs || a.createdAtMs - b.createdAtMs, ); } - -export function isCommitmentKindEnabled( - kind: CommitmentKind, - categories: ReturnType["categories"], -): boolean { - switch (kind) { - case "event_check_in": - return categories.eventCheckIns; - case "deadline_check": - return categories.deadlineCheckIns; - case "open_loop": - return categories.openLoops; - case "care_check_in": - return categories.careCheckIns !== false; - } -} diff --git a/src/config/schema.base.generated.ts b/src/config/schema.base.generated.ts index c7691ca557a..28640ca3bc4 100644 --- a/src/config/schema.base.generated.ts +++ b/src/config/schema.base.generated.ts @@ -21073,145 +21073,21 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { type: "boolean", title: "Commitments Enabled", description: - "Global inferred commitment feature gate. Set false to disable background extraction, storage, and heartbeat delivery for inferred follow-ups.", + "Enable hidden LLM extraction, storage, and heartbeat delivery for inferred follow-up commitments. Default: false.", }, - store: { - type: "string", - title: "Commitments Store Path", + maxPerDay: { + type: "integer", + exclusiveMinimum: 0, + maximum: 9007199254740991, + title: "Commitments per Day", description: - "Optional JSON store path for inferred commitments. Leave unset to use the default OpenClaw state directory store.", - }, - categories: { - type: "object", - properties: { - eventCheckIns: { - type: "boolean", - title: "Event Check-ins", - description: - "Enables inferred event check-ins such as asking how an interview or appointment went. Default: true.", - }, - deadlineCheckIns: { - type: "boolean", - title: "Deadline Check-ins", - description: - "Enables inferred deadline or progress check-ins for work the user expects to revisit. Default: true.", - }, - openLoops: { - type: "boolean", - title: "Open-loop Check-ins", - description: - "Enables inferred open-loop check-ins when the user is waiting on an outcome or unresolved next step. Default: true.", - }, - careCheckIns: { - anyOf: [ - { - type: "boolean", - }, - { - type: "string", - const: "gentle", - }, - ], - title: "Care Check-ins", - description: - 'Controls personal care check-ins. Use "gentle" for conservative care follow-ups, true for normal extraction, or false to disable them.', - }, - }, - additionalProperties: false, - title: "Commitment Categories", - description: - "Category gates for inferred commitments such as event check-ins, deadline progress, open loops, and care check-ins. Use these to narrow what OpenClaw infers while keeping the system enabled.", - }, - extraction: { - type: "object", - properties: { - enabled: { - type: "boolean", - title: "Commitment Extraction Enabled", - description: - "Enables hidden background LLM extraction for inferred commitments. Set false to keep stored commitments deliverable while preventing new inferred commitments.", - }, - model: { - type: "string", - title: "Commitment Extraction Model", - description: - "Optional provider/model override for hidden commitment extraction runs. Leave unset to use the active agent model.", - }, - debounceMs: { - type: "integer", - minimum: 0, - maximum: 9007199254740991, - title: "Commitment Extraction Debounce (ms)", - description: - "Milliseconds to wait before draining queued conversation turns into a batched hidden extraction run. Default: 15000.", - }, - batchMaxItems: { - type: "integer", - exclusiveMinimum: 0, - maximum: 9007199254740991, - title: "Commitment Extraction Batch Size", - description: - "Maximum queued turn extractions sent in one hidden model call. Default: 8.", - }, - confidenceThreshold: { - type: "number", - minimum: 0, - maximum: 1, - title: "Commitment Confidence Threshold", - description: - "Minimum accepted confidence from the extractor for routine inferred commitments. Default: 0.72.", - }, - careConfidenceThreshold: { - type: "number", - minimum: 0, - maximum: 1, - title: "Care Commitment Confidence Threshold", - description: - "Minimum accepted confidence from the extractor for personal care check-ins. Default: 0.86.", - }, - timeoutSeconds: { - type: "integer", - exclusiveMinimum: 0, - maximum: 9007199254740991, - title: "Commitment Extraction Timeout (sec)", - description: - "Maximum runtime in seconds for a hidden extraction pass before it is abandoned. Default: 45.", - }, - }, - additionalProperties: false, - title: "Commitment Extraction", - description: - "Background extraction controls for the hidden LLM pass that creates inferred commitments without adding content to the conversation transcript.", - }, - delivery: { - type: "object", - properties: { - maxPerHeartbeat: { - type: "integer", - exclusiveMinimum: 0, - maximum: 9007199254740991, - title: "Commitments per Heartbeat", - description: - "Maximum due inferred commitments injected into one heartbeat turn. Default: 3.", - }, - expireAfterHours: { - type: "integer", - exclusiveMinimum: 0, - maximum: 9007199254740991, - title: "Commitment Expiration (hours)", - description: - "Number of hours after the due time before a pending inferred commitment expires instead of being delivered. Default: 72.", - }, - }, - additionalProperties: false, - title: "Commitment Delivery", - description: "Heartbeat delivery controls for due inferred commitments.", + "Maximum inferred follow-up commitments delivered per agent session in a rolling day. Default: 3.", }, }, additionalProperties: false, title: "Commitments", description: - "Inferred follow-up commitment controls for automatically detecting check-ins from conversation turns and delivering them through heartbeat runs. Keep enabled for ambient follow-ups, or disable when you only want explicit reminders.", + "Inferred follow-up commitment controls for automatically detecting check-ins from conversation turns and delivering them through heartbeat runs.", }, hooks: { type: "object", @@ -24448,99 +24324,19 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { }, commitments: { label: "Commitments", - help: "Inferred follow-up commitment controls for automatically detecting check-ins from conversation turns and delivering them through heartbeat runs. Keep enabled for ambient follow-ups, or disable when you only want explicit reminders.", + help: "Inferred follow-up commitment controls for automatically detecting check-ins from conversation turns and delivering them through heartbeat runs.", tags: ["advanced"], }, "commitments.enabled": { label: "Commitments Enabled", - help: "Global inferred commitment feature gate. Set false to disable background extraction, storage, and heartbeat delivery for inferred follow-ups.", + help: "Enable hidden LLM extraction, storage, and heartbeat delivery for inferred follow-up commitments. Default: false.", tags: ["advanced"], }, - "commitments.store": { - label: "Commitments Store Path", - help: "Optional JSON store path for inferred commitments. Leave unset to use the default OpenClaw state directory store.", - tags: ["storage"], - }, - "commitments.categories": { - label: "Commitment Categories", - help: "Category gates for inferred commitments such as event check-ins, deadline progress, open loops, and care check-ins. Use these to narrow what OpenClaw infers while keeping the system enabled.", - tags: ["advanced"], - }, - "commitments.categories.eventCheckIns": { - label: "Event Check-ins", - help: "Enables inferred event check-ins such as asking how an interview or appointment went. Default: true.", - tags: ["advanced"], - }, - "commitments.categories.deadlineCheckIns": { - label: "Deadline Check-ins", - help: "Enables inferred deadline or progress check-ins for work the user expects to revisit. Default: true.", - tags: ["advanced"], - }, - "commitments.categories.openLoops": { - label: "Open-loop Check-ins", - help: "Enables inferred open-loop check-ins when the user is waiting on an outcome or unresolved next step. Default: true.", - tags: ["advanced"], - }, - "commitments.categories.careCheckIns": { - label: "Care Check-ins", - help: 'Controls personal care check-ins. Use "gentle" for conservative care follow-ups, true for normal extraction, or false to disable them.', - tags: ["advanced"], - }, - "commitments.extraction": { - label: "Commitment Extraction", - help: "Background extraction controls for the hidden LLM pass that creates inferred commitments without adding content to the conversation transcript.", - tags: ["advanced"], - }, - "commitments.extraction.enabled": { - label: "Commitment Extraction Enabled", - help: "Enables hidden background LLM extraction for inferred commitments. Set false to keep stored commitments deliverable while preventing new inferred commitments.", - tags: ["advanced"], - }, - "commitments.extraction.model": { - label: "Commitment Extraction Model", - help: "Optional provider/model override for hidden commitment extraction runs. Leave unset to use the active agent model.", - tags: ["models"], - }, - "commitments.extraction.debounceMs": { - label: "Commitment Extraction Debounce (ms)", - help: "Milliseconds to wait before draining queued conversation turns into a batched hidden extraction run. Default: 15000.", + "commitments.maxPerDay": { + label: "Commitments per Day", + help: "Maximum inferred follow-up commitments delivered per agent session in a rolling day. Default: 3.", tags: ["performance"], }, - "commitments.extraction.batchMaxItems": { - label: "Commitment Extraction Batch Size", - help: "Maximum queued turn extractions sent in one hidden model call. Default: 8.", - tags: ["performance"], - }, - "commitments.extraction.confidenceThreshold": { - label: "Commitment Confidence Threshold", - help: "Minimum accepted confidence from the extractor for routine inferred commitments. Default: 0.72.", - tags: ["advanced"], - }, - "commitments.extraction.careConfidenceThreshold": { - label: "Care Commitment Confidence Threshold", - help: "Minimum accepted confidence from the extractor for personal care check-ins. Default: 0.86.", - tags: ["advanced"], - }, - "commitments.extraction.timeoutSeconds": { - label: "Commitment Extraction Timeout (sec)", - help: "Maximum runtime in seconds for a hidden extraction pass before it is abandoned. Default: 45.", - tags: ["performance"], - }, - "commitments.delivery": { - label: "Commitment Delivery", - help: "Heartbeat delivery controls for due inferred commitments.", - tags: ["advanced"], - }, - "commitments.delivery.maxPerHeartbeat": { - label: "Commitments per Heartbeat", - help: "Maximum due inferred commitments injected into one heartbeat turn. Default: 3.", - tags: ["performance", "automation"], - }, - "commitments.delivery.expireAfterHours": { - label: "Commitment Expiration (hours)", - help: "Number of hours after the due time before a pending inferred commitment expires instead of being delivered. Default: 72.", - tags: ["advanced"], - }, "diagnostics.enabled": { label: "Diagnostics Enabled", help: "Master toggle for diagnostics instrumentation output in logs and telemetry wiring paths. Defaults to enabled; set false only in tightly constrained environments.", diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index 332cc2f3ad0..e65abfbf47c 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -191,42 +191,11 @@ export const FIELD_HELP: Record = { "acp.runtime.installCommand": "Optional operator install/setup command shown by `/acp install` and `/acp doctor` when ACP backend wiring is missing.", commitments: - "Inferred follow-up commitment controls for automatically detecting check-ins from conversation turns and delivering them through heartbeat runs. Keep enabled for ambient follow-ups, or disable when you only want explicit reminders.", + "Inferred follow-up commitment controls for automatically detecting check-ins from conversation turns and delivering them through heartbeat runs.", "commitments.enabled": - "Global inferred commitment feature gate. Set false to disable background extraction, storage, and heartbeat delivery for inferred follow-ups.", - "commitments.store": - "Optional JSON store path for inferred commitments. Leave unset to use the default OpenClaw state directory store.", - "commitments.categories": - "Category gates for inferred commitments such as event check-ins, deadline progress, open loops, and care check-ins. Use these to narrow what OpenClaw infers while keeping the system enabled.", - "commitments.categories.eventCheckIns": - "Enables inferred event check-ins such as asking how an interview or appointment went. Default: true.", - "commitments.categories.deadlineCheckIns": - "Enables inferred deadline or progress check-ins for work the user expects to revisit. Default: true.", - "commitments.categories.openLoops": - "Enables inferred open-loop check-ins when the user is waiting on an outcome or unresolved next step. Default: true.", - "commitments.categories.careCheckIns": - 'Controls personal care check-ins. Use "gentle" for conservative care follow-ups, true for normal extraction, or false to disable them.', - "commitments.extraction": - "Background extraction controls for the hidden LLM pass that creates inferred commitments without adding content to the conversation transcript.", - "commitments.extraction.enabled": - "Enables hidden background LLM extraction for inferred commitments. Set false to keep stored commitments deliverable while preventing new inferred commitments.", - "commitments.extraction.model": - "Optional provider/model override for hidden commitment extraction runs. Leave unset to use the active agent model.", - "commitments.extraction.debounceMs": - "Milliseconds to wait before draining queued conversation turns into a batched hidden extraction run. Default: 15000.", - "commitments.extraction.batchMaxItems": - "Maximum queued turn extractions sent in one hidden model call. Default: 8.", - "commitments.extraction.confidenceThreshold": - "Minimum accepted confidence from the extractor for routine inferred commitments. Default: 0.72.", - "commitments.extraction.careConfidenceThreshold": - "Minimum accepted confidence from the extractor for personal care check-ins. Default: 0.86.", - "commitments.extraction.timeoutSeconds": - "Maximum runtime in seconds for a hidden extraction pass before it is abandoned. Default: 45.", - "commitments.delivery": "Heartbeat delivery controls for due inferred commitments.", - "commitments.delivery.maxPerHeartbeat": - "Maximum due inferred commitments injected into one heartbeat turn. Default: 3.", - "commitments.delivery.expireAfterHours": - "Number of hours after the due time before a pending inferred commitment expires instead of being delivered. Default: 72.", + "Enable hidden LLM extraction, storage, and heartbeat delivery for inferred follow-up commitments. Default: false.", + "commitments.maxPerDay": + "Maximum inferred follow-up commitments delivered per agent session in a rolling day. Default: 3.", "agents.list.*.skills": "Optional allowlist of skills for this agent. If omitted, the agent inherits agents.defaults.skills when set; otherwise skills stay unrestricted. Set [] for no skills. An explicit list fully replaces inherited defaults instead of merging with them.", "agents.list[].skills": diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index e421a3879c6..89001dc9ac4 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -37,23 +37,7 @@ export const FIELD_LABELS: Record = { "update.auto.betaCheckIntervalHours": "Auto Update Beta Check Interval (hours)", commitments: "Commitments", "commitments.enabled": "Commitments Enabled", - "commitments.store": "Commitments Store Path", - "commitments.categories": "Commitment Categories", - "commitments.categories.eventCheckIns": "Event Check-ins", - "commitments.categories.deadlineCheckIns": "Deadline Check-ins", - "commitments.categories.openLoops": "Open-loop Check-ins", - "commitments.categories.careCheckIns": "Care Check-ins", - "commitments.extraction": "Commitment Extraction", - "commitments.extraction.enabled": "Commitment Extraction Enabled", - "commitments.extraction.model": "Commitment Extraction Model", - "commitments.extraction.debounceMs": "Commitment Extraction Debounce (ms)", - "commitments.extraction.batchMaxItems": "Commitment Extraction Batch Size", - "commitments.extraction.confidenceThreshold": "Commitment Confidence Threshold", - "commitments.extraction.careConfidenceThreshold": "Care Commitment Confidence Threshold", - "commitments.extraction.timeoutSeconds": "Commitment Extraction Timeout (sec)", - "commitments.delivery": "Commitment Delivery", - "commitments.delivery.maxPerHeartbeat": "Commitments per Heartbeat", - "commitments.delivery.expireAfterHours": "Commitment Expiration (hours)", + "commitments.maxPerDay": "Commitments per Day", "diagnostics.enabled": "Diagnostics Enabled", "diagnostics.flags": "Diagnostics Flags", "diagnostics.stuckSessionWarnMs": "Stuck Session Warning Threshold (ms)", diff --git a/src/config/types.commitments.ts b/src/config/types.commitments.ts index 8406e4def88..c14c3e88f8a 100644 --- a/src/config/types.commitments.ts +++ b/src/config/types.commitments.ts @@ -1,47 +1,6 @@ -export type CommitmentCategoryConfig = { - /** Enable inferred event check-ins such as "interview tomorrow". Default: true. */ - eventCheckIns?: boolean; - /** Enable inferred deadline/progress check-ins. Default: true. */ - deadlineCheckIns?: boolean; - /** Enable inferred open-loop check-ins such as "waiting to hear back". Default: true. */ - openLoops?: boolean; - /** - * Enable personal care check-ins. "gentle" keeps conservative extraction and delivery wording. - * Default: "gentle". - */ - careCheckIns?: boolean | "gentle"; -}; - -export type CommitmentExtractionConfig = { - /** Enable the background LLM extractor. Default: true. */ - enabled?: boolean; - /** Optional model override (provider/model) for extractor runs. Defaults to the agent model. */ - model?: string; - /** Debounce before draining queued extraction items. Default: 15000. */ - debounceMs?: number; - /** Max extraction items per model call. Default: 8. */ - batchMaxItems?: number; - /** Minimum confidence accepted for routine inferred commitments. Default: 0.72. */ - confidenceThreshold?: number; - /** Minimum confidence accepted for care check-ins. Default: 0.86. */ - careConfidenceThreshold?: number; - /** Extractor run timeout in seconds. Default: 45. */ - timeoutSeconds?: number; -}; - -export type CommitmentDeliveryConfig = { - /** Max due commitments injected into one heartbeat turn. Default: 3. */ - maxPerHeartbeat?: number; - /** Pending commitments older than this after latest due time are expired. Default: 72. */ - expireAfterHours?: number; -}; - export type CommitmentsConfig = { - /** Enable inferred commitment creation and heartbeat delivery. Default: true. */ + /** Enable inferred follow-up extraction, storage, and heartbeat delivery. Default: false. */ enabled?: boolean; - /** Optional JSON store path. Defaults to ~/.openclaw/commitments/commitments.json. */ - store?: string; - categories?: CommitmentCategoryConfig; - extraction?: CommitmentExtractionConfig; - delivery?: CommitmentDeliveryConfig; + /** Maximum inferred follow-up commitments delivered per agent session in a rolling day. Default: 3. */ + maxPerDay?: number; }; diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index b191979ef59..0085a536d88 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -257,35 +257,7 @@ const CrestodianSchema = z const CommitmentsSchema = z .object({ enabled: z.boolean().optional(), - store: z.string().optional(), - categories: z - .object({ - eventCheckIns: z.boolean().optional(), - deadlineCheckIns: z.boolean().optional(), - openLoops: z.boolean().optional(), - careCheckIns: z.union([z.boolean(), z.literal("gentle")]).optional(), - }) - .strict() - .optional(), - extraction: z - .object({ - enabled: z.boolean().optional(), - model: z.string().optional(), - debounceMs: z.number().int().nonnegative().optional(), - batchMaxItems: z.number().int().positive().optional(), - confidenceThreshold: z.number().min(0).max(1).optional(), - careConfidenceThreshold: z.number().min(0).max(1).optional(), - timeoutSeconds: z.number().int().positive().optional(), - }) - .strict() - .optional(), - delivery: z - .object({ - maxPerHeartbeat: z.number().int().positive().optional(), - expireAfterHours: z.number().int().positive().optional(), - }) - .strict() - .optional(), + maxPerDay: z.number().int().positive().optional(), }) .strict() .optional(); diff --git a/src/infra/heartbeat-runner.commitments.test.ts b/src/infra/heartbeat-runner.commitments.test.ts index ce15ad42b01..90cd44b2e79 100644 --- a/src/infra/heartbeat-runner.commitments.test.ts +++ b/src/infra/heartbeat-runner.commitments.test.ts @@ -1,5 +1,4 @@ -import path from "node:path"; -import { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { HEARTBEAT_TOKEN } from "../auto-reply/tokens.js"; import { loadCommitmentStore, saveCommitmentStore } from "../commitments/store.js"; import type { CommitmentRecord } from "../commitments/types.js"; @@ -13,6 +12,10 @@ installHeartbeatRunnerTestRuntime(); describe("runHeartbeatOnce commitments", () => { const nowMs = Date.parse("2026-04-29T17:00:00.000Z"); + afterEach(() => { + vi.unstubAllEnvs(); + }); + function buildCommitment(params: { id: string; sessionKey: string; @@ -48,7 +51,7 @@ describe("runHeartbeatOnce commitments", () => { async function setupCommitmentCase(params?: { replyText?: string }) { return await withTempHeartbeatSandbox(async ({ tmpDir, storePath, replySpy }) => { - const commitmentStorePath = path.join(tmpDir, "commitments.json"); + vi.stubEnv("OPENCLAW_STATE_DIR", tmpDir); const sessionKey = "agent:main:telegram:user-155462274"; const cfg: OpenClawConfig = { agents: { @@ -62,14 +65,14 @@ describe("runHeartbeatOnce commitments", () => { }, channels: { telegram: { allowFrom: ["*"] } }, session: { store: storePath }, - commitments: { store: commitmentStorePath }, + commitments: { enabled: true }, }; await seedSessionStore(storePath, sessionKey, { lastChannel: "telegram", lastProvider: "telegram", lastTo: "stale-target", }); - await saveCommitmentStore(commitmentStorePath, { + await saveCommitmentStore(undefined, { version: 1, commitments: [buildCommitment({ id: "cm_interview", sessionKey, to: "155462274" })], }); @@ -103,7 +106,7 @@ describe("runHeartbeatOnce commitments", () => { return { result, sendTelegram, - store: await loadCommitmentStore(commitmentStorePath), + store: await loadCommitmentStore(), }; }); }