mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 12:30:43 +00:00
Agents: simplify inferred commitment config (#74189)
This commit is contained in:
committed by
Vignesh
parent
11771ec172
commit
aecde2b3ac
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<OpenClawConfig> {
|
||||
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);
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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<OpenClawConfig> {
|
||||
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<typeof setTimeout>,
|
||||
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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
92
src/commitments/store.test.ts
Normal file
92
src/commitments/store.test.ts
Normal file
@@ -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<void> {
|
||||
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>): 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);
|
||||
});
|
||||
});
|
||||
@@ -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<CommitmentRecord[]> {
|
||||
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<string>();
|
||||
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<CommitmentRecord[]> {
|
||||
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<typeof resolveCommitmentsConfig>["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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user