























@@ -20,6 +20,15 @@ import type {
20202121const STORE_VERSION = 1 as const;
2222const ROLLING_DAY_MS = 24 * 60 * 60 * 1000;
23+const COMMITMENT_KINDS = new Set([
24+"event_check_in",
25+"deadline_check",
26+"care_check_in",
27+"open_loop",
28+]);
29+const COMMITMENT_SENSITIVITIES = new Set(["routine", "personal", "care"]);
30+const COMMITMENT_SOURCES = new Set(["inferred_user_context", "agent_promise"]);
31+const COMMITMENT_STATUSES = new Set(["pending", "sent", "dismissed", "snoozed", "expired"]);
23322433type LoadedCommitmentStore = {
2534store: CommitmentStoreFile;
@@ -49,6 +58,26 @@ function isRecord(value: unknown): value is Record<string, unknown> {
4958return typeof value === "object" && value !== null && !Array.isArray(value);
5059}
516061+function normalizeRequiredString(value: unknown): string | undefined {
62+if (typeof value !== "string") {
63+return undefined;
64+}
65+const trimmed = value.trim();
66+return trimmed ? trimmed : undefined;
67+}
68+69+function normalizeOptionalString(value: unknown): string | undefined {
70+return typeof value === "string" ? value.trim() || undefined : undefined;
71+}
72+73+function normalizeNonNegativeNumber(value: unknown): number | undefined {
74+return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
75+}
76+77+function normalizeNonNegativeInteger(value: unknown): number | undefined {
78+return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : undefined;
79+}
80+5281function coerceCommitment(raw: unknown): CommitmentRecord | undefined {
5382if (!isRecord(raw)) {
5483return undefined;
@@ -57,35 +86,94 @@ function coerceCommitment(raw: unknown): CommitmentRecord | undefined {
5786if (!dueWindow) {
5887return undefined;
5988}
60-const requiredStrings = [
61-raw.id,
62-raw.agentId,
63-raw.sessionKey,
64-raw.channel,
65-raw.kind,
66-raw.sensitivity,
67-raw.source,
68-raw.status,
69-raw.reason,
70-raw.suggestedText,
71-raw.dedupeKey,
72-];
73-if (requiredStrings.some((value) => typeof value !== "string" || !value.trim())) {
74-return undefined;
75-}
89+90+const id = normalizeRequiredString(raw.id);
91+const agentId = normalizeRequiredString(raw.agentId);
92+const sessionKey = normalizeRequiredString(raw.sessionKey);
93+const channel = normalizeRequiredString(raw.channel);
94+const reason = normalizeRequiredString(raw.reason);
95+const suggestedText = normalizeRequiredString(raw.suggestedText);
96+const dedupeKey = normalizeRequiredString(raw.dedupeKey);
97+const kind = normalizeRequiredString(raw.kind);
98+const sensitivity = normalizeRequiredString(raw.sensitivity);
99+const source = normalizeRequiredString(raw.source);
100+const status = normalizeRequiredString(raw.status);
101+const confidence = normalizeNonNegativeNumber(raw.confidence);
102+const createdAtMs = normalizeNonNegativeNumber(raw.createdAtMs);
103+const updatedAtMs = normalizeNonNegativeNumber(raw.updatedAtMs);
104+const attempts = normalizeNonNegativeInteger(raw.attempts);
105+const earliestMs = normalizeNonNegativeNumber(dueWindow.earliestMs);
106+const latestMs = normalizeNonNegativeNumber(dueWindow.latestMs);
107+const timezone = normalizeRequiredString(dueWindow.timezone);
108+const accountId = normalizeOptionalString(raw.accountId);
109+const to = normalizeOptionalString(raw.to);
110+const threadId = normalizeOptionalString(raw.threadId);
111+const senderId = normalizeOptionalString(raw.senderId);
112+const sourceMessageId = normalizeOptionalString(raw.sourceMessageId);
113+const sourceRunId = normalizeOptionalString(raw.sourceRunId);
114+const lastAttemptAtMs = normalizeNonNegativeNumber(raw.lastAttemptAtMs);
115+const sentAtMs = normalizeNonNegativeNumber(raw.sentAtMs);
116+const dismissedAtMs = normalizeNonNegativeNumber(raw.dismissedAtMs);
117+const snoozedUntilMs = normalizeNonNegativeNumber(raw.snoozedUntilMs);
118+const expiredAtMs = normalizeNonNegativeNumber(raw.expiredAtMs);
119+76120if (
77-typeof raw.confidence !== "number" ||
78-typeof raw.createdAtMs !== "number" ||
79-typeof raw.updatedAtMs !== "number" ||
80-typeof raw.attempts !== "number" ||
81-typeof dueWindow.earliestMs !== "number" ||
82-typeof dueWindow.latestMs !== "number" ||
83-typeof dueWindow.timezone !== "string"
121+!id ||
122+!agentId ||
123+!sessionKey ||
124+!channel ||
125+!reason ||
126+!suggestedText ||
127+!dedupeKey ||
128+!kind ||
129+!sensitivity ||
130+!source ||
131+!status ||
132+!COMMITMENT_KINDS.has(kind) ||
133+!COMMITMENT_SENSITIVITIES.has(sensitivity) ||
134+!COMMITMENT_SOURCES.has(source) ||
135+!COMMITMENT_STATUSES.has(status) ||
136+confidence === undefined ||
137+createdAtMs === undefined ||
138+updatedAtMs === undefined ||
139+attempts === undefined ||
140+earliestMs === undefined ||
141+latestMs === undefined ||
142+!timezone ||
143+latestMs < earliestMs
84144) {
85145return undefined;
86146}
87-const commitment = { ...raw } as CommitmentRecord;
88-return stripLegacySourceText(commitment);
147+148+return {
149+ id,
150+ agentId,
151+ sessionKey,
152+ channel,
153+ ...(accountId ? { accountId } : {}),
154+ ...(to ? { to } : {}),
155+ ...(threadId ? { threadId } : {}),
156+ ...(senderId ? { senderId } : {}),
157+kind: kind as CommitmentRecord["kind"],
158+sensitivity: sensitivity as CommitmentRecord["sensitivity"],
159+source: source as CommitmentRecord["source"],
160+status: status as CommitmentRecord["status"],
161+ reason,
162+ suggestedText,
163+ dedupeKey,
164+ confidence,
165+dueWindow: { earliestMs, latestMs, timezone },
166+ ...(sourceMessageId ? { sourceMessageId } : {}),
167+ ...(sourceRunId ? { sourceRunId } : {}),
168+ createdAtMs,
169+ updatedAtMs,
170+ attempts,
171+ ...(lastAttemptAtMs !== undefined ? { lastAttemptAtMs } : {}),
172+ ...(sentAtMs !== undefined ? { sentAtMs } : {}),
173+ ...(dismissedAtMs !== undefined ? { dismissedAtMs } : {}),
174+ ...(snoozedUntilMs !== undefined ? { snoozedUntilMs } : {}),
175+ ...(expiredAtMs !== undefined ? { expiredAtMs } : {}),
176+};
89177}
9017891179function hasLegacySourceText(raw: unknown): boolean {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。