session-steward 0.9.0 → 0.10.0
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/CHANGELOG.md +7 -0
- package/README.md +37 -49
- package/bin/session-steward-mcp.mjs +4 -4
- package/bin/session-steward-scheduler.mjs +61 -0
- package/lib/cleanup-scheduler-service.mjs +205 -0
- package/lib/cleanup-schedules.mjs +439 -0
- package/lib/cli.mjs +145 -117
- package/lib/installed-products.mjs +44 -0
- package/lib/mcp.mjs +426 -416
- package/lib/providers/claude-code/events.mjs +2 -0
- package/lib/providers/claude-code/store.mjs +12 -3
- package/lib/providers/codex/database-families.mjs +1 -1
- package/lib/providers/codex/store.mjs +4 -2
- package/lib/server.mjs +49 -115
- package/lib/session-cleanup.mjs +540 -0
- package/lib/settings.mjs +1 -0
- package/package.json +3 -2
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { getProvider } from "./providers/index.mjs";
|
|
6
|
+
import { runSessionCleanup } from "./session-cleanup.mjs";
|
|
7
|
+
import { getDefaultConfigDirectory } from "./settings.mjs";
|
|
8
|
+
|
|
9
|
+
const STATE_VERSION = 1;
|
|
10
|
+
const MAX_SCHEDULES = 100;
|
|
11
|
+
const MAX_SESSIONS_PER_RUN = 100;
|
|
12
|
+
const RUN_CLAIM_TIMEOUT_MS = 6 * 60 * 60 * 1_000;
|
|
13
|
+
const LOCK_TIMEOUT_MS = 5_000;
|
|
14
|
+
const STALE_LOCK_MS = 60_000;
|
|
15
|
+
const DAY_MS = 24 * 60 * 60 * 1_000;
|
|
16
|
+
const MAX_DAY_COUNT = 3_650;
|
|
17
|
+
const LEGACY_FREQUENCY_DAYS = Object.freeze({ daily: 1, weekly: 7 });
|
|
18
|
+
const PROVIDERS = new Set(["codex", "claude-code"]);
|
|
19
|
+
const ARCHIVE_STATUSES = new Set(["all", "active", "archived"]);
|
|
20
|
+
const CLEANUP_MODES = new Set(["standard", "thorough"]);
|
|
21
|
+
const SELECTION_ORDERS = new Set(["oldest", "largest"]);
|
|
22
|
+
|
|
23
|
+
function emptyState() {
|
|
24
|
+
return { schedules: [], version: STATE_VERSION };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function requiredChoice(value, values, label) {
|
|
28
|
+
if (!values.has(value)) throw new Error(`${label} is not supported.`);
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function optionalText(value, label, maximumLength) {
|
|
33
|
+
if (value === undefined || value === null || value === "") return null;
|
|
34
|
+
if (typeof value !== "string" || value.includes("\0") || value.length > maximumLength) {
|
|
35
|
+
throw new Error(`${label} is not valid.`);
|
|
36
|
+
}
|
|
37
|
+
const trimmed = value.trim();
|
|
38
|
+
if (!trimmed) return null;
|
|
39
|
+
return trimmed;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function requiredDayCount(value, label) {
|
|
43
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > MAX_DAY_COUNT) {
|
|
44
|
+
throw new Error(`${label} must be a whole number between 1 and ${MAX_DAY_COUNT}.`);
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeDefinition(definition) {
|
|
50
|
+
if (!definition || typeof definition !== "object") {
|
|
51
|
+
throw new Error("Enter cleanup schedule settings.");
|
|
52
|
+
}
|
|
53
|
+
const name = optionalText(definition.name, "Schedule name", 100);
|
|
54
|
+
if (!name) throw new Error("Enter a schedule name.");
|
|
55
|
+
const provider = requiredChoice(definition.provider, PROVIDERS, "Provider");
|
|
56
|
+
const inactiveDays = requiredDayCount(definition.inactiveDays, "Inactivity period in days");
|
|
57
|
+
const runEveryDays = requiredDayCount(
|
|
58
|
+
definition.runEveryDays ?? LEGACY_FREQUENCY_DAYS[definition.frequency],
|
|
59
|
+
"Run interval in days",
|
|
60
|
+
);
|
|
61
|
+
const cleanupMode = requiredChoice(
|
|
62
|
+
definition.cleanupMode ?? "thorough",
|
|
63
|
+
CLEANUP_MODES,
|
|
64
|
+
"Cleanup mode",
|
|
65
|
+
);
|
|
66
|
+
const archiveStatus = requiredChoice(
|
|
67
|
+
definition.archiveStatus ?? "all",
|
|
68
|
+
ARCHIVE_STATUSES,
|
|
69
|
+
"Archive status",
|
|
70
|
+
);
|
|
71
|
+
const selectionOrder = requiredChoice(
|
|
72
|
+
definition.selectionOrder ?? "oldest",
|
|
73
|
+
SELECTION_ORDERS,
|
|
74
|
+
"Selection order",
|
|
75
|
+
);
|
|
76
|
+
const minimumTranscriptBytes = definition.minimumTranscriptBytes ?? null;
|
|
77
|
+
if (
|
|
78
|
+
minimumTranscriptBytes !== null
|
|
79
|
+
&& (!Number.isSafeInteger(minimumTranscriptBytes) || minimumTranscriptBytes <= 0)
|
|
80
|
+
) {
|
|
81
|
+
throw new Error("Minimum transcript bytes must be a positive whole number.");
|
|
82
|
+
}
|
|
83
|
+
const maxSessions = definition.maxSessions ?? 25;
|
|
84
|
+
if (!Number.isSafeInteger(maxSessions) || maxSessions < 1 || maxSessions > MAX_SESSIONS_PER_RUN) {
|
|
85
|
+
throw new Error(`Maximum sessions per run must be between 1 and ${MAX_SESSIONS_PER_RUN}.`);
|
|
86
|
+
}
|
|
87
|
+
const providerHomeOverride = definition.providerHomeOverride ?? null;
|
|
88
|
+
if (
|
|
89
|
+
providerHomeOverride !== null
|
|
90
|
+
&& (
|
|
91
|
+
typeof providerHomeOverride !== "string"
|
|
92
|
+
|| providerHomeOverride.includes("\0")
|
|
93
|
+
|| !path.isAbsolute(providerHomeOverride)
|
|
94
|
+
)
|
|
95
|
+
) {
|
|
96
|
+
throw new Error("Provider folder override is not valid.");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
archiveStatus,
|
|
101
|
+
cleanupMode,
|
|
102
|
+
enabled: definition.enabled !== false,
|
|
103
|
+
inactiveDays,
|
|
104
|
+
includeInternals: Boolean(definition.includeInternals),
|
|
105
|
+
includeSupporting: Boolean(definition.includeSupporting),
|
|
106
|
+
maxSessions,
|
|
107
|
+
minimumTranscriptBytes,
|
|
108
|
+
name,
|
|
109
|
+
provider,
|
|
110
|
+
providerHomeOverride,
|
|
111
|
+
runEveryDays,
|
|
112
|
+
selectionOrder,
|
|
113
|
+
workspace: optionalText(definition.workspace, "Workspace", 4_096),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function nextRunAt(schedule, now) {
|
|
118
|
+
return schedule.enabled ? now + schedule.runEveryDays * DAY_MS : null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function normalizeStoredSchedule(schedule) {
|
|
122
|
+
if (Number.isSafeInteger(schedule?.runEveryDays)) return schedule;
|
|
123
|
+
const runEveryDays = LEGACY_FREQUENCY_DAYS[schedule?.frequency];
|
|
124
|
+
if (!runEveryDays) return schedule;
|
|
125
|
+
const { frequency: _frequency, ...rest } = schedule;
|
|
126
|
+
return { ...rest, runEveryDays };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function readState(statePath) {
|
|
130
|
+
try {
|
|
131
|
+
const parsed = JSON.parse(await fs.readFile(statePath, "utf8"));
|
|
132
|
+
if (parsed?.version !== STATE_VERSION || !Array.isArray(parsed.schedules)) return emptyState();
|
|
133
|
+
return {
|
|
134
|
+
...parsed,
|
|
135
|
+
schedules: parsed.schedules.map(normalizeStoredSchedule),
|
|
136
|
+
};
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (error?.code === "ENOENT" || error instanceof SyntaxError) return emptyState();
|
|
139
|
+
throw new Error("Session Steward could not read cleanup schedules.", { cause: error });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function writeState(statePath, state) {
|
|
144
|
+
const directory = path.dirname(statePath);
|
|
145
|
+
const temporaryPath = path.join(
|
|
146
|
+
directory,
|
|
147
|
+
`.cleanup-schedules-${process.pid}-${randomBytes(8).toString("hex")}.tmp`,
|
|
148
|
+
);
|
|
149
|
+
await fs.mkdir(directory, { mode: 0o700, recursive: true });
|
|
150
|
+
let handle;
|
|
151
|
+
try {
|
|
152
|
+
handle = await fs.open(temporaryPath, "wx", 0o600);
|
|
153
|
+
await handle.writeFile(`${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
154
|
+
await handle.sync();
|
|
155
|
+
await handle.close();
|
|
156
|
+
handle = null;
|
|
157
|
+
await fs.rename(temporaryPath, statePath);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
await handle?.close().catch(() => {});
|
|
160
|
+
await fs.rm(temporaryPath, { force: true }).catch(() => {});
|
|
161
|
+
throw new Error("Session Steward could not save cleanup schedules.", { cause: error });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function delay(milliseconds) {
|
|
166
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function acquireLock(lockPath) {
|
|
170
|
+
const startedAt = Date.now();
|
|
171
|
+
while (Date.now() - startedAt < LOCK_TIMEOUT_MS) {
|
|
172
|
+
try {
|
|
173
|
+
await fs.mkdir(path.dirname(lockPath), { mode: 0o700, recursive: true });
|
|
174
|
+
return await fs.open(lockPath, "wx", 0o600);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (error?.code !== "EEXIST") throw error;
|
|
177
|
+
try {
|
|
178
|
+
const stats = await fs.stat(lockPath);
|
|
179
|
+
if (Date.now() - stats.mtimeMs > STALE_LOCK_MS) {
|
|
180
|
+
await fs.rm(lockPath, { force: true });
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
} catch (statError) {
|
|
184
|
+
if (statError?.code !== "ENOENT") throw statError;
|
|
185
|
+
}
|
|
186
|
+
await delay(25);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
throw new Error("Cleanup schedules are busy. Try again shortly.");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function publicSchedule(schedule) {
|
|
193
|
+
const { providerHomeOverride: _providerHomeOverride, ...safe } = schedule;
|
|
194
|
+
return structuredClone(safe);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function privateSchedule(schedule) {
|
|
198
|
+
return structuredClone(schedule);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function createCleanupScheduleStore({
|
|
202
|
+
configDirectory = getDefaultConfigDirectory(),
|
|
203
|
+
createId = randomUUID,
|
|
204
|
+
now = Date.now,
|
|
205
|
+
} = {}) {
|
|
206
|
+
const statePath = path.join(configDirectory, "cleanup-schedules.json");
|
|
207
|
+
const lockPath = path.join(configDirectory, "cleanup-schedules.lock");
|
|
208
|
+
|
|
209
|
+
async function mutate(mutator) {
|
|
210
|
+
const lock = await acquireLock(lockPath);
|
|
211
|
+
try {
|
|
212
|
+
const state = await readState(statePath);
|
|
213
|
+
const value = await mutator(state);
|
|
214
|
+
await writeState(statePath, state);
|
|
215
|
+
return value;
|
|
216
|
+
} finally {
|
|
217
|
+
await lock.close().catch(() => {});
|
|
218
|
+
await fs.rm(lockPath, { force: true }).catch(() => {});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
async claim(id, { force = false } = {}) {
|
|
224
|
+
return mutate((state) => {
|
|
225
|
+
const schedule = state.schedules.find((item) => item.id === id);
|
|
226
|
+
if (!schedule) throw new Error("That cleanup schedule does not exist.");
|
|
227
|
+
const currentTime = now();
|
|
228
|
+
if (!schedule.enabled && !force) return null;
|
|
229
|
+
if (!force && schedule.nextRunAtMs > currentTime) return null;
|
|
230
|
+
if (
|
|
231
|
+
Number.isFinite(schedule.runningSinceMs)
|
|
232
|
+
&& currentTime - schedule.runningSinceMs < RUN_CLAIM_TIMEOUT_MS
|
|
233
|
+
) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
schedule.runningSinceMs = currentTime;
|
|
237
|
+
schedule.nextRunAtMs = nextRunAt(schedule, currentTime);
|
|
238
|
+
schedule.updatedAtMs = currentTime;
|
|
239
|
+
return privateSchedule(schedule);
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
async complete(id, run) {
|
|
244
|
+
return mutate((state) => {
|
|
245
|
+
const schedule = state.schedules.find((item) => item.id === id);
|
|
246
|
+
if (!schedule) return null;
|
|
247
|
+
schedule.lastRun = run;
|
|
248
|
+
schedule.runningSinceMs = null;
|
|
249
|
+
schedule.updatedAtMs = now();
|
|
250
|
+
return publicSchedule(schedule);
|
|
251
|
+
});
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
async list() {
|
|
255
|
+
const state = await readState(statePath);
|
|
256
|
+
return state.schedules.map(publicSchedule);
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
async remove(id) {
|
|
260
|
+
return mutate((state) => {
|
|
261
|
+
const index = state.schedules.findIndex((item) => item.id === id);
|
|
262
|
+
if (index < 0) throw new Error("That cleanup schedule does not exist.");
|
|
263
|
+
const [removed] = state.schedules.splice(index, 1);
|
|
264
|
+
return publicSchedule(removed);
|
|
265
|
+
});
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
async save(definition, { id } = {}) {
|
|
269
|
+
const normalized = normalizeDefinition(definition);
|
|
270
|
+
return mutate((state) => {
|
|
271
|
+
const currentTime = now();
|
|
272
|
+
if (id === undefined) {
|
|
273
|
+
if (state.schedules.length >= MAX_SCHEDULES) {
|
|
274
|
+
throw new Error(`Session Steward supports up to ${MAX_SCHEDULES} cleanup schedules.`);
|
|
275
|
+
}
|
|
276
|
+
const schedule = {
|
|
277
|
+
...normalized,
|
|
278
|
+
createdAtMs: currentTime,
|
|
279
|
+
id: createId(),
|
|
280
|
+
lastRun: null,
|
|
281
|
+
nextRunAtMs: nextRunAt(normalized, currentTime),
|
|
282
|
+
runningSinceMs: null,
|
|
283
|
+
updatedAtMs: currentTime,
|
|
284
|
+
};
|
|
285
|
+
state.schedules.push(schedule);
|
|
286
|
+
return publicSchedule(schedule);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const index = state.schedules.findIndex((item) => item.id === id);
|
|
290
|
+
if (index < 0) throw new Error("That cleanup schedule does not exist.");
|
|
291
|
+
const existing = state.schedules[index];
|
|
292
|
+
const timingChanged = existing.runEveryDays !== normalized.runEveryDays
|
|
293
|
+
|| existing.enabled !== normalized.enabled;
|
|
294
|
+
const schedule = {
|
|
295
|
+
...existing,
|
|
296
|
+
...normalized,
|
|
297
|
+
nextRunAtMs: timingChanged
|
|
298
|
+
? nextRunAt(normalized, currentTime)
|
|
299
|
+
: existing.nextRunAtMs,
|
|
300
|
+
updatedAtMs: currentTime,
|
|
301
|
+
};
|
|
302
|
+
state.schedules[index] = schedule;
|
|
303
|
+
return publicSchedule(schedule);
|
|
304
|
+
});
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function scheduleProviderOptions(schedule, settings) {
|
|
310
|
+
const home = schedule.providerHomeOverride ?? settings.getHome(schedule.provider);
|
|
311
|
+
if (schedule.provider === "codex") return { codexHome: home };
|
|
312
|
+
const options = { claudeHome: home };
|
|
313
|
+
if (typeof settings.getClaudeDesktopDataHome === "function") {
|
|
314
|
+
options.desktopDataHome = settings.getClaudeDesktopDataHome();
|
|
315
|
+
}
|
|
316
|
+
return options;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function listingOptions(schedule, settings, now) {
|
|
320
|
+
return {
|
|
321
|
+
archiveStatus: schedule.archiveStatus,
|
|
322
|
+
includeInternals: schedule.includeInternals,
|
|
323
|
+
includeSupporting: schedule.includeSupporting,
|
|
324
|
+
inactiveBeforeMs: now() - schedule.inactiveDays * DAY_MS,
|
|
325
|
+
minimumTranscriptBytes: schedule.minimumTranscriptBytes ?? undefined,
|
|
326
|
+
pageSize: schedule.maxSessions,
|
|
327
|
+
search: "",
|
|
328
|
+
sort: schedule.selectionOrder === "largest" ? "size" : "updated",
|
|
329
|
+
workspace: schedule.workspace ?? undefined,
|
|
330
|
+
...scheduleProviderOptions(schedule, settings),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function findCandidates(schedule, provider, settings, now) {
|
|
335
|
+
const options = listingOptions(schedule, settings, now);
|
|
336
|
+
if (options.sort === "size") {
|
|
337
|
+
const result = await provider.listSessions({ ...options, page: 1 });
|
|
338
|
+
return result.records.slice(0, schedule.maxSessions);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const first = await provider.listSessions({ ...options, page: 1 });
|
|
342
|
+
if (first.pageCount === 1) return [...first.records].reverse();
|
|
343
|
+
const oldest = await provider.listSessions({ ...options, page: first.pageCount });
|
|
344
|
+
const records = [...oldest.records].reverse();
|
|
345
|
+
if (records.length < schedule.maxSessions && first.pageCount > 1) {
|
|
346
|
+
const previous = await provider.listSessions({ ...options, page: first.pageCount - 1 });
|
|
347
|
+
records.push(...[...previous.records].reverse());
|
|
348
|
+
}
|
|
349
|
+
return records.slice(0, schedule.maxSessions);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function safeRunResult(result, candidateCount, atMs) {
|
|
353
|
+
return {
|
|
354
|
+
affectedSessionCount: result.affectedSessionCount,
|
|
355
|
+
atMs,
|
|
356
|
+
candidateCount,
|
|
357
|
+
cleanupFallback: result.cleanupFallback,
|
|
358
|
+
cleanupMode: result.cleanupMode,
|
|
359
|
+
deletedSessionCount: result.deletedSessionCount,
|
|
360
|
+
requestedCleanupMode: result.requestedCleanupMode,
|
|
361
|
+
status: result.status,
|
|
362
|
+
transcriptBytes: result.transcriptBytes,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export async function runCleanupSchedule({
|
|
367
|
+
cleanup = runSessionCleanup,
|
|
368
|
+
force = false,
|
|
369
|
+
id,
|
|
370
|
+
now = Date.now,
|
|
371
|
+
resolveProvider = getProvider,
|
|
372
|
+
scheduleStore,
|
|
373
|
+
settings,
|
|
374
|
+
signal,
|
|
375
|
+
} = {}) {
|
|
376
|
+
const schedule = await scheduleStore.claim(id, { force });
|
|
377
|
+
if (!schedule) return { id, status: "not-due" };
|
|
378
|
+
const atMs = now();
|
|
379
|
+
let candidateCount = 0;
|
|
380
|
+
let run;
|
|
381
|
+
try {
|
|
382
|
+
const provider = resolveProvider(schedule.provider);
|
|
383
|
+
const candidates = await findCandidates(schedule, provider, settings, now);
|
|
384
|
+
candidateCount = candidates.length;
|
|
385
|
+
if (candidates.length === 0) {
|
|
386
|
+
run = {
|
|
387
|
+
affectedSessionCount: 0,
|
|
388
|
+
atMs,
|
|
389
|
+
candidateCount: 0,
|
|
390
|
+
deletedSessionCount: 0,
|
|
391
|
+
status: "no-matches",
|
|
392
|
+
transcriptBytes: 0,
|
|
393
|
+
};
|
|
394
|
+
} else {
|
|
395
|
+
const result = await cleanup({
|
|
396
|
+
options: scheduleProviderOptions(schedule, settings),
|
|
397
|
+
provider,
|
|
398
|
+
recordIds: candidates.map((record) => record.id),
|
|
399
|
+
scope: schedule.cleanupMode === "thorough" ? "deep" : "core",
|
|
400
|
+
signal,
|
|
401
|
+
});
|
|
402
|
+
run = safeRunResult(result, candidates.length, atMs);
|
|
403
|
+
}
|
|
404
|
+
} catch {
|
|
405
|
+
run = {
|
|
406
|
+
affectedSessionCount: 0,
|
|
407
|
+
atMs,
|
|
408
|
+
candidateCount,
|
|
409
|
+
deletedSessionCount: null,
|
|
410
|
+
status: "failed",
|
|
411
|
+
transcriptBytes: 0,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
await scheduleStore.complete(schedule.id, run);
|
|
415
|
+
return { id: schedule.id, provider: schedule.provider, ...run };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export async function runDueCleanupSchedules({
|
|
419
|
+
cleanup = runSessionCleanup,
|
|
420
|
+
now = Date.now,
|
|
421
|
+
resolveProvider = getProvider,
|
|
422
|
+
scheduleStore,
|
|
423
|
+
settings,
|
|
424
|
+
} = {}) {
|
|
425
|
+
const schedules = await scheduleStore.list();
|
|
426
|
+
const results = [];
|
|
427
|
+
for (const schedule of schedules) {
|
|
428
|
+
if (!schedule.enabled || schedule.nextRunAtMs > now()) continue;
|
|
429
|
+
results.push(await runCleanupSchedule({
|
|
430
|
+
cleanup,
|
|
431
|
+
id: schedule.id,
|
|
432
|
+
now,
|
|
433
|
+
resolveProvider,
|
|
434
|
+
scheduleStore,
|
|
435
|
+
settings,
|
|
436
|
+
}));
|
|
437
|
+
}
|
|
438
|
+
return results;
|
|
439
|
+
}
|