@happyvertical/smrt-core 0.51.27 → 0.51.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +20 -0
- package/README.md +47 -0
- package/agents/build-knowledge.md +16 -0
- package/agents/change-feed.md +95 -4
- package/agents/change-signals.md +6 -1
- package/agents/run-once.md +268 -0
- package/agents/schema-paths.md +17 -5
- package/dist/browser.js +4 -3
- package/dist/change-feed-authz.d.ts +141 -0
- package/dist/change-feed-authz.d.ts.map +1 -0
- package/dist/change-feed-authz.js +260 -0
- package/dist/change-feed-authz.js.map +1 -0
- package/dist/change-feed.d.ts +15 -0
- package/dist/change-feed.d.ts.map +1 -1
- package/dist/change-feed.js +4 -0
- package/dist/change-feed.js.map +1 -1
- package/dist/errors.d.ts +28 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +36 -1
- package/dist/errors.js.map +1 -1
- package/dist/generators/changes-route.d.ts.map +1 -1
- package/dist/generators/changes-route.js +13 -3
- package/dist/generators/changes-route.js.map +1 -1
- package/dist/generators/events-route.d.ts +35 -4
- package/dist/generators/events-route.d.ts.map +1 -1
- package/dist/generators/events-route.js +78 -14
- package/dist/generators/events-route.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/knowledge-graph.d.ts.map +1 -1
- package/dist/knowledge-graph.js +1 -1
- package/dist/knowledge-graph.js.map +1 -1
- package/dist/manifest/static-manifest.js +1 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/migrations/differ.d.ts +6 -3
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +11 -5
- package/dist/migrations/differ.js.map +1 -1
- package/dist/registry/class-registration.d.ts.map +1 -1
- package/dist/registry/class-registration.js +21 -1
- package/dist/registry/class-registration.js.map +1 -1
- package/dist/registry/types.d.ts +3 -1
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/run-once.d.ts +80 -0
- package/dist/run-once.d.ts.map +1 -0
- package/dist/run-once.js +311 -0
- package/dist/run-once.js.map +1 -0
- package/dist/smrt-knowledge.json +20 -14
- package/dist/system/index.d.ts +1 -0
- package/dist/system/index.d.ts.map +1 -1
- package/dist/system/index.js +3 -2
- package/dist/system/retention.d.ts +53 -4
- package/dist/system/retention.d.ts.map +1 -1
- package/dist/system/retention.js +67 -14
- package/dist/system/retention.js.map +1 -1
- package/dist/system/schema.d.ts +33 -1
- package/dist/system/schema.d.ts.map +1 -1
- package/dist/system/schema.js +60 -3
- package/dist/system/schema.js.map +1 -1
- package/dist/vite-plugin/changes-route.d.ts.map +1 -1
- package/dist/vite-plugin/changes-route.js +21 -5
- package/dist/vite-plugin/changes-route.js.map +1 -1
- package/dist/vite-plugin/events-route.d.ts.map +1 -1
- package/dist/vite-plugin/events-route.js +9 -0
- package/dist/vite-plugin/events-route.js.map +1 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +14 -0
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/worker-registration.d.ts +34 -0
- package/dist/vite-plugin/worker-registration.d.ts.map +1 -0
- package/dist/vite-plugin/worker-registration.js +108 -0
- package/dist/vite-plugin/worker-registration.js.map +1 -0
- package/package.json +4 -4
|
@@ -31,6 +31,24 @@ export interface DispatchRetentionPolicy {
|
|
|
31
31
|
/** Drop failed dispatches last touched more than this many days ago (default 90). */
|
|
32
32
|
failedOlderThanDays?: number;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Retention for `runOnce()` idempotency claims (`_smrt_run_once_claims`, #3080).
|
|
36
|
+
*
|
|
37
|
+
* Only `status: 'completed'` rows with a `completed_at` older than the window
|
|
38
|
+
* are ever deleted — an `'in_progress'` claim has no `completed_at` and is
|
|
39
|
+
* never touched by this task regardless of age. See
|
|
40
|
+
* {@link pruneRunOnceClaims} and `agents/run-once.md` for the consequence of
|
|
41
|
+
* this window: a caller that replays a `runOnce()` token after its claim has
|
|
42
|
+
* been swept runs `work()` again instead of replaying the stored result, so
|
|
43
|
+
* `maxAgeDays` must stay comfortably longer than any realistic client retry
|
|
44
|
+
* window.
|
|
45
|
+
*/
|
|
46
|
+
export interface RunOnceClaimsRetentionPolicy {
|
|
47
|
+
/** Run this task (default `true`). */
|
|
48
|
+
enabled?: boolean;
|
|
49
|
+
/** Drop completed claims older than this many days (default 30). */
|
|
50
|
+
maxAgeDays?: number;
|
|
51
|
+
}
|
|
34
52
|
/**
|
|
35
53
|
* Retention policy for a sweep.
|
|
36
54
|
*
|
|
@@ -51,6 +69,8 @@ export interface RetentionPolicy {
|
|
|
51
69
|
contexts?: ContextsRetentionPolicy | false;
|
|
52
70
|
/** `_smrt_dispatch` retention, or `false` to skip. */
|
|
53
71
|
dispatch?: DispatchRetentionPolicy | false;
|
|
72
|
+
/** `_smrt_run_once_claims` retention, or `false` to skip. */
|
|
73
|
+
runOnceClaims?: RunOnceClaimsRetentionPolicy | false;
|
|
54
74
|
/**
|
|
55
75
|
* Enable/disable individual tasks contributed through
|
|
56
76
|
* {@link registerRetentionTask}, keyed by task name. Unlisted tasks run.
|
|
@@ -71,6 +91,7 @@ export declare const DEFAULT_RETENTION_POLICY: {
|
|
|
71
91
|
aiUsage: AiUsageRetentionPolicy;
|
|
72
92
|
contexts: ContextsRetentionPolicy;
|
|
73
93
|
dispatch: DispatchRetentionPolicy;
|
|
94
|
+
runOnceClaims: RunOnceClaimsRetentionPolicy;
|
|
74
95
|
};
|
|
75
96
|
/** Per-run inputs handed to every retention task. */
|
|
76
97
|
export interface RetentionTaskContext {
|
|
@@ -122,9 +143,9 @@ export interface RetentionTask {
|
|
|
122
143
|
* off by name. Re-registering the same name replaces the previous task, which
|
|
123
144
|
* keeps module re-evaluation (HMR, repeated test imports) idempotent.
|
|
124
145
|
*
|
|
125
|
-
* @throws If `task.name` is empty, or is one of the
|
|
126
|
-
* names (`changes`, `ai-usage`, `contexts`, `dispatch`)
|
|
127
|
-
* {@link RESERVED_TASK_NAMES}.
|
|
146
|
+
* @throws If `task.name` is empty, or is one of the five built-in table
|
|
147
|
+
* names (`changes`, `ai-usage`, `contexts`, `dispatch`, `run-once-claims`)
|
|
148
|
+
* — see {@link RESERVED_TASK_NAMES}.
|
|
128
149
|
*/
|
|
129
150
|
export declare function registerRetentionTask(task: RetentionTask): void;
|
|
130
151
|
/** Remove a registered retention task. Returns `true` if one was removed. */
|
|
@@ -205,6 +226,34 @@ export declare function pruneExpiredContexts(db: DatabaseInterface, options?: {
|
|
|
205
226
|
}): Promise<{
|
|
206
227
|
pruned: number;
|
|
207
228
|
}>;
|
|
229
|
+
/** Bounds for {@link pruneRunOnceClaims}. */
|
|
230
|
+
export interface RunOnceClaimsRetention {
|
|
231
|
+
/** Delete completed claims older than this many milliseconds. */
|
|
232
|
+
maxAgeMs: number;
|
|
233
|
+
/** Count matching records without deleting them. */
|
|
234
|
+
dryRun?: boolean;
|
|
235
|
+
/** Clock for the age cutoff (default: now). */
|
|
236
|
+
now?: Date;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Prune completed `runOnce()` idempotency claims to bound
|
|
240
|
+
* `_smrt_run_once_claims` growth (#3080).
|
|
241
|
+
*
|
|
242
|
+
* Only rows with `status = 'completed'` AND a `completed_at` older than the
|
|
243
|
+
* cutoff are deleted. An `'in_progress'` claim has no `completed_at`, so this
|
|
244
|
+
* predicate can never match one — a claim held open by an in-flight (or
|
|
245
|
+
* abandoned) transaction is never pruned by age alone.
|
|
246
|
+
*
|
|
247
|
+
* Predicate is covered by `idx_smrt_run_once_claims_status_completed`
|
|
248
|
+
* (`status, completed_at`).
|
|
249
|
+
*
|
|
250
|
+
* @param db - Database holding the system tables.
|
|
251
|
+
* @param retention - `maxAgeMs` is required; there is no row-count bound.
|
|
252
|
+
* @returns Number of claims deleted (or, under `dryRun`, matched).
|
|
253
|
+
*/
|
|
254
|
+
export declare function pruneRunOnceClaims(db: DatabaseInterface, retention: RunOnceClaimsRetention): Promise<{
|
|
255
|
+
pruned: number;
|
|
256
|
+
}>;
|
|
208
257
|
/** Aggregate outcome of one {@link runRetentionSweep} call. */
|
|
209
258
|
export interface RetentionSweepResult {
|
|
210
259
|
/** Whether this run only counted rows. */
|
|
@@ -224,7 +273,7 @@ export interface RetentionSweepResult {
|
|
|
224
273
|
* Apply a {@link RetentionPolicy} across every framework-owned table.
|
|
225
274
|
*
|
|
226
275
|
* Built-in tasks run first, in a fixed order (`changes`, `ai-usage`,
|
|
227
|
-
* `contexts`, `dispatch`), followed by tasks registered via
|
|
276
|
+
* `contexts`, `dispatch`, `run-once-claims`), followed by tasks registered via
|
|
228
277
|
* {@link registerRetentionTask} in name order. Tables that do not exist in
|
|
229
278
|
* this database are reported as `skipped: 'unavailable'` rather than failing —
|
|
230
279
|
* a sweep must be safe to run against a partially bootstrapped database.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retention.d.ts","sourceRoot":"","sources":["../../src/system/retention.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"retention.d.ts","sourceRoot":"","sources":["../../src/system/retention.ts"],"names":[],"mappings":"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"}
|
package/dist/system/retention.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { toSafeInteger } from "../utils/safe-integer.js";
|
|
2
2
|
import { detectEngine } from "../schema/ddl/index.js";
|
|
3
|
+
import { RUN_ONCE_CLAIMS_TABLE } from "./schema.js";
|
|
3
4
|
import { pruneChangeFeed } from "../change-feed.js";
|
|
4
5
|
import { createLogger } from "@happyvertical/logger";
|
|
5
6
|
//#region src/system/retention.ts
|
|
@@ -10,8 +11,10 @@ import { createLogger } from "@happyvertical/logger";
|
|
|
10
11
|
* this module, unbounded by default: `_smrt_changes` grew one row per
|
|
11
12
|
* framework save/delete, `_smrt_ai_usage` one row per AI call (persistence is
|
|
12
13
|
* on by default, see `config.ts`), `_smrt_contexts` accumulated rows whose
|
|
13
|
-
* `expires_at` nothing ever enforced,
|
|
14
|
-
* work until an operator remembered to run `smrt dispatch:cleanup
|
|
14
|
+
* `expires_at` nothing ever enforced, `_smrt_dispatch` retained completed
|
|
15
|
+
* work until an operator remembered to run `smrt dispatch:cleanup`, and
|
|
16
|
+
* `_smrt_run_once_claims` (#3080) gains one row per `runOnce()` submission
|
|
17
|
+
* with nothing pruning completed ones.
|
|
15
18
|
*
|
|
16
19
|
* This module supplies the missing half:
|
|
17
20
|
*
|
|
@@ -37,10 +40,11 @@ import { createLogger } from "@happyvertical/logger";
|
|
|
37
40
|
*
|
|
38
41
|
* Every predicate below is indexed by the system DDL in `system/schema.ts`
|
|
39
42
|
* (`_smrt_contexts(expires_at)`, `_smrt_ai_usage(tenant_id, created_at)`,
|
|
40
|
-
* `_smrt_dispatch(status, processed_at)` / `(status, updated_at)
|
|
41
|
-
*
|
|
42
|
-
* (`_smrt_jobs(status, completed_at)`,
|
|
43
|
-
* A new retention predicate must ship with
|
|
43
|
+
* `_smrt_dispatch(status, processed_at)` / `(status, updated_at)`,
|
|
44
|
+
* `_smrt_run_once_claims(status, completed_at)`) or by the jobs compatibility
|
|
45
|
+
* path in `system/compatibility.ts` (`_smrt_jobs(status, completed_at)`,
|
|
46
|
+
* `_smrt_job_events(created_at)`). A new retention predicate must ship with
|
|
47
|
+
* its index.
|
|
44
48
|
*
|
|
45
49
|
* @see https://github.com/happyvertical/smrt/issues/2375
|
|
46
50
|
* @packageDocumentation
|
|
@@ -64,11 +68,12 @@ var DEFAULT_RETENTION_POLICY = {
|
|
|
64
68
|
dispatch: {
|
|
65
69
|
completedOlderThanDays: 30,
|
|
66
70
|
failedOlderThanDays: 90
|
|
67
|
-
}
|
|
71
|
+
},
|
|
72
|
+
runOnceClaims: { maxAgeDays: 30 }
|
|
68
73
|
};
|
|
69
74
|
var RETENTION_TASKS_KEY = Symbol.for("smrt.retention-tasks");
|
|
70
75
|
/**
|
|
71
|
-
* Names {@link builtInTasks} always uses for the
|
|
76
|
+
* Names {@link builtInTasks} always uses for the five framework-owned tables.
|
|
72
77
|
*
|
|
73
78
|
* Reserved against contributed tasks: `RetentionTaskResult.task` is
|
|
74
79
|
* documented unique within a sweep, and a contributed task sharing one of
|
|
@@ -82,7 +87,8 @@ var RESERVED_TASK_NAMES = /* @__PURE__ */ new Set([
|
|
|
82
87
|
"changes",
|
|
83
88
|
"ai-usage",
|
|
84
89
|
"contexts",
|
|
85
|
-
"dispatch"
|
|
90
|
+
"dispatch",
|
|
91
|
+
"run-once-claims"
|
|
86
92
|
]);
|
|
87
93
|
/**
|
|
88
94
|
* The task registry, held on `globalThis` rather than in module scope.
|
|
@@ -106,9 +112,9 @@ function registeredTasks() {
|
|
|
106
112
|
* off by name. Re-registering the same name replaces the previous task, which
|
|
107
113
|
* keeps module re-evaluation (HMR, repeated test imports) idempotent.
|
|
108
114
|
*
|
|
109
|
-
* @throws If `task.name` is empty, or is one of the
|
|
110
|
-
* names (`changes`, `ai-usage`, `contexts`, `dispatch`)
|
|
111
|
-
* {@link RESERVED_TASK_NAMES}.
|
|
115
|
+
* @throws If `task.name` is empty, or is one of the five built-in table
|
|
116
|
+
* names (`changes`, `ai-usage`, `contexts`, `dispatch`, `run-once-claims`)
|
|
117
|
+
* — see {@link RESERVED_TASK_NAMES}.
|
|
112
118
|
*/
|
|
113
119
|
function registerRetentionTask(task) {
|
|
114
120
|
if (!task.name) throw new Error("registerRetentionTask requires a non-empty task name");
|
|
@@ -207,10 +213,36 @@ async function pruneExpiredContexts(db, options = {}) {
|
|
|
207
213
|
return { pruned: await deleteFrom(db, CONTEXTS_TABLE, ["expires_at IS NOT NULL", "expires_at < ?"], [(options.now ?? /* @__PURE__ */ new Date()).toISOString()], options.dryRun ?? false) };
|
|
208
214
|
}
|
|
209
215
|
/**
|
|
216
|
+
* Prune completed `runOnce()` idempotency claims to bound
|
|
217
|
+
* `_smrt_run_once_claims` growth (#3080).
|
|
218
|
+
*
|
|
219
|
+
* Only rows with `status = 'completed'` AND a `completed_at` older than the
|
|
220
|
+
* cutoff are deleted. An `'in_progress'` claim has no `completed_at`, so this
|
|
221
|
+
* predicate can never match one — a claim held open by an in-flight (or
|
|
222
|
+
* abandoned) transaction is never pruned by age alone.
|
|
223
|
+
*
|
|
224
|
+
* Predicate is covered by `idx_smrt_run_once_claims_status_completed`
|
|
225
|
+
* (`status, completed_at`).
|
|
226
|
+
*
|
|
227
|
+
* @param db - Database holding the system tables.
|
|
228
|
+
* @param retention - `maxAgeMs` is required; there is no row-count bound.
|
|
229
|
+
* @returns Number of claims deleted (or, under `dryRun`, matched).
|
|
230
|
+
*/
|
|
231
|
+
async function pruneRunOnceClaims(db, retention) {
|
|
232
|
+
const { maxAgeMs, dryRun = false } = retention;
|
|
233
|
+
if (!Number.isFinite(maxAgeMs) || maxAgeMs < 0) throw new Error(`pruneRunOnceClaims maxAgeMs must be >= 0, got ${maxAgeMs}`);
|
|
234
|
+
const now = retention.now ?? /* @__PURE__ */ new Date();
|
|
235
|
+
return { pruned: await deleteFrom(db, RUN_ONCE_CLAIMS_TABLE, [
|
|
236
|
+
"status = 'completed'",
|
|
237
|
+
"completed_at IS NOT NULL",
|
|
238
|
+
"completed_at < ?"
|
|
239
|
+
], [new Date(now.getTime() - maxAgeMs).toISOString()], dryRun) };
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
210
242
|
* Apply a {@link RetentionPolicy} across every framework-owned table.
|
|
211
243
|
*
|
|
212
244
|
* Built-in tasks run first, in a fixed order (`changes`, `ai-usage`,
|
|
213
|
-
* `contexts`, `dispatch`), followed by tasks registered via
|
|
245
|
+
* `contexts`, `dispatch`, `run-once-claims`), followed by tasks registered via
|
|
214
246
|
* {@link registerRetentionTask} in name order. Tables that do not exist in
|
|
215
247
|
* this database are reported as `skipped: 'unavailable'` rather than failing —
|
|
216
248
|
* a sweep must be safe to run against a partially bootstrapped database.
|
|
@@ -338,6 +370,11 @@ function builtInTasks(policy) {
|
|
|
338
370
|
name: "dispatch",
|
|
339
371
|
table: "_smrt_dispatch",
|
|
340
372
|
task: dispatchTask(policy)
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
name: "run-once-claims",
|
|
376
|
+
table: RUN_ONCE_CLAIMS_TABLE,
|
|
377
|
+
task: runOnceClaimsTask(policy)
|
|
341
378
|
}
|
|
342
379
|
];
|
|
343
380
|
}
|
|
@@ -411,6 +448,22 @@ function dispatchTask(policy) {
|
|
|
411
448
|
}
|
|
412
449
|
};
|
|
413
450
|
}
|
|
451
|
+
function runOnceClaimsTask(policy) {
|
|
452
|
+
const config = resolveEntry(policy.runOnceClaims, DEFAULT_RETENTION_POLICY.runOnceClaims);
|
|
453
|
+
if (!config) return null;
|
|
454
|
+
return {
|
|
455
|
+
name: "run-once-claims",
|
|
456
|
+
description: "Prune completed runOnce() idempotency claims (_smrt_run_once_claims)",
|
|
457
|
+
run: async (db, context) => {
|
|
458
|
+
const { pruned } = await pruneRunOnceClaims(db, {
|
|
459
|
+
maxAgeMs: (config.maxAgeDays ?? DEFAULT_RETENTION_POLICY.runOnceClaims.maxAgeDays ?? 30) * MS_PER_DAY,
|
|
460
|
+
dryRun: context.dryRun,
|
|
461
|
+
now: context.now
|
|
462
|
+
});
|
|
463
|
+
return pruned;
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
}
|
|
414
467
|
function getQueryRows(result) {
|
|
415
468
|
if (Array.isArray(result)) return result;
|
|
416
469
|
if (result && typeof result === "object" && "rows" in result) {
|
|
@@ -492,6 +545,6 @@ async function tableExists(db, table) {
|
|
|
492
545
|
}
|
|
493
546
|
}
|
|
494
547
|
//#endregion
|
|
495
|
-
export { DEFAULT_RETENTION_POLICY, clearRetentionTasks, getRetentionTasks, pruneAiUsage, pruneExpiredContexts, registerRetentionTask, runRetentionSweep, unregisterRetentionTask };
|
|
548
|
+
export { DEFAULT_RETENTION_POLICY, clearRetentionTasks, getRetentionTasks, pruneAiUsage, pruneExpiredContexts, pruneRunOnceClaims, registerRetentionTask, runRetentionSweep, unregisterRetentionTask };
|
|
496
549
|
|
|
497
550
|
//# sourceMappingURL=retention.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retention.js","names":[],"sources":["../../src/system/retention.ts"],"sourcesContent":["/**\n * System-table retention — bounded growth for framework-owned tables (#2375).\n *\n * Several `_smrt_*` tables are append-only by construction and were, until\n * this module, unbounded by default: `_smrt_changes` grew one row per\n * framework save/delete, `_smrt_ai_usage` one row per AI call (persistence is\n * on by default, see `config.ts`), `_smrt_contexts` accumulated rows whose\n * `expires_at` nothing ever enforced, and `_smrt_dispatch` retained completed\n * work until an operator remembered to run `smrt dispatch:cleanup`.\n *\n * This module supplies the missing half:\n *\n * - {@link pruneAiUsage} — the retention API `_smrt_ai_usage` never had.\n * - {@link pruneExpiredContexts} — enforcement for `_smrt_contexts.expires_at`.\n * - {@link runRetentionSweep} — one entry point that applies a documented,\n * opt-out {@link RetentionPolicy} across every framework-owned table, plus\n * any task other packages contribute via {@link registerRetentionTask}\n * (`@happyvertical/smrt-jobs` registers job/job-event cleanup;\n * `@happyvertical/smrt-users` registers session and token expiry).\n *\n * Scheduling is the caller's: `smrt db:prune` is the cron entry point, and a\n * running `TaskRunner` sweeps on an interval unless configured otherwise.\n *\n * ## Failure policy\n *\n * A sweep is maintenance, not a transaction. One failing task never aborts the\n * rest — the failure is captured on that task's {@link RetentionTaskResult}\n * and the sweep continues. Callers decide what a partial sweep means; the CLI\n * exits non-zero when any task failed.\n *\n * ## Predicate indexes\n *\n * Every predicate below is indexed by the system DDL in `system/schema.ts`\n * (`_smrt_contexts(expires_at)`, `_smrt_ai_usage(tenant_id, created_at)`,\n * `_smrt_dispatch(status, processed_at)` / `(status, updated_at)`) or by the\n * jobs compatibility path in `system/compatibility.ts`\n * (`_smrt_jobs(status, completed_at)`, `_smrt_job_events(created_at)`).\n * A new retention predicate must ship with its index.\n *\n * @see https://github.com/happyvertical/smrt/issues/2375\n * @packageDocumentation\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { pruneChangeFeed } from '../change-feed.js';\nimport { detectEngine } from '../schema/ddl/index.js';\nimport { toSafeInteger } from '../utils/safe-integer.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/** Milliseconds in a day — the unit every retention policy is expressed in. */\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\n\n// ============================================================================\n// Policy\n// ============================================================================\n\n/** Retention for the append-only change feed (`_smrt_changes`). */\nexport interface ChangeFeedRetentionPolicy {\n /** Run this task (default `true`). */\n enabled?: boolean;\n /** Drop entries older than this many days (default 30). */\n maxAgeDays?: number;\n /** Additionally keep at most this many newest entries (default: unbounded). */\n maxRows?: number;\n}\n\n/** Retention for AI usage telemetry (`_smrt_ai_usage`). */\nexport interface AiUsageRetentionPolicy {\n /** Run this task (default `true`). */\n enabled?: boolean;\n /** Drop records older than this many days (default 90). */\n maxAgeDays?: number;\n /** Additionally keep at most this many newest records (default: unbounded). */\n maxRows?: number;\n}\n\n/** Retention for remembered context entries (`_smrt_contexts`). */\nexport interface ContextsRetentionPolicy {\n /** Run this task (default `true`). */\n enabled?: boolean;\n}\n\n/** Retention for the dispatch queue (`_smrt_dispatch`). */\nexport interface DispatchRetentionPolicy {\n /** Run this task (default `true`). */\n enabled?: boolean;\n /** Drop completed dispatches processed more than this many days ago (default 30). */\n completedOlderThanDays?: number;\n /** Drop failed dispatches last touched more than this many days ago (default 90). */\n failedOlderThanDays?: number;\n}\n\n/**\n * Retention policy for a sweep.\n *\n * Every table is pruned by default; set a table's entry to `false` (or\n * `{ enabled: false }`) to opt that table out, or `enabled: false` at the top\n * level to opt the whole sweep out.\n */\nexport interface RetentionPolicy {\n /** Run the sweep at all (default `true`). */\n enabled?: boolean;\n /** Report what would be pruned without deleting anything (default `false`). */\n dryRun?: boolean;\n /** `_smrt_changes` retention, or `false` to skip. */\n changes?: ChangeFeedRetentionPolicy | false;\n /** `_smrt_ai_usage` retention, or `false` to skip. */\n aiUsage?: AiUsageRetentionPolicy | false;\n /** `_smrt_contexts` expiry enforcement, or `false` to skip. */\n contexts?: ContextsRetentionPolicy | false;\n /** `_smrt_dispatch` retention, or `false` to skip. */\n dispatch?: DispatchRetentionPolicy | false;\n /**\n * Enable/disable individual tasks contributed through\n * {@link registerRetentionTask}, keyed by task name. Unlisted tasks run.\n */\n tasks?: Record<string, boolean>;\n}\n\n/**\n * Documented retention defaults.\n *\n * Chosen to be generous relative to every in-tree consumer: change-feed\n * cursors resync automatically past the window, AI usage outlives a monthly\n * billing cycle by two months, and dispatch keeps a full quarter of failures\n * for forensics. Applications that need longer history raise these; those that\n * need none set the table to `false`.\n */\nexport const DEFAULT_RETENTION_POLICY: {\n changes: ChangeFeedRetentionPolicy;\n aiUsage: AiUsageRetentionPolicy;\n contexts: ContextsRetentionPolicy;\n dispatch: DispatchRetentionPolicy;\n} = {\n changes: { maxAgeDays: 30 },\n aiUsage: { maxAgeDays: 90 },\n contexts: {},\n dispatch: { completedOlderThanDays: 30, failedOlderThanDays: 90 },\n};\n\n// ============================================================================\n// Tasks\n// ============================================================================\n\n/** Per-run inputs handed to every retention task. */\nexport interface RetentionTaskContext {\n /** Count what would be deleted instead of deleting it. */\n dryRun: boolean;\n /** Sweep start time — every task derives its cutoffs from this one clock. */\n now: Date;\n}\n\n/** What one task did (or why it did nothing). */\nexport interface RetentionTaskResult {\n /** Task name, unique within a sweep. */\n task: string;\n /** Rows deleted, or — under `dryRun` — rows that would be deleted. */\n pruned: number;\n /**\n * Why the task did no work: `disabled` (policy opted it out) or\n * `unavailable` (the table does not exist in this database).\n */\n skipped?: 'disabled' | 'unavailable';\n /** Message of the error this task failed with, if any. */\n error?: string;\n /** Optional per-bucket breakdown (e.g. completed vs failed dispatches). */\n details?: Record<string, number>;\n}\n\n/** What a task returns; a bare number is shorthand for `{ pruned }`. */\nexport type RetentionTaskOutcome =\n | number\n | { pruned: number; details?: Record<string, number> };\n\n/**\n * A retention task contributed by a package that owns its own tables.\n *\n * Tasks must honour `context.dryRun` — a task that deletes during a dry run\n * makes the whole preview a lie.\n */\nexport interface RetentionTask {\n /** Unique name; also the {@link RetentionPolicy.tasks} opt-out key. */\n name: string;\n /** One-line description surfaced by `smrt db:prune --json`. */\n description?: string;\n /** Perform (or, under `dryRun`, count) the deletion. */\n run(\n db: DatabaseInterface,\n context: RetentionTaskContext,\n ): Promise<RetentionTaskOutcome>;\n}\n\nconst RETENTION_TASKS_KEY = Symbol.for('smrt.retention-tasks');\n\n/**\n * Names {@link builtInTasks} always uses for the four framework-owned tables.\n *\n * Reserved against contributed tasks: `RetentionTaskResult.task` is\n * documented unique within a sweep, and a contributed task sharing one of\n * these names would collide with a built-in result — silently, since nothing\n * else in the sweep loop checks for it — and would also be unreachable\n * through {@link RetentionPolicy.tasks}, which only ever sees the *last*\n * task run under that name, never the built-in one (opting a built-in table\n * out goes through its own dedicated policy field instead).\n */\nconst RESERVED_TASK_NAMES = new Set([\n 'changes',\n 'ai-usage',\n 'contexts',\n 'dispatch',\n]);\n\n/**\n * The task registry, held on `globalThis` rather than in module scope.\n *\n * Contributing packages resolve their own copy of `@happyvertical/smrt-core`\n * whenever a consumer's install is not fully deduped, and the CLI resolves a\n * third. A module-local `Map` would give each of them a private, empty\n * registry — the task would register in one and the sweep would run in\n * another. `ObjectRegistry` is on `globalThis` for the same reason.\n */\nfunction registeredTasks(): Map<string, RetentionTask> {\n const root = globalThis as typeof globalThis & {\n [RETENTION_TASKS_KEY]?: Map<string, RetentionTask>;\n };\n root[RETENTION_TASKS_KEY] ??= new Map<string, RetentionTask>();\n return root[RETENTION_TASKS_KEY];\n}\n\n/**\n * Register a retention task so {@link runRetentionSweep} includes it.\n *\n * Registering a task does not schedule anything — a task only runs when\n * something calls {@link runRetentionSweep}, and the policy can still turn it\n * off by name. Re-registering the same name replaces the previous task, which\n * keeps module re-evaluation (HMR, repeated test imports) idempotent.\n *\n * @throws If `task.name` is empty, or is one of the four built-in table\n * names (`changes`, `ai-usage`, `contexts`, `dispatch`) — see\n * {@link RESERVED_TASK_NAMES}.\n */\nexport function registerRetentionTask(task: RetentionTask): void {\n if (!task.name) {\n throw new Error('registerRetentionTask requires a non-empty task name');\n }\n if (RESERVED_TASK_NAMES.has(task.name)) {\n throw new Error(\n `registerRetentionTask: \"${task.name}\" is a built-in task name (reserved: ${[...RESERVED_TASK_NAMES].join(', ')})`,\n );\n }\n registeredTasks().set(task.name, task);\n}\n\n/** Remove a registered retention task. Returns `true` if one was removed. */\nexport function unregisterRetentionTask(name: string): boolean {\n return registeredTasks().delete(name);\n}\n\n/** Registered retention tasks, ordered by name for deterministic sweeps. */\nexport function getRetentionTasks(): RetentionTask[] {\n return [...registeredTasks().values()].sort((left, right) =>\n left.name.localeCompare(right.name),\n );\n}\n\n/** Drop every registered retention task (test helper). */\nexport function clearRetentionTasks(): void {\n registeredTasks().clear();\n}\n\n// ============================================================================\n// AI usage retention\n// ============================================================================\n\n/** Bounds for {@link pruneAiUsage}. At least one bound is required. */\nexport interface AiUsageRetention {\n /** Delete records older than this many milliseconds. */\n maxAgeMs?: number;\n /**\n * Keep at most this many newest records (by `created_at`).\n *\n * A ceiling, not a floor: combined with `maxAgeMs`, the two bounds union\n * — a row past the age cutoff is deleted regardless of `maxRows`, so\n * `maxRows` never resurrects rows the age bound already excludes. If every\n * row is already older than `maxAgeMs`, a finite `maxRows` keeps none of\n * them; it does not guarantee `maxRows` survivors independent of age. (A\n * `maxRows` that overrode age this way would defeat the reason `maxAgeMs`\n * usually exists in the first place — a compliance/privacy deletion\n * deadline that a row-count floor should not be able to postpone.)\n *\n * Rows sharing the cutoff timestamp all survive, so the retained count can\n * exceed `maxRows` by the size of one timestamp tie.\n */\n maxRows?: number;\n /**\n * Restrict the prune to one tenant: a string matches that tenant, `null`\n * matches the global (tenant-less) records. Omit to prune every tenant.\n */\n tenantId?: string | null;\n /** Count matching records without deleting them. */\n dryRun?: boolean;\n /** Clock for the age cutoff (default: now). */\n now?: Date;\n}\n\n/** Name of the AI usage telemetry table. */\nconst AI_USAGE_TABLE = '_smrt_ai_usage';\n\n/** Name of the remembered-context table. */\nconst CONTEXTS_TABLE = '_smrt_contexts';\n\n/**\n * Prune AI usage telemetry to bound `_smrt_ai_usage` growth.\n *\n * `_smrt_ai_usage` gains one row per AI call and persistence is on by default,\n * so a busy deployment writes millions of rows a year with nothing to remove\n * them. Both bounds may be combined; each is applied independently and the\n * returned count is the total deleted.\n *\n * Predicates are covered by `idx_smrt_ai_usage_tenant_created`\n * (`tenant_id, created_at`) — the same index the subscriptions billing meter\n * needs for its `tenant_id + created_at` range scan.\n *\n * @param db - Database holding the system tables.\n * @param retention - At least one of `maxAgeMs` / `maxRows`.\n * @returns Number of records deleted (or, under `dryRun`, matched).\n */\nexport async function pruneAiUsage(\n db: DatabaseInterface,\n retention: AiUsageRetention,\n): Promise<{ pruned: number }> {\n const { maxAgeMs, maxRows, dryRun = false } = retention;\n\n if (maxAgeMs == null && maxRows == null) {\n throw new Error('pruneAiUsage requires maxAgeMs and/or maxRows');\n }\n if (maxAgeMs != null && (!Number.isFinite(maxAgeMs) || maxAgeMs < 0)) {\n throw new Error(`pruneAiUsage maxAgeMs must be >= 0, got ${maxAgeMs}`);\n }\n if (maxRows != null && (!Number.isFinite(maxRows) || maxRows < 0)) {\n throw new Error(`pruneAiUsage maxRows must be >= 0, got ${maxRows}`);\n }\n\n const now = retention.now ?? new Date();\n const scope = tenantScopeClause(retention);\n let pruned = 0;\n\n // Rows the age bound already accounted for. Redundant when they were really\n // deleted, load-bearing under `dryRun`, where they were not — without it the\n // row bound would count the same records a second time.\n const alreadyCounted: { conditions: string[]; params: unknown[] } = {\n conditions: [],\n params: [],\n };\n\n if (maxAgeMs != null) {\n const cutoff = new Date(now.getTime() - maxAgeMs).toISOString();\n pruned += await deleteFrom(\n db,\n AI_USAGE_TABLE,\n [...scope.conditions, 'created_at < ?'],\n [...scope.params, cutoff],\n dryRun,\n );\n alreadyCounted.conditions.push('created_at >= ?');\n alreadyCounted.params.push(cutoff);\n }\n\n if (maxRows != null) {\n const keep = Math.floor(maxRows);\n if (keep === 0) {\n pruned += await deleteFrom(\n db,\n AI_USAGE_TABLE,\n [...scope.conditions, ...alreadyCounted.conditions],\n [...scope.params, ...alreadyCounted.params],\n dryRun,\n );\n } else {\n // The oldest retained row's timestamp is the cutoff. `LIMIT 1 OFFSET n`\n // is portable across every supported engine, unlike the\n // `LIMIT ALL`/`LIMIT -1` spellings of \"all rows past an offset\".\n const boundary = await selectOne(\n db,\n `SELECT created_at FROM ${AI_USAGE_TABLE}\n ${whereClause(scope.conditions)}\n ORDER BY created_at DESC\n LIMIT 1 OFFSET ${keep - 1}`,\n scope.params,\n );\n const cutoff = boundary?.created_at;\n if (cutoff != null) {\n pruned += await deleteFrom(\n db,\n AI_USAGE_TABLE,\n [...scope.conditions, 'created_at < ?', ...alreadyCounted.conditions],\n [...scope.params, cutoff, ...alreadyCounted.params],\n dryRun,\n );\n }\n }\n }\n\n return { pruned };\n}\n\n/**\n * Delete context entries whose `expires_at` has passed.\n *\n * `remember({ expiresAt })` has always stored the expiry and never acted on\n * it. Enforcement is deliberately prune-side only: `recall()`/`recallAll()`\n * keep their documented \"expiry is not applied at read time\" contract, so this\n * bounds storage without changing read semantics for existing callers.\n * (`LearningMemory` filters expired rows itself.)\n *\n * Rows with a `NULL` `expires_at` never expire and are never touched.\n * The predicate is covered by `idx_smrt_contexts_expires_at`.\n *\n * @param db - Database holding the system tables.\n * @param options.now - Clock for the expiry comparison (default: now).\n * @param options.dryRun - Count matching rows without deleting them.\n * @returns Number of expired entries deleted (or, under `dryRun`, matched).\n */\nexport async function pruneExpiredContexts(\n db: DatabaseInterface,\n options: { now?: Date; dryRun?: boolean } = {},\n): Promise<{ pruned: number }> {\n const now = (options.now ?? new Date()).toISOString();\n const pruned = await deleteFrom(\n db,\n CONTEXTS_TABLE,\n ['expires_at IS NOT NULL', 'expires_at < ?'],\n [now],\n options.dryRun ?? false,\n );\n return { pruned };\n}\n\n// ============================================================================\n// Sweep\n// ============================================================================\n\n/** Aggregate outcome of one {@link runRetentionSweep} call. */\nexport interface RetentionSweepResult {\n /** Whether this run only counted rows. */\n dryRun: boolean;\n /** Sweep start time, ISO-8601. */\n startedAt: string;\n /** Wall-clock duration of the sweep. */\n durationMs: number;\n /** Total rows pruned (or matched) across all tasks. */\n pruned: number;\n /** Per-task outcomes, in execution order. */\n tasks: RetentionTaskResult[];\n /** Whether any task failed — the sweep never throws for a task failure. */\n failed: boolean;\n}\n\n/**\n * Apply a {@link RetentionPolicy} across every framework-owned table.\n *\n * Built-in tasks run first, in a fixed order (`changes`, `ai-usage`,\n * `contexts`, `dispatch`), followed by tasks registered via\n * {@link registerRetentionTask} in name order. Tables that do not exist in\n * this database are reported as `skipped: 'unavailable'` rather than failing —\n * a sweep must be safe to run against a partially bootstrapped database.\n *\n * @param db - Database holding the system tables.\n * @param policy - Overrides on {@link DEFAULT_RETENTION_POLICY}.\n * @returns Per-task and aggregate counts; never throws for a task failure.\n */\nexport async function runRetentionSweep(\n db: DatabaseInterface,\n policy: RetentionPolicy = {},\n): Promise<RetentionSweepResult> {\n const startedAt = new Date();\n const dryRun = policy.dryRun ?? false;\n\n if (policy.enabled === false) {\n return {\n dryRun,\n startedAt: startedAt.toISOString(),\n durationMs: 0,\n pruned: 0,\n tasks: [],\n failed: false,\n };\n }\n\n const context: RetentionTaskContext = { dryRun, now: startedAt };\n const tasks: RetentionTaskResult[] = [];\n\n for (const builtIn of builtInTasks(policy)) {\n if (!builtIn.task) {\n tasks.push({ task: builtIn.name, pruned: 0, skipped: 'disabled' });\n continue;\n }\n tasks.push(await runTask(db, builtIn.table, builtIn.task, context));\n }\n\n for (const task of getRetentionTasks()) {\n if (policy.tasks?.[task.name] === false) {\n tasks.push({ task: task.name, pruned: 0, skipped: 'disabled' });\n continue;\n }\n tasks.push(await runTask(db, null, task, context));\n }\n\n const pruned = tasks.reduce((total, task) => total + task.pruned, 0);\n const failed = tasks.some((task) => task.error !== undefined);\n\n logger.info('[smrt] retention sweep complete', {\n dryRun,\n pruned,\n failed,\n tasks: tasks.map((task) => `${task.task}=${task.pruned}`).join(' '),\n });\n\n return {\n dryRun,\n startedAt: startedAt.toISOString(),\n durationMs: Date.now() - startedAt.getTime(),\n pruned,\n tasks,\n failed,\n };\n}\n\n/**\n * Run one task, converting an absent table or a thrown error into a result.\n *\n * `requiredTable` is checked before the task runs so a sweep against a\n * database that never bootstrapped a given system table reports\n * `unavailable` instead of surfacing an engine-specific \"no such table\".\n */\nasync function runTask(\n db: DatabaseInterface,\n requiredTable: string | null,\n task: RetentionTask,\n context: RetentionTaskContext,\n): Promise<RetentionTaskResult> {\n try {\n if (requiredTable && !(await tableExists(db, requiredTable))) {\n return { task: task.name, pruned: 0, skipped: 'unavailable' };\n }\n\n const outcome = await task.run(db, context);\n return typeof outcome === 'number'\n ? { task: task.name, pruned: outcome }\n : { task: task.name, pruned: outcome.pruned, details: outcome.details };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n logger.warn(`[smrt] retention task \"${task.name}\" failed: ${message}`);\n return { task: task.name, pruned: 0, error: message };\n }\n}\n\n/** Resolve a per-table policy entry, or `null` when it is opted out. */\nfunction resolveEntry<T extends { enabled?: boolean }>(\n entry: T | false | undefined,\n defaults: T,\n): T | null {\n if (entry === false) return null;\n const merged = { ...defaults, ...(entry ?? {}) };\n return merged.enabled === false ? null : merged;\n}\n\n/** A built-in task plus the table it needs and whether policy enabled it. */\ninterface BuiltInRetentionTask {\n name: string;\n table: string;\n task: RetentionTask | null;\n}\n\n/** The framework-owned tasks, in their fixed execution order. */\nfunction builtInTasks(policy: RetentionPolicy): BuiltInRetentionTask[] {\n return [\n { name: 'changes', table: '_smrt_changes', task: changeFeedTask(policy) },\n { name: 'ai-usage', table: AI_USAGE_TABLE, task: aiUsageTask(policy) },\n { name: 'contexts', table: CONTEXTS_TABLE, task: contextsTask(policy) },\n { name: 'dispatch', table: '_smrt_dispatch', task: dispatchTask(policy) },\n ];\n}\n\nfunction changeFeedTask(policy: RetentionPolicy): RetentionTask | null {\n const config = resolveEntry<ChangeFeedRetentionPolicy>(\n policy.changes,\n DEFAULT_RETENTION_POLICY.changes,\n );\n if (!config) return null;\n\n return {\n name: 'changes',\n description: 'Prune the append-only change feed (_smrt_changes)',\n run: async (db, context) => {\n const { pruned } = await pruneChangeFeed(db, {\n maxAgeMs:\n config.maxAgeDays == null\n ? undefined\n : config.maxAgeDays * MS_PER_DAY,\n maxRows: config.maxRows,\n dryRun: context.dryRun,\n });\n return pruned;\n },\n };\n}\n\nfunction aiUsageTask(policy: RetentionPolicy): RetentionTask | null {\n const config = resolveEntry<AiUsageRetentionPolicy>(\n policy.aiUsage,\n DEFAULT_RETENTION_POLICY.aiUsage,\n );\n if (!config) return null;\n\n return {\n name: 'ai-usage',\n description: 'Prune AI usage telemetry (_smrt_ai_usage)',\n run: async (db, context) => {\n const { pruned } = await pruneAiUsage(db, {\n maxAgeMs:\n config.maxAgeDays == null\n ? undefined\n : config.maxAgeDays * MS_PER_DAY,\n maxRows: config.maxRows,\n dryRun: context.dryRun,\n now: context.now,\n });\n return pruned;\n },\n };\n}\n\nfunction contextsTask(policy: RetentionPolicy): RetentionTask | null {\n const config = resolveEntry<ContextsRetentionPolicy>(\n policy.contexts,\n DEFAULT_RETENTION_POLICY.contexts,\n );\n if (!config) return null;\n\n return {\n name: 'contexts',\n description: 'Delete expired context entries (_smrt_contexts)',\n run: async (db, context) => {\n const { pruned } = await pruneExpiredContexts(db, {\n now: context.now,\n dryRun: context.dryRun,\n });\n return pruned;\n },\n };\n}\n\nfunction dispatchTask(policy: RetentionPolicy): RetentionTask | null {\n const config = resolveEntry<DispatchRetentionPolicy>(\n policy.dispatch,\n DEFAULT_RETENTION_POLICY.dispatch,\n );\n if (!config) return null;\n\n return {\n name: 'dispatch',\n description: 'Prune processed dispatch queue rows (_smrt_dispatch)',\n run: async (db, context) => {\n // Imported lazily: the dispatch collection pulls in the whole dispatch\n // module graph, which this module must not force onto callers that only\n // prune system tables.\n const { DispatchCollection } = await import(\n '../dispatch/collections/Dispatches.js'\n );\n const result = await DispatchCollection.cleanup(db, {\n completedOlderThanDays: config.completedOlderThanDays,\n failedOlderThanDays: config.failedOlderThanDays,\n dryRun: context.dryRun,\n });\n return {\n pruned: result.completedDeleted + result.failedDeleted,\n details: {\n completed: result.completedDeleted,\n failed: result.failedDeleted,\n },\n };\n },\n };\n}\n\n// ============================================================================\n// SQL helpers\n// ============================================================================\n\nfunction getQueryRows(result: unknown): Record<string, unknown>[] {\n if (Array.isArray(result)) {\n return result as Record<string, unknown>[];\n }\n if (result && typeof result === 'object' && 'rows' in result) {\n const rows = (result as { rows?: unknown }).rows;\n if (Array.isArray(rows)) {\n return rows as Record<string, unknown>[];\n }\n }\n return [];\n}\n\nfunction getDatabaseUrl(db: DatabaseInterface): string {\n return db.url || (db as { config?: { url?: string } }).config?.url || '';\n}\n\n/**\n * Rewrite `?` placeholders to `$n` on PostgreSQL.\n *\n * Retention SQL is written once in the portable `?` form; only the binding\n * syntax differs between engines.\n */\nfunction bindPlaceholders(db: DatabaseInterface, sql: string): string {\n const engine = detectEngine(\n getDatabaseUrl(db),\n (db as { type?: string }).type,\n );\n if (engine !== 'postgres') return sql;\n\n let index = 0;\n return sql.replace(/\\?/g, () => `$${++index}`);\n}\n\nfunction whereClause(conditions: string[]): string {\n return conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';\n}\n\nfunction tenantScopeClause(retention: AiUsageRetention): {\n conditions: string[];\n params: unknown[];\n} {\n if (!('tenantId' in retention)) return { conditions: [], params: [] };\n if (retention.tenantId === undefined) return { conditions: [], params: [] };\n if (retention.tenantId === null) {\n return { conditions: ['tenant_id IS NULL'], params: [] };\n }\n return { conditions: ['tenant_id = ?'], params: [retention.tenantId] };\n}\n\nasync function selectOne(\n db: DatabaseInterface,\n sql: string,\n params: unknown[],\n): Promise<Record<string, unknown> | undefined> {\n const rows = getQueryRows(\n await db.query(bindPlaceholders(db, sql), ...params),\n );\n return rows[0];\n}\n\n/**\n * Count matching rows, then delete them unless this is a dry run.\n *\n * Counting first gives a usable number on every adapter — `rowCount` is not\n * reliably populated across the engines SMRT supports — and is what makes\n * `dryRun` a genuine preview of the same predicate rather than an estimate of\n * a different one.\n *\n * The count and the delete are two statements and deliberately not a\n * transaction: a maintenance sweep must not hold a write lock over a large\n * delete. The returned figure is therefore approximate under concurrent\n * writers, exactly as `pruneChangeFeed` documents.\n */\nasync function deleteFrom(\n db: DatabaseInterface,\n table: string,\n conditions: string[],\n params: unknown[],\n dryRun: boolean,\n): Promise<number> {\n const where = whereClause(conditions);\n const countRow = await selectOne(\n db,\n `SELECT COUNT(*) AS total FROM ${table} ${where}`,\n params,\n );\n const total = toSafeInteger(countRow?.total ?? 0, 'Retention count');\n if (total <= 0) return 0;\n\n if (!dryRun) {\n await db.query(\n bindPlaceholders(db, `DELETE FROM ${table} ${where}`),\n ...params,\n );\n }\n\n return total;\n}\n\n/** Best-effort table probe used to report `unavailable` rather than throwing. */\nasync function tableExists(\n db: DatabaseInterface,\n table: string,\n): Promise<boolean> {\n try {\n if (typeof db.tableExists === 'function') {\n return await db.tableExists(table);\n }\n } catch {\n return false;\n }\n\n try {\n await db.query(`SELECT 1 FROM ${table} LIMIT 1`);\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"retention.js","names":[],"sources":["../../src/system/retention.ts"],"sourcesContent":["/**\n * System-table retention — bounded growth for framework-owned tables (#2375).\n *\n * Several `_smrt_*` tables are append-only by construction and were, until\n * this module, unbounded by default: `_smrt_changes` grew one row per\n * framework save/delete, `_smrt_ai_usage` one row per AI call (persistence is\n * on by default, see `config.ts`), `_smrt_contexts` accumulated rows whose\n * `expires_at` nothing ever enforced, `_smrt_dispatch` retained completed\n * work until an operator remembered to run `smrt dispatch:cleanup`, and\n * `_smrt_run_once_claims` (#3080) gains one row per `runOnce()` submission\n * with nothing pruning completed ones.\n *\n * This module supplies the missing half:\n *\n * - {@link pruneAiUsage} — the retention API `_smrt_ai_usage` never had.\n * - {@link pruneExpiredContexts} — enforcement for `_smrt_contexts.expires_at`.\n * - {@link runRetentionSweep} — one entry point that applies a documented,\n * opt-out {@link RetentionPolicy} across every framework-owned table, plus\n * any task other packages contribute via {@link registerRetentionTask}\n * (`@happyvertical/smrt-jobs` registers job/job-event cleanup;\n * `@happyvertical/smrt-users` registers session and token expiry).\n *\n * Scheduling is the caller's: `smrt db:prune` is the cron entry point, and a\n * running `TaskRunner` sweeps on an interval unless configured otherwise.\n *\n * ## Failure policy\n *\n * A sweep is maintenance, not a transaction. One failing task never aborts the\n * rest — the failure is captured on that task's {@link RetentionTaskResult}\n * and the sweep continues. Callers decide what a partial sweep means; the CLI\n * exits non-zero when any task failed.\n *\n * ## Predicate indexes\n *\n * Every predicate below is indexed by the system DDL in `system/schema.ts`\n * (`_smrt_contexts(expires_at)`, `_smrt_ai_usage(tenant_id, created_at)`,\n * `_smrt_dispatch(status, processed_at)` / `(status, updated_at)`,\n * `_smrt_run_once_claims(status, completed_at)`) or by the jobs compatibility\n * path in `system/compatibility.ts` (`_smrt_jobs(status, completed_at)`,\n * `_smrt_job_events(created_at)`). A new retention predicate must ship with\n * its index.\n *\n * @see https://github.com/happyvertical/smrt/issues/2375\n * @packageDocumentation\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { pruneChangeFeed } from '../change-feed.js';\nimport { detectEngine } from '../schema/ddl/index.js';\nimport { toSafeInteger } from '../utils/safe-integer.js';\nimport { RUN_ONCE_CLAIMS_TABLE } from './schema.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/** Milliseconds in a day — the unit every retention policy is expressed in. */\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\n\n// ============================================================================\n// Policy\n// ============================================================================\n\n/** Retention for the append-only change feed (`_smrt_changes`). */\nexport interface ChangeFeedRetentionPolicy {\n /** Run this task (default `true`). */\n enabled?: boolean;\n /** Drop entries older than this many days (default 30). */\n maxAgeDays?: number;\n /** Additionally keep at most this many newest entries (default: unbounded). */\n maxRows?: number;\n}\n\n/** Retention for AI usage telemetry (`_smrt_ai_usage`). */\nexport interface AiUsageRetentionPolicy {\n /** Run this task (default `true`). */\n enabled?: boolean;\n /** Drop records older than this many days (default 90). */\n maxAgeDays?: number;\n /** Additionally keep at most this many newest records (default: unbounded). */\n maxRows?: number;\n}\n\n/** Retention for remembered context entries (`_smrt_contexts`). */\nexport interface ContextsRetentionPolicy {\n /** Run this task (default `true`). */\n enabled?: boolean;\n}\n\n/** Retention for the dispatch queue (`_smrt_dispatch`). */\nexport interface DispatchRetentionPolicy {\n /** Run this task (default `true`). */\n enabled?: boolean;\n /** Drop completed dispatches processed more than this many days ago (default 30). */\n completedOlderThanDays?: number;\n /** Drop failed dispatches last touched more than this many days ago (default 90). */\n failedOlderThanDays?: number;\n}\n\n/**\n * Retention for `runOnce()` idempotency claims (`_smrt_run_once_claims`, #3080).\n *\n * Only `status: 'completed'` rows with a `completed_at` older than the window\n * are ever deleted — an `'in_progress'` claim has no `completed_at` and is\n * never touched by this task regardless of age. See\n * {@link pruneRunOnceClaims} and `agents/run-once.md` for the consequence of\n * this window: a caller that replays a `runOnce()` token after its claim has\n * been swept runs `work()` again instead of replaying the stored result, so\n * `maxAgeDays` must stay comfortably longer than any realistic client retry\n * window.\n */\nexport interface RunOnceClaimsRetentionPolicy {\n /** Run this task (default `true`). */\n enabled?: boolean;\n /** Drop completed claims older than this many days (default 30). */\n maxAgeDays?: number;\n}\n\n/**\n * Retention policy for a sweep.\n *\n * Every table is pruned by default; set a table's entry to `false` (or\n * `{ enabled: false }`) to opt that table out, or `enabled: false` at the top\n * level to opt the whole sweep out.\n */\nexport interface RetentionPolicy {\n /** Run the sweep at all (default `true`). */\n enabled?: boolean;\n /** Report what would be pruned without deleting anything (default `false`). */\n dryRun?: boolean;\n /** `_smrt_changes` retention, or `false` to skip. */\n changes?: ChangeFeedRetentionPolicy | false;\n /** `_smrt_ai_usage` retention, or `false` to skip. */\n aiUsage?: AiUsageRetentionPolicy | false;\n /** `_smrt_contexts` expiry enforcement, or `false` to skip. */\n contexts?: ContextsRetentionPolicy | false;\n /** `_smrt_dispatch` retention, or `false` to skip. */\n dispatch?: DispatchRetentionPolicy | false;\n /** `_smrt_run_once_claims` retention, or `false` to skip. */\n runOnceClaims?: RunOnceClaimsRetentionPolicy | false;\n /**\n * Enable/disable individual tasks contributed through\n * {@link registerRetentionTask}, keyed by task name. Unlisted tasks run.\n */\n tasks?: Record<string, boolean>;\n}\n\n/**\n * Documented retention defaults.\n *\n * Chosen to be generous relative to every in-tree consumer: change-feed\n * cursors resync automatically past the window, AI usage outlives a monthly\n * billing cycle by two months, and dispatch keeps a full quarter of failures\n * for forensics. Applications that need longer history raise these; those that\n * need none set the table to `false`.\n */\nexport const DEFAULT_RETENTION_POLICY: {\n changes: ChangeFeedRetentionPolicy;\n aiUsage: AiUsageRetentionPolicy;\n contexts: ContextsRetentionPolicy;\n dispatch: DispatchRetentionPolicy;\n runOnceClaims: RunOnceClaimsRetentionPolicy;\n} = {\n changes: { maxAgeDays: 30 },\n aiUsage: { maxAgeDays: 90 },\n contexts: {},\n dispatch: { completedOlderThanDays: 30, failedOlderThanDays: 90 },\n // 30 days is generous relative to any realistic runOnce() client retry —\n // see RunOnceClaimsRetentionPolicy for the replay consequence of a shorter\n // window.\n runOnceClaims: { maxAgeDays: 30 },\n};\n\n// ============================================================================\n// Tasks\n// ============================================================================\n\n/** Per-run inputs handed to every retention task. */\nexport interface RetentionTaskContext {\n /** Count what would be deleted instead of deleting it. */\n dryRun: boolean;\n /** Sweep start time — every task derives its cutoffs from this one clock. */\n now: Date;\n}\n\n/** What one task did (or why it did nothing). */\nexport interface RetentionTaskResult {\n /** Task name, unique within a sweep. */\n task: string;\n /** Rows deleted, or — under `dryRun` — rows that would be deleted. */\n pruned: number;\n /**\n * Why the task did no work: `disabled` (policy opted it out) or\n * `unavailable` (the table does not exist in this database).\n */\n skipped?: 'disabled' | 'unavailable';\n /** Message of the error this task failed with, if any. */\n error?: string;\n /** Optional per-bucket breakdown (e.g. completed vs failed dispatches). */\n details?: Record<string, number>;\n}\n\n/** What a task returns; a bare number is shorthand for `{ pruned }`. */\nexport type RetentionTaskOutcome =\n | number\n | { pruned: number; details?: Record<string, number> };\n\n/**\n * A retention task contributed by a package that owns its own tables.\n *\n * Tasks must honour `context.dryRun` — a task that deletes during a dry run\n * makes the whole preview a lie.\n */\nexport interface RetentionTask {\n /** Unique name; also the {@link RetentionPolicy.tasks} opt-out key. */\n name: string;\n /** One-line description surfaced by `smrt db:prune --json`. */\n description?: string;\n /** Perform (or, under `dryRun`, count) the deletion. */\n run(\n db: DatabaseInterface,\n context: RetentionTaskContext,\n ): Promise<RetentionTaskOutcome>;\n}\n\nconst RETENTION_TASKS_KEY = Symbol.for('smrt.retention-tasks');\n\n/**\n * Names {@link builtInTasks} always uses for the five framework-owned tables.\n *\n * Reserved against contributed tasks: `RetentionTaskResult.task` is\n * documented unique within a sweep, and a contributed task sharing one of\n * these names would collide with a built-in result — silently, since nothing\n * else in the sweep loop checks for it — and would also be unreachable\n * through {@link RetentionPolicy.tasks}, which only ever sees the *last*\n * task run under that name, never the built-in one (opting a built-in table\n * out goes through its own dedicated policy field instead).\n */\nconst RESERVED_TASK_NAMES = new Set([\n 'changes',\n 'ai-usage',\n 'contexts',\n 'dispatch',\n 'run-once-claims',\n]);\n\n/**\n * The task registry, held on `globalThis` rather than in module scope.\n *\n * Contributing packages resolve their own copy of `@happyvertical/smrt-core`\n * whenever a consumer's install is not fully deduped, and the CLI resolves a\n * third. A module-local `Map` would give each of them a private, empty\n * registry — the task would register in one and the sweep would run in\n * another. `ObjectRegistry` is on `globalThis` for the same reason.\n */\nfunction registeredTasks(): Map<string, RetentionTask> {\n const root = globalThis as typeof globalThis & {\n [RETENTION_TASKS_KEY]?: Map<string, RetentionTask>;\n };\n root[RETENTION_TASKS_KEY] ??= new Map<string, RetentionTask>();\n return root[RETENTION_TASKS_KEY];\n}\n\n/**\n * Register a retention task so {@link runRetentionSweep} includes it.\n *\n * Registering a task does not schedule anything — a task only runs when\n * something calls {@link runRetentionSweep}, and the policy can still turn it\n * off by name. Re-registering the same name replaces the previous task, which\n * keeps module re-evaluation (HMR, repeated test imports) idempotent.\n *\n * @throws If `task.name` is empty, or is one of the five built-in table\n * names (`changes`, `ai-usage`, `contexts`, `dispatch`, `run-once-claims`)\n * — see {@link RESERVED_TASK_NAMES}.\n */\nexport function registerRetentionTask(task: RetentionTask): void {\n if (!task.name) {\n throw new Error('registerRetentionTask requires a non-empty task name');\n }\n if (RESERVED_TASK_NAMES.has(task.name)) {\n throw new Error(\n `registerRetentionTask: \"${task.name}\" is a built-in task name (reserved: ${[...RESERVED_TASK_NAMES].join(', ')})`,\n );\n }\n registeredTasks().set(task.name, task);\n}\n\n/** Remove a registered retention task. Returns `true` if one was removed. */\nexport function unregisterRetentionTask(name: string): boolean {\n return registeredTasks().delete(name);\n}\n\n/** Registered retention tasks, ordered by name for deterministic sweeps. */\nexport function getRetentionTasks(): RetentionTask[] {\n return [...registeredTasks().values()].sort((left, right) =>\n left.name.localeCompare(right.name),\n );\n}\n\n/** Drop every registered retention task (test helper). */\nexport function clearRetentionTasks(): void {\n registeredTasks().clear();\n}\n\n// ============================================================================\n// AI usage retention\n// ============================================================================\n\n/** Bounds for {@link pruneAiUsage}. At least one bound is required. */\nexport interface AiUsageRetention {\n /** Delete records older than this many milliseconds. */\n maxAgeMs?: number;\n /**\n * Keep at most this many newest records (by `created_at`).\n *\n * A ceiling, not a floor: combined with `maxAgeMs`, the two bounds union\n * — a row past the age cutoff is deleted regardless of `maxRows`, so\n * `maxRows` never resurrects rows the age bound already excludes. If every\n * row is already older than `maxAgeMs`, a finite `maxRows` keeps none of\n * them; it does not guarantee `maxRows` survivors independent of age. (A\n * `maxRows` that overrode age this way would defeat the reason `maxAgeMs`\n * usually exists in the first place — a compliance/privacy deletion\n * deadline that a row-count floor should not be able to postpone.)\n *\n * Rows sharing the cutoff timestamp all survive, so the retained count can\n * exceed `maxRows` by the size of one timestamp tie.\n */\n maxRows?: number;\n /**\n * Restrict the prune to one tenant: a string matches that tenant, `null`\n * matches the global (tenant-less) records. Omit to prune every tenant.\n */\n tenantId?: string | null;\n /** Count matching records without deleting them. */\n dryRun?: boolean;\n /** Clock for the age cutoff (default: now). */\n now?: Date;\n}\n\n/** Name of the AI usage telemetry table. */\nconst AI_USAGE_TABLE = '_smrt_ai_usage';\n\n/** Name of the remembered-context table. */\nconst CONTEXTS_TABLE = '_smrt_contexts';\n\n/**\n * Prune AI usage telemetry to bound `_smrt_ai_usage` growth.\n *\n * `_smrt_ai_usage` gains one row per AI call and persistence is on by default,\n * so a busy deployment writes millions of rows a year with nothing to remove\n * them. Both bounds may be combined; each is applied independently and the\n * returned count is the total deleted.\n *\n * Predicates are covered by `idx_smrt_ai_usage_tenant_created`\n * (`tenant_id, created_at`) — the same index the subscriptions billing meter\n * needs for its `tenant_id + created_at` range scan.\n *\n * @param db - Database holding the system tables.\n * @param retention - At least one of `maxAgeMs` / `maxRows`.\n * @returns Number of records deleted (or, under `dryRun`, matched).\n */\nexport async function pruneAiUsage(\n db: DatabaseInterface,\n retention: AiUsageRetention,\n): Promise<{ pruned: number }> {\n const { maxAgeMs, maxRows, dryRun = false } = retention;\n\n if (maxAgeMs == null && maxRows == null) {\n throw new Error('pruneAiUsage requires maxAgeMs and/or maxRows');\n }\n if (maxAgeMs != null && (!Number.isFinite(maxAgeMs) || maxAgeMs < 0)) {\n throw new Error(`pruneAiUsage maxAgeMs must be >= 0, got ${maxAgeMs}`);\n }\n if (maxRows != null && (!Number.isFinite(maxRows) || maxRows < 0)) {\n throw new Error(`pruneAiUsage maxRows must be >= 0, got ${maxRows}`);\n }\n\n const now = retention.now ?? new Date();\n const scope = tenantScopeClause(retention);\n let pruned = 0;\n\n // Rows the age bound already accounted for. Redundant when they were really\n // deleted, load-bearing under `dryRun`, where they were not — without it the\n // row bound would count the same records a second time.\n const alreadyCounted: { conditions: string[]; params: unknown[] } = {\n conditions: [],\n params: [],\n };\n\n if (maxAgeMs != null) {\n const cutoff = new Date(now.getTime() - maxAgeMs).toISOString();\n pruned += await deleteFrom(\n db,\n AI_USAGE_TABLE,\n [...scope.conditions, 'created_at < ?'],\n [...scope.params, cutoff],\n dryRun,\n );\n alreadyCounted.conditions.push('created_at >= ?');\n alreadyCounted.params.push(cutoff);\n }\n\n if (maxRows != null) {\n const keep = Math.floor(maxRows);\n if (keep === 0) {\n pruned += await deleteFrom(\n db,\n AI_USAGE_TABLE,\n [...scope.conditions, ...alreadyCounted.conditions],\n [...scope.params, ...alreadyCounted.params],\n dryRun,\n );\n } else {\n // The oldest retained row's timestamp is the cutoff. `LIMIT 1 OFFSET n`\n // is portable across every supported engine, unlike the\n // `LIMIT ALL`/`LIMIT -1` spellings of \"all rows past an offset\".\n const boundary = await selectOne(\n db,\n `SELECT created_at FROM ${AI_USAGE_TABLE}\n ${whereClause(scope.conditions)}\n ORDER BY created_at DESC\n LIMIT 1 OFFSET ${keep - 1}`,\n scope.params,\n );\n const cutoff = boundary?.created_at;\n if (cutoff != null) {\n pruned += await deleteFrom(\n db,\n AI_USAGE_TABLE,\n [...scope.conditions, 'created_at < ?', ...alreadyCounted.conditions],\n [...scope.params, cutoff, ...alreadyCounted.params],\n dryRun,\n );\n }\n }\n }\n\n return { pruned };\n}\n\n/**\n * Delete context entries whose `expires_at` has passed.\n *\n * `remember({ expiresAt })` has always stored the expiry and never acted on\n * it. Enforcement is deliberately prune-side only: `recall()`/`recallAll()`\n * keep their documented \"expiry is not applied at read time\" contract, so this\n * bounds storage without changing read semantics for existing callers.\n * (`LearningMemory` filters expired rows itself.)\n *\n * Rows with a `NULL` `expires_at` never expire and are never touched.\n * The predicate is covered by `idx_smrt_contexts_expires_at`.\n *\n * @param db - Database holding the system tables.\n * @param options.now - Clock for the expiry comparison (default: now).\n * @param options.dryRun - Count matching rows without deleting them.\n * @returns Number of expired entries deleted (or, under `dryRun`, matched).\n */\nexport async function pruneExpiredContexts(\n db: DatabaseInterface,\n options: { now?: Date; dryRun?: boolean } = {},\n): Promise<{ pruned: number }> {\n const now = (options.now ?? new Date()).toISOString();\n const pruned = await deleteFrom(\n db,\n CONTEXTS_TABLE,\n ['expires_at IS NOT NULL', 'expires_at < ?'],\n [now],\n options.dryRun ?? false,\n );\n return { pruned };\n}\n\n// ============================================================================\n// runOnce() claim retention\n// ============================================================================\n\n/** Bounds for {@link pruneRunOnceClaims}. */\nexport interface RunOnceClaimsRetention {\n /** Delete completed claims older than this many milliseconds. */\n maxAgeMs: number;\n /** Count matching records without deleting them. */\n dryRun?: boolean;\n /** Clock for the age cutoff (default: now). */\n now?: Date;\n}\n\n/**\n * Prune completed `runOnce()` idempotency claims to bound\n * `_smrt_run_once_claims` growth (#3080).\n *\n * Only rows with `status = 'completed'` AND a `completed_at` older than the\n * cutoff are deleted. An `'in_progress'` claim has no `completed_at`, so this\n * predicate can never match one — a claim held open by an in-flight (or\n * abandoned) transaction is never pruned by age alone.\n *\n * Predicate is covered by `idx_smrt_run_once_claims_status_completed`\n * (`status, completed_at`).\n *\n * @param db - Database holding the system tables.\n * @param retention - `maxAgeMs` is required; there is no row-count bound.\n * @returns Number of claims deleted (or, under `dryRun`, matched).\n */\nexport async function pruneRunOnceClaims(\n db: DatabaseInterface,\n retention: RunOnceClaimsRetention,\n): Promise<{ pruned: number }> {\n const { maxAgeMs, dryRun = false } = retention;\n\n if (!Number.isFinite(maxAgeMs) || maxAgeMs < 0) {\n throw new Error(\n `pruneRunOnceClaims maxAgeMs must be >= 0, got ${maxAgeMs}`,\n );\n }\n\n const now = retention.now ?? new Date();\n const cutoff = new Date(now.getTime() - maxAgeMs).toISOString();\n const pruned = await deleteFrom(\n db,\n RUN_ONCE_CLAIMS_TABLE,\n [\"status = 'completed'\", 'completed_at IS NOT NULL', 'completed_at < ?'],\n [cutoff],\n dryRun,\n );\n return { pruned };\n}\n\n// ============================================================================\n// Sweep\n// ============================================================================\n\n/** Aggregate outcome of one {@link runRetentionSweep} call. */\nexport interface RetentionSweepResult {\n /** Whether this run only counted rows. */\n dryRun: boolean;\n /** Sweep start time, ISO-8601. */\n startedAt: string;\n /** Wall-clock duration of the sweep. */\n durationMs: number;\n /** Total rows pruned (or matched) across all tasks. */\n pruned: number;\n /** Per-task outcomes, in execution order. */\n tasks: RetentionTaskResult[];\n /** Whether any task failed — the sweep never throws for a task failure. */\n failed: boolean;\n}\n\n/**\n * Apply a {@link RetentionPolicy} across every framework-owned table.\n *\n * Built-in tasks run first, in a fixed order (`changes`, `ai-usage`,\n * `contexts`, `dispatch`, `run-once-claims`), followed by tasks registered via\n * {@link registerRetentionTask} in name order. Tables that do not exist in\n * this database are reported as `skipped: 'unavailable'` rather than failing —\n * a sweep must be safe to run against a partially bootstrapped database.\n *\n * @param db - Database holding the system tables.\n * @param policy - Overrides on {@link DEFAULT_RETENTION_POLICY}.\n * @returns Per-task and aggregate counts; never throws for a task failure.\n */\nexport async function runRetentionSweep(\n db: DatabaseInterface,\n policy: RetentionPolicy = {},\n): Promise<RetentionSweepResult> {\n const startedAt = new Date();\n const dryRun = policy.dryRun ?? false;\n\n if (policy.enabled === false) {\n return {\n dryRun,\n startedAt: startedAt.toISOString(),\n durationMs: 0,\n pruned: 0,\n tasks: [],\n failed: false,\n };\n }\n\n const context: RetentionTaskContext = { dryRun, now: startedAt };\n const tasks: RetentionTaskResult[] = [];\n\n for (const builtIn of builtInTasks(policy)) {\n if (!builtIn.task) {\n tasks.push({ task: builtIn.name, pruned: 0, skipped: 'disabled' });\n continue;\n }\n tasks.push(await runTask(db, builtIn.table, builtIn.task, context));\n }\n\n for (const task of getRetentionTasks()) {\n if (policy.tasks?.[task.name] === false) {\n tasks.push({ task: task.name, pruned: 0, skipped: 'disabled' });\n continue;\n }\n tasks.push(await runTask(db, null, task, context));\n }\n\n const pruned = tasks.reduce((total, task) => total + task.pruned, 0);\n const failed = tasks.some((task) => task.error !== undefined);\n\n logger.info('[smrt] retention sweep complete', {\n dryRun,\n pruned,\n failed,\n tasks: tasks.map((task) => `${task.task}=${task.pruned}`).join(' '),\n });\n\n return {\n dryRun,\n startedAt: startedAt.toISOString(),\n durationMs: Date.now() - startedAt.getTime(),\n pruned,\n tasks,\n failed,\n };\n}\n\n/**\n * Run one task, converting an absent table or a thrown error into a result.\n *\n * `requiredTable` is checked before the task runs so a sweep against a\n * database that never bootstrapped a given system table reports\n * `unavailable` instead of surfacing an engine-specific \"no such table\".\n */\nasync function runTask(\n db: DatabaseInterface,\n requiredTable: string | null,\n task: RetentionTask,\n context: RetentionTaskContext,\n): Promise<RetentionTaskResult> {\n try {\n if (requiredTable && !(await tableExists(db, requiredTable))) {\n return { task: task.name, pruned: 0, skipped: 'unavailable' };\n }\n\n const outcome = await task.run(db, context);\n return typeof outcome === 'number'\n ? { task: task.name, pruned: outcome }\n : { task: task.name, pruned: outcome.pruned, details: outcome.details };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n logger.warn(`[smrt] retention task \"${task.name}\" failed: ${message}`);\n return { task: task.name, pruned: 0, error: message };\n }\n}\n\n/** Resolve a per-table policy entry, or `null` when it is opted out. */\nfunction resolveEntry<T extends { enabled?: boolean }>(\n entry: T | false | undefined,\n defaults: T,\n): T | null {\n if (entry === false) return null;\n const merged = { ...defaults, ...(entry ?? {}) };\n return merged.enabled === false ? null : merged;\n}\n\n/** A built-in task plus the table it needs and whether policy enabled it. */\ninterface BuiltInRetentionTask {\n name: string;\n table: string;\n task: RetentionTask | null;\n}\n\n/** The framework-owned tasks, in their fixed execution order. */\nfunction builtInTasks(policy: RetentionPolicy): BuiltInRetentionTask[] {\n return [\n { name: 'changes', table: '_smrt_changes', task: changeFeedTask(policy) },\n { name: 'ai-usage', table: AI_USAGE_TABLE, task: aiUsageTask(policy) },\n { name: 'contexts', table: CONTEXTS_TABLE, task: contextsTask(policy) },\n { name: 'dispatch', table: '_smrt_dispatch', task: dispatchTask(policy) },\n {\n name: 'run-once-claims',\n table: RUN_ONCE_CLAIMS_TABLE,\n task: runOnceClaimsTask(policy),\n },\n ];\n}\n\nfunction changeFeedTask(policy: RetentionPolicy): RetentionTask | null {\n const config = resolveEntry<ChangeFeedRetentionPolicy>(\n policy.changes,\n DEFAULT_RETENTION_POLICY.changes,\n );\n if (!config) return null;\n\n return {\n name: 'changes',\n description: 'Prune the append-only change feed (_smrt_changes)',\n run: async (db, context) => {\n const { pruned } = await pruneChangeFeed(db, {\n maxAgeMs:\n config.maxAgeDays == null\n ? undefined\n : config.maxAgeDays * MS_PER_DAY,\n maxRows: config.maxRows,\n dryRun: context.dryRun,\n });\n return pruned;\n },\n };\n}\n\nfunction aiUsageTask(policy: RetentionPolicy): RetentionTask | null {\n const config = resolveEntry<AiUsageRetentionPolicy>(\n policy.aiUsage,\n DEFAULT_RETENTION_POLICY.aiUsage,\n );\n if (!config) return null;\n\n return {\n name: 'ai-usage',\n description: 'Prune AI usage telemetry (_smrt_ai_usage)',\n run: async (db, context) => {\n const { pruned } = await pruneAiUsage(db, {\n maxAgeMs:\n config.maxAgeDays == null\n ? undefined\n : config.maxAgeDays * MS_PER_DAY,\n maxRows: config.maxRows,\n dryRun: context.dryRun,\n now: context.now,\n });\n return pruned;\n },\n };\n}\n\nfunction contextsTask(policy: RetentionPolicy): RetentionTask | null {\n const config = resolveEntry<ContextsRetentionPolicy>(\n policy.contexts,\n DEFAULT_RETENTION_POLICY.contexts,\n );\n if (!config) return null;\n\n return {\n name: 'contexts',\n description: 'Delete expired context entries (_smrt_contexts)',\n run: async (db, context) => {\n const { pruned } = await pruneExpiredContexts(db, {\n now: context.now,\n dryRun: context.dryRun,\n });\n return pruned;\n },\n };\n}\n\nfunction dispatchTask(policy: RetentionPolicy): RetentionTask | null {\n const config = resolveEntry<DispatchRetentionPolicy>(\n policy.dispatch,\n DEFAULT_RETENTION_POLICY.dispatch,\n );\n if (!config) return null;\n\n return {\n name: 'dispatch',\n description: 'Prune processed dispatch queue rows (_smrt_dispatch)',\n run: async (db, context) => {\n // Imported lazily: the dispatch collection pulls in the whole dispatch\n // module graph, which this module must not force onto callers that only\n // prune system tables.\n const { DispatchCollection } = await import(\n '../dispatch/collections/Dispatches.js'\n );\n const result = await DispatchCollection.cleanup(db, {\n completedOlderThanDays: config.completedOlderThanDays,\n failedOlderThanDays: config.failedOlderThanDays,\n dryRun: context.dryRun,\n });\n return {\n pruned: result.completedDeleted + result.failedDeleted,\n details: {\n completed: result.completedDeleted,\n failed: result.failedDeleted,\n },\n };\n },\n };\n}\n\nfunction runOnceClaimsTask(policy: RetentionPolicy): RetentionTask | null {\n const config = resolveEntry<RunOnceClaimsRetentionPolicy>(\n policy.runOnceClaims,\n DEFAULT_RETENTION_POLICY.runOnceClaims,\n );\n if (!config) return null;\n\n return {\n name: 'run-once-claims',\n description:\n 'Prune completed runOnce() idempotency claims (_smrt_run_once_claims)',\n run: async (db, context) => {\n const { pruned } = await pruneRunOnceClaims(db, {\n maxAgeMs:\n (config.maxAgeDays ??\n DEFAULT_RETENTION_POLICY.runOnceClaims.maxAgeDays ??\n 30) * MS_PER_DAY,\n dryRun: context.dryRun,\n now: context.now,\n });\n return pruned;\n },\n };\n}\n\n// ============================================================================\n// SQL helpers\n// ============================================================================\n\nfunction getQueryRows(result: unknown): Record<string, unknown>[] {\n if (Array.isArray(result)) {\n return result as Record<string, unknown>[];\n }\n if (result && typeof result === 'object' && 'rows' in result) {\n const rows = (result as { rows?: unknown }).rows;\n if (Array.isArray(rows)) {\n return rows as Record<string, unknown>[];\n }\n }\n return [];\n}\n\nfunction getDatabaseUrl(db: DatabaseInterface): string {\n return db.url || (db as { config?: { url?: string } }).config?.url || '';\n}\n\n/**\n * Rewrite `?` placeholders to `$n` on PostgreSQL.\n *\n * Retention SQL is written once in the portable `?` form; only the binding\n * syntax differs between engines.\n */\nfunction bindPlaceholders(db: DatabaseInterface, sql: string): string {\n const engine = detectEngine(\n getDatabaseUrl(db),\n (db as { type?: string }).type,\n );\n if (engine !== 'postgres') return sql;\n\n let index = 0;\n return sql.replace(/\\?/g, () => `$${++index}`);\n}\n\nfunction whereClause(conditions: string[]): string {\n return conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';\n}\n\nfunction tenantScopeClause(retention: AiUsageRetention): {\n conditions: string[];\n params: unknown[];\n} {\n if (!('tenantId' in retention)) return { conditions: [], params: [] };\n if (retention.tenantId === undefined) return { conditions: [], params: [] };\n if (retention.tenantId === null) {\n return { conditions: ['tenant_id IS NULL'], params: [] };\n }\n return { conditions: ['tenant_id = ?'], params: [retention.tenantId] };\n}\n\nasync function selectOne(\n db: DatabaseInterface,\n sql: string,\n params: unknown[],\n): Promise<Record<string, unknown> | undefined> {\n const rows = getQueryRows(\n await db.query(bindPlaceholders(db, sql), ...params),\n );\n return rows[0];\n}\n\n/**\n * Count matching rows, then delete them unless this is a dry run.\n *\n * Counting first gives a usable number on every adapter — `rowCount` is not\n * reliably populated across the engines SMRT supports — and is what makes\n * `dryRun` a genuine preview of the same predicate rather than an estimate of\n * a different one.\n *\n * The count and the delete are two statements and deliberately not a\n * transaction: a maintenance sweep must not hold a write lock over a large\n * delete. The returned figure is therefore approximate under concurrent\n * writers, exactly as `pruneChangeFeed` documents.\n */\nasync function deleteFrom(\n db: DatabaseInterface,\n table: string,\n conditions: string[],\n params: unknown[],\n dryRun: boolean,\n): Promise<number> {\n const where = whereClause(conditions);\n const countRow = await selectOne(\n db,\n `SELECT COUNT(*) AS total FROM ${table} ${where}`,\n params,\n );\n const total = toSafeInteger(countRow?.total ?? 0, 'Retention count');\n if (total <= 0) return 0;\n\n if (!dryRun) {\n await db.query(\n bindPlaceholders(db, `DELETE FROM ${table} ${where}`),\n ...params,\n );\n }\n\n return total;\n}\n\n/** Best-effort table probe used to report `unavailable` rather than throwing. */\nasync function tableExists(\n db: DatabaseInterface,\n table: string,\n): Promise<boolean> {\n try {\n if (typeof db.tableExists === 'function') {\n return await db.tableExists(table);\n }\n } catch {\n return false;\n }\n\n try {\n await db.query(`SELECT 1 FROM ${table} LIMIT 1`);\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":"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"}
|
package/dist/system/schema.d.ts
CHANGED
|
@@ -193,6 +193,31 @@ export declare const ENSURE_POSTGRES_CHANGE_FEED_SCHEMA: string;
|
|
|
193
193
|
* by name and the tracker handles idempotency.
|
|
194
194
|
*/
|
|
195
195
|
export declare const CREATE_SMRT_BACKFILLS_TABLE = "\nCREATE TABLE IF NOT EXISTS _smrt_backfills (\n name TEXT PRIMARY KEY,\n applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n description TEXT,\n package_name TEXT\n);\n";
|
|
196
|
+
/** The insert-only claim table backing `runOnce()` (`../run-once.ts`). Browser-safe: retention imports it. */
|
|
197
|
+
export declare const RUN_ONCE_CLAIMS_TABLE = "_smrt_run_once_claims";
|
|
198
|
+
/**
|
|
199
|
+
* Idempotency claims for `runOnce()` (#3080).
|
|
200
|
+
*
|
|
201
|
+
* `claim_key` is the sha256 hex digest of the sorted-key-JSON encoding of
|
|
202
|
+
* `(tenantId, actor, token, contentDigest)` — see `../run-once.ts` for the
|
|
203
|
+
* derivation and why the token alone is not enough (it collapses distinct
|
|
204
|
+
* submissions that reuse a form's token). It is the table's PRIMARY KEY, so
|
|
205
|
+
* a second `INSERT` for the same key always raises a unique-constraint
|
|
206
|
+
* violation instead of silently adopting the first caller's row: exactly the
|
|
207
|
+
* "insert-only against a UNIQUE column" claim the issue asks for.
|
|
208
|
+
*
|
|
209
|
+
* Neither the raw token nor the raw submitted content is stored — only
|
|
210
|
+
* digests — so this table cannot leak submitted business data. `status`
|
|
211
|
+
* moves from `'in_progress'` to `'completed'` inside the SAME transaction
|
|
212
|
+
* that runs the caller's work, so `result` (the caller's JSON-serialized,
|
|
213
|
+
* ready-to-replay return value) and `completed_at` are only ever set once,
|
|
214
|
+
* atomically with whatever else that transaction wrote. If the work throws,
|
|
215
|
+
* `runOnce()` never marks the claim completed, and the transaction that
|
|
216
|
+
* inserted it rolls back the insert along with it — a claim is never left
|
|
217
|
+
* permanently `'in_progress'` by a failed attempt; a retry with the same key
|
|
218
|
+
* can always run again.
|
|
219
|
+
*/
|
|
220
|
+
export declare const CREATE_SMRT_RUN_ONCE_CLAIMS_TABLE = "\nCREATE TABLE IF NOT EXISTS _smrt_run_once_claims (\n claim_key TEXT PRIMARY KEY,\n tenant_id TEXT NOT NULL,\n actor TEXT NOT NULL,\n content_digest TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'in_progress',\n result TEXT,\n created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n completed_at TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_run_once_claims_tenant\n ON _smrt_run_once_claims(tenant_id, created_at);\n\n-- Retention predicate for pruneRunOnceClaims() (#3080): completed claims are\n-- aged out on completed_at, scoped to status = 'completed' so an in_progress\n-- claim (completed_at IS NULL) is never matched by the sweep.\nCREATE INDEX IF NOT EXISTS idx_smrt_run_once_claims_status_completed\n ON _smrt_run_once_claims(status, completed_at);\n";
|
|
196
221
|
/**
|
|
197
222
|
* All system table creation statements
|
|
198
223
|
*/
|
|
@@ -271,8 +296,15 @@ export declare function getSystemTableDDLForEngine(ddl: string, engine: Database
|
|
|
271
296
|
* sets — never published, never stamped in any real `_smrt_migrations` table
|
|
272
297
|
* — so merging them reuses no version an existing install could have already
|
|
273
298
|
* recorded; see the note on {@link SMRT_SCHEMA_DDL_CHECKSUMS}.)
|
|
299
|
+
*
|
|
300
|
+
* 1.11.0 adds `_smrt_run_once_claims` (#3080), the insert-only claim table
|
|
301
|
+
* behind `runOnce()` (`../run-once.ts`).
|
|
302
|
+
*
|
|
303
|
+
* 1.11.1 adds `idx_smrt_run_once_claims_status_completed` (#3080), the
|
|
304
|
+
* `(status, completed_at)` index backing `pruneRunOnceClaims()`'s retention
|
|
305
|
+
* predicate (`system/retention.ts`).
|
|
274
306
|
*/
|
|
275
|
-
export declare const SMRT_SCHEMA_VERSION = "1.
|
|
307
|
+
export declare const SMRT_SCHEMA_VERSION = "1.11.1";
|
|
276
308
|
/**
|
|
277
309
|
* Canonical form of the system DDL that {@link SMRT_SCHEMA_DDL_CHECKSUMS} covers.
|
|
278
310
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/system/schema.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D;;;GAGG;AACH,eAAO,MAAM,0BAA0B,qpCAoCtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,4MAQxC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mCAAmC,o0BA4B/C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,u0BA2BxC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,0BAA0B,igDAiDtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,wCAAwC,69CAkCpD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,0BAA0B,ilCAkCtC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,yBAAyB,weAkBrC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,0CAA0C,uWAYtD,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,kCAAkC,0BAA0B,CAAC;AAE1E,wEAAwE;AACxE,eAAO,MAAM,kCAAkC,QACqB,CAAC;AAErE,0EAA0E;AAC1E,eAAO,MAAM,yCAAyC,wBAAwB,CAAC;AAE/E,gFAAgF;AAChF,eAAO,MAAM,+CAA+C,yBACpC,CAAC;AAEzB,iFAAiF;AACjF,eAAO,MAAM,oDAAoD,yEAAkG,CAAC;AAEpK,8EAA8E;AAC9E,eAAO,MAAM,6CAA6C,sEAA+F,CAAC;AAE1J,oEAAoE;AACpE,eAAO,MAAM,mDAAmD,gCAA8D,CAAC;AAE/H;;;;;;;;GAQG;AACH,eAAO,MAAM,kCAAkC,gCAAgC,CAAC;AAEhF,2EAA2E;AAC3E,eAAO,MAAM,wCAAwC,wBAAwB,CAAC;AAE9E,6EAA6E;AAC7E,eAAO,MAAM,4CAA4C,iCAAyD,CAAC;AAEnH,wDAAwD;AACxD,eAAO,MAAM,gCAAgC,OAAO,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,0CAA0C,grHAiItD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,2CAA2C,u5IA4HvD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,iDAAiD,yqFA0F7D,CAAC;AAaF;;;;;;GAMG;AACH,eAAO,MAAM,4CAA4C,QAoBxD,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,2CAA2C,QAmCvD,CAAC;AAaF;;;;;;GAMG;AACH,eAAO,MAAM,kCAAkC,QAoC9C,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B,gLAOvC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,iBAAiB,
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/system/schema.ts"],"names":[],"mappings":"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"}
|
package/dist/system/schema.js
CHANGED
|
@@ -885,6 +885,51 @@ CREATE TABLE IF NOT EXISTS _smrt_backfills (
|
|
|
885
885
|
package_name TEXT
|
|
886
886
|
);
|
|
887
887
|
`;
|
|
888
|
+
/** The insert-only claim table backing `runOnce()` (`../run-once.ts`). Browser-safe: retention imports it. */
|
|
889
|
+
var RUN_ONCE_CLAIMS_TABLE = "_smrt_run_once_claims";
|
|
890
|
+
/**
|
|
891
|
+
* Idempotency claims for `runOnce()` (#3080).
|
|
892
|
+
*
|
|
893
|
+
* `claim_key` is the sha256 hex digest of the sorted-key-JSON encoding of
|
|
894
|
+
* `(tenantId, actor, token, contentDigest)` — see `../run-once.ts` for the
|
|
895
|
+
* derivation and why the token alone is not enough (it collapses distinct
|
|
896
|
+
* submissions that reuse a form's token). It is the table's PRIMARY KEY, so
|
|
897
|
+
* a second `INSERT` for the same key always raises a unique-constraint
|
|
898
|
+
* violation instead of silently adopting the first caller's row: exactly the
|
|
899
|
+
* "insert-only against a UNIQUE column" claim the issue asks for.
|
|
900
|
+
*
|
|
901
|
+
* Neither the raw token nor the raw submitted content is stored — only
|
|
902
|
+
* digests — so this table cannot leak submitted business data. `status`
|
|
903
|
+
* moves from `'in_progress'` to `'completed'` inside the SAME transaction
|
|
904
|
+
* that runs the caller's work, so `result` (the caller's JSON-serialized,
|
|
905
|
+
* ready-to-replay return value) and `completed_at` are only ever set once,
|
|
906
|
+
* atomically with whatever else that transaction wrote. If the work throws,
|
|
907
|
+
* `runOnce()` never marks the claim completed, and the transaction that
|
|
908
|
+
* inserted it rolls back the insert along with it — a claim is never left
|
|
909
|
+
* permanently `'in_progress'` by a failed attempt; a retry with the same key
|
|
910
|
+
* can always run again.
|
|
911
|
+
*/
|
|
912
|
+
var CREATE_SMRT_RUN_ONCE_CLAIMS_TABLE = `
|
|
913
|
+
CREATE TABLE IF NOT EXISTS _smrt_run_once_claims (
|
|
914
|
+
claim_key TEXT PRIMARY KEY,
|
|
915
|
+
tenant_id TEXT NOT NULL,
|
|
916
|
+
actor TEXT NOT NULL,
|
|
917
|
+
content_digest TEXT NOT NULL,
|
|
918
|
+
status TEXT NOT NULL DEFAULT 'in_progress',
|
|
919
|
+
result TEXT,
|
|
920
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
921
|
+
completed_at TIMESTAMP
|
|
922
|
+
);
|
|
923
|
+
|
|
924
|
+
CREATE INDEX IF NOT EXISTS idx_smrt_run_once_claims_tenant
|
|
925
|
+
ON _smrt_run_once_claims(tenant_id, created_at);
|
|
926
|
+
|
|
927
|
+
-- Retention predicate for pruneRunOnceClaims() (#3080): completed claims are
|
|
928
|
+
-- aged out on completed_at, scoped to status = 'completed' so an in_progress
|
|
929
|
+
-- claim (completed_at IS NULL) is never matched by the sweep.
|
|
930
|
+
CREATE INDEX IF NOT EXISTS idx_smrt_run_once_claims_status_completed
|
|
931
|
+
ON _smrt_run_once_claims(status, completed_at);
|
|
932
|
+
`;
|
|
888
933
|
/**
|
|
889
934
|
* All system table creation statements
|
|
890
935
|
*/
|
|
@@ -893,6 +938,7 @@ var ALL_SYSTEM_TABLES = [
|
|
|
893
938
|
CREATE_SMRT_MIGRATIONS_TABLE,
|
|
894
939
|
CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE,
|
|
895
940
|
CREATE_SMRT_BACKFILLS_TABLE,
|
|
941
|
+
CREATE_SMRT_RUN_ONCE_CLAIMS_TABLE,
|
|
896
942
|
CREATE_SMRT_EMBEDDINGS_TABLE,
|
|
897
943
|
CREATE_SMRT_DISPATCH_TABLE,
|
|
898
944
|
CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE,
|
|
@@ -973,10 +1019,21 @@ function getSystemTableDDLForEngine(ddl, engine) {
|
|
|
973
1019
|
* sets — never published, never stamped in any real `_smrt_migrations` table
|
|
974
1020
|
* — so merging them reuses no version an existing install could have already
|
|
975
1021
|
* recorded; see the note on {@link SMRT_SCHEMA_DDL_CHECKSUMS}.)
|
|
1022
|
+
*
|
|
1023
|
+
* 1.11.0 adds `_smrt_run_once_claims` (#3080), the insert-only claim table
|
|
1024
|
+
* behind `runOnce()` (`../run-once.ts`).
|
|
1025
|
+
*
|
|
1026
|
+
* 1.11.1 adds `idx_smrt_run_once_claims_status_completed` (#3080), the
|
|
1027
|
+
* `(status, completed_at)` index backing `pruneRunOnceClaims()`'s retention
|
|
1028
|
+
* predicate (`system/retention.ts`).
|
|
976
1029
|
*/
|
|
977
|
-
var SMRT_SCHEMA_VERSION = "1.
|
|
978
|
-
Object.freeze({
|
|
1030
|
+
var SMRT_SCHEMA_VERSION = "1.11.1";
|
|
1031
|
+
Object.freeze({
|
|
1032
|
+
"1.10.1": "f796ee3b3f7ab8b9dc659ecaa68884ec01277408c3dd6edea540853fce369c16",
|
|
1033
|
+
"1.11.0": "c51351a76c96beb64fa7e5922f78d776367af882ed1ff37229f9e9ffacee8372",
|
|
1034
|
+
"1.11.1": "98112937a3bbb3883c3db1fd5e836751f91a479c1c5da64d4410263a6581ef1f"
|
|
1035
|
+
});
|
|
979
1036
|
//#endregion
|
|
980
|
-
export { ALL_SYSTEM_TABLES, CREATE_POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION, CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION, CREATE_POSTGRES_CHANGE_FEED_DRAIN_FUNCTION, CREATE_POSTGRES_SMRT_CHANGES_PENDING_TABLE, CREATE_POSTGRES_SMRT_CHANGES_TABLE, CREATE_SMRT_AI_USAGE_TABLE, CREATE_SMRT_BACKFILLS_TABLE, CREATE_SMRT_CHANGES_TABLE, CREATE_SMRT_CONTEXTS_TABLE, CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE, CREATE_SMRT_DISPATCH_TABLE, CREATE_SMRT_EMBEDDINGS_TABLE, CREATE_SMRT_MIGRATIONS_TABLE, CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE, ENSURE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION, ENSURE_POSTGRES_CHANGE_FEED_SCHEMA, FRAMEWORK_OPERATIONAL_TABLES, LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY, POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_IDENTITY, POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_NAME, POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY, POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME, POSTGRES_CHANGE_FEED_DRAIN_BATCH, POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_IDENTITY, POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME, POSTGRES_CHANGE_FEED_HELPER_MARKER, POSTGRES_CHANGE_FEED_PENDING_TABLE, REPLACE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION, RETIRED_SYSTEM_TABLES, SMRT_SCHEMA_VERSION, getSystemTableDDL, getSystemTableDDLForEngine };
|
|
1037
|
+
export { ALL_SYSTEM_TABLES, CREATE_POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION, CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION, CREATE_POSTGRES_CHANGE_FEED_DRAIN_FUNCTION, CREATE_POSTGRES_SMRT_CHANGES_PENDING_TABLE, CREATE_POSTGRES_SMRT_CHANGES_TABLE, CREATE_SMRT_AI_USAGE_TABLE, CREATE_SMRT_BACKFILLS_TABLE, CREATE_SMRT_CHANGES_TABLE, CREATE_SMRT_CONTEXTS_TABLE, CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE, CREATE_SMRT_DISPATCH_TABLE, CREATE_SMRT_EMBEDDINGS_TABLE, CREATE_SMRT_MIGRATIONS_TABLE, CREATE_SMRT_RUN_ONCE_CLAIMS_TABLE, CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE, ENSURE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION, ENSURE_POSTGRES_CHANGE_FEED_SCHEMA, FRAMEWORK_OPERATIONAL_TABLES, LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY, POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_IDENTITY, POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_NAME, POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY, POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME, POSTGRES_CHANGE_FEED_DRAIN_BATCH, POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_IDENTITY, POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME, POSTGRES_CHANGE_FEED_HELPER_MARKER, POSTGRES_CHANGE_FEED_PENDING_TABLE, REPLACE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION, RETIRED_SYSTEM_TABLES, RUN_ONCE_CLAIMS_TABLE, SMRT_SCHEMA_VERSION, getSystemTableDDL, getSystemTableDDLForEngine };
|
|
981
1038
|
|
|
982
1039
|
//# sourceMappingURL=schema.js.map
|