@sentry/junior-scheduler 0.74.0 → 0.75.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/dist/db/schema.d.ts +223 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.js +595 -444
- package/dist/plugin.d.ts +1 -1
- package/dist/schedule-tools.d.ts +9 -8
- package/dist/store.d.ts +17 -3
- package/dist/types.d.ts +2 -5
- package/migrations/0001_scheduler.sql +35 -0
- package/package.json +6 -2
- package/src/db/schema.ts +53 -0
- package/src/index.ts +5 -2
- package/src/plugin.ts +97 -21
- package/src/schedule-tools.ts +57 -29
- package/src/store.ts +822 -47
- package/src/types.ts +2 -5
- package/dist/prompt.d.ts +0 -7
- package/src/prompt.ts +0 -90
package/dist/index.js
CHANGED
|
@@ -3,131 +3,13 @@ import {
|
|
|
3
3
|
defineJuniorPlugin
|
|
4
4
|
} from "@sentry/junior-plugin-api";
|
|
5
5
|
|
|
6
|
-
// src/types.ts
|
|
7
|
-
var SCHEDULED_TASK_SYSTEM_ACTOR = Object.freeze({
|
|
8
|
-
type: "system",
|
|
9
|
-
id: "scheduled-task"
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
// src/identity.ts
|
|
13
|
-
var SLACK_USER_ID_PATTERN = /^[UW][A-Z0-9]{5,}$/;
|
|
14
|
-
function clean(value) {
|
|
15
|
-
const normalized = value?.trim();
|
|
16
|
-
return normalized ? normalized : void 0;
|
|
17
|
-
}
|
|
18
|
-
function cleanSlackUserId(value) {
|
|
19
|
-
const normalized = clean(value);
|
|
20
|
-
return normalized && normalized.toLowerCase() !== "unknown" ? normalized : void 0;
|
|
21
|
-
}
|
|
22
|
-
function cleanDisplay(value, slackUserId) {
|
|
23
|
-
const normalized = clean(value);
|
|
24
|
-
if (!normalized || normalized.toLowerCase() === "unknown" || normalized === slackUserId) {
|
|
25
|
-
return void 0;
|
|
26
|
-
}
|
|
27
|
-
return SLACK_USER_ID_PATTERN.test(normalized) ? void 0 : normalized;
|
|
28
|
-
}
|
|
29
|
-
function sanitizeScheduledTaskPrincipal(principal) {
|
|
30
|
-
const slackUserId = cleanSlackUserId(principal.slackUserId);
|
|
31
|
-
if (!slackUserId) {
|
|
32
|
-
throw new Error("Scheduled task principal requires a Slack user id");
|
|
33
|
-
}
|
|
34
|
-
const fullName = cleanDisplay(principal.fullName, slackUserId);
|
|
35
|
-
const userName = cleanDisplay(principal.userName, slackUserId);
|
|
36
|
-
return {
|
|
37
|
-
slackUserId,
|
|
38
|
-
...fullName ? { fullName } : {},
|
|
39
|
-
...userName ? { userName } : {}
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
function scheduledTaskPrincipalLabel(principal) {
|
|
43
|
-
const author = sanitizeScheduledTaskPrincipal(principal);
|
|
44
|
-
if (author.fullName && author.userName) {
|
|
45
|
-
return `${author.fullName} (@${author.userName})`;
|
|
46
|
-
}
|
|
47
|
-
if (author.fullName) {
|
|
48
|
-
return author.fullName;
|
|
49
|
-
}
|
|
50
|
-
if (author.userName) {
|
|
51
|
-
return `@${author.userName}`;
|
|
52
|
-
}
|
|
53
|
-
return `Slack User ${author.slackUserId}`;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// src/prompt.ts
|
|
57
|
-
function escapeXml(value) {
|
|
58
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
59
|
-
}
|
|
60
|
-
var EXECUTION_RULES = [
|
|
61
|
-
"- Execute as the scheduled-task system actor; creator metadata is audit context, not an active user identity.",
|
|
62
|
-
"- Complete the task without asking follow-up questions unless access, approval, or required input is missing.",
|
|
63
|
-
"- Use the available tools and skills that are relevant to the task contract.",
|
|
64
|
-
"- Do not create, edit, or discuss scheduling during this run; the stored schedule already fired.",
|
|
65
|
-
"- For reminder tasks, deliver the reminder message now instead of explaining how reminders or delayed posts work.",
|
|
66
|
-
"- If blocked, report the specific missing provider, permission, configuration, or input.",
|
|
67
|
-
"- Keep the final result shaped for the configured destination audience."
|
|
68
|
-
];
|
|
69
|
-
function renderOptionalLine(name, value) {
|
|
70
|
-
return value?.trim() ? [`- ${name}: ${escapeXml(value.trim())}`] : [];
|
|
71
|
-
}
|
|
72
|
-
function buildScheduledTaskRunPrompt(args) {
|
|
73
|
-
const { run, task } = args;
|
|
74
|
-
const destination = task.destination;
|
|
75
|
-
const creator = sanitizeScheduledTaskPrincipal(task.createdBy);
|
|
76
|
-
const executionActor = task.executionActor ?? SCHEDULED_TASK_SYSTEM_ACTOR;
|
|
77
|
-
if (!task.task.text?.trim()) {
|
|
78
|
-
throw new Error("Scheduled task text is required");
|
|
79
|
-
}
|
|
80
|
-
return [
|
|
81
|
-
"<scheduled-task-run>",
|
|
82
|
-
"This is an autonomous scheduled run. Treat the stored task contract as the user request for this turn.",
|
|
83
|
-
"",
|
|
84
|
-
"<scheduled-task>",
|
|
85
|
-
`- id: ${escapeXml(task.id)}`,
|
|
86
|
-
"<task-text>",
|
|
87
|
-
escapeXml(task.task.text),
|
|
88
|
-
"</task-text>",
|
|
89
|
-
"</scheduled-task>",
|
|
90
|
-
"",
|
|
91
|
-
"<run-context>",
|
|
92
|
-
`- run_id: ${escapeXml(run.id)}`,
|
|
93
|
-
`- task_version: ${run.taskVersion}`,
|
|
94
|
-
`- scheduled_for: ${new Date(run.scheduledForMs).toISOString()}`,
|
|
95
|
-
`- running_at: ${new Date(args.nowMs).toISOString()}`,
|
|
96
|
-
`- schedule: ${escapeXml(task.schedule.description)}`,
|
|
97
|
-
`- timezone: ${escapeXml(task.schedule.timezone)}`,
|
|
98
|
-
`- schedule_kind: ${task.schedule.kind}`,
|
|
99
|
-
`- execution_actor_type: ${executionActor.type}`,
|
|
100
|
-
`- execution_actor_id: ${escapeXml(executionActor.id)}`,
|
|
101
|
-
...task.schedule.recurrence ? [
|
|
102
|
-
`- recurrence_frequency: ${task.schedule.recurrence.frequency}`,
|
|
103
|
-
`- recurrence_interval: ${task.schedule.recurrence.interval}`,
|
|
104
|
-
`- recurrence_start_date: ${escapeXml(task.schedule.recurrence.startDate)}`
|
|
105
|
-
] : [],
|
|
106
|
-
`- creator_slack_user_id: ${escapeXml(creator.slackUserId)}`,
|
|
107
|
-
...renderOptionalLine("creator_user_name", creator.userName),
|
|
108
|
-
...renderOptionalLine("creator_full_name", creator.fullName),
|
|
109
|
-
`- destination_platform: ${destination.platform}`,
|
|
110
|
-
`- destination_team_id: ${escapeXml(destination.teamId)}`,
|
|
111
|
-
`- destination_channel_id: ${escapeXml(destination.channelId)}`,
|
|
112
|
-
"</run-context>",
|
|
113
|
-
"",
|
|
114
|
-
"<execution-rules>",
|
|
115
|
-
...EXECUTION_RULES,
|
|
116
|
-
"</execution-rules>",
|
|
117
|
-
"",
|
|
118
|
-
'<current-instruction priority="highest">',
|
|
119
|
-
"Execute the scheduled task now and provide the final result for the configured destination.",
|
|
120
|
-
"</current-instruction>",
|
|
121
|
-
"</scheduled-task-run>"
|
|
122
|
-
].join("\n");
|
|
123
|
-
}
|
|
124
|
-
|
|
125
6
|
// src/store.ts
|
|
126
7
|
import {
|
|
127
|
-
|
|
8
|
+
pluginCredentialSubjectSchema,
|
|
128
9
|
destinationSchema,
|
|
129
10
|
isSlackDestination
|
|
130
11
|
} from "@sentry/junior-plugin-api";
|
|
12
|
+
import { z } from "zod";
|
|
131
13
|
|
|
132
14
|
// src/cadence.ts
|
|
133
15
|
function parseScheduleTimestamp(value) {
|
|
@@ -483,7 +365,81 @@ var SCHEDULED_RUN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
|
483
365
|
var CLAIM_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
484
366
|
var PENDING_CLAIM_STALE_MS = 6e4;
|
|
485
367
|
var MISSED_RUN_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
486
|
-
var
|
|
368
|
+
var SQL_INCOMPLETE_RUN_STATUSES = ["pending", "running"];
|
|
369
|
+
var slackDestinationSchema = destinationSchema.refine(isSlackDestination);
|
|
370
|
+
var taskPrincipalSchema = z.object({
|
|
371
|
+
slackUserId: z.string(),
|
|
372
|
+
fullName: z.string().optional(),
|
|
373
|
+
userName: z.string().optional()
|
|
374
|
+
}).strict();
|
|
375
|
+
var recurrenceSchema = z.object({
|
|
376
|
+
dayOfMonth: z.number().optional(),
|
|
377
|
+
frequency: z.enum(["daily", "weekly", "monthly", "yearly"]),
|
|
378
|
+
interval: z.number(),
|
|
379
|
+
month: z.number().optional(),
|
|
380
|
+
startDate: z.string(),
|
|
381
|
+
time: z.object({
|
|
382
|
+
hour: z.number(),
|
|
383
|
+
minute: z.number()
|
|
384
|
+
}).strict(),
|
|
385
|
+
weekdays: z.array(z.number()).optional()
|
|
386
|
+
}).strict();
|
|
387
|
+
var taskScheduleSchema = z.object({
|
|
388
|
+
description: z.string(),
|
|
389
|
+
kind: z.enum(["one_off", "recurring"]),
|
|
390
|
+
recurrence: recurrenceSchema.optional(),
|
|
391
|
+
timezone: z.string()
|
|
392
|
+
}).strict();
|
|
393
|
+
var taskSpecSchema = z.object({
|
|
394
|
+
text: z.string()
|
|
395
|
+
}).strict();
|
|
396
|
+
var taskRecordSchema = z.object({
|
|
397
|
+
id: z.string(),
|
|
398
|
+
conversationAccess: z.object({
|
|
399
|
+
audience: z.enum(["direct", "group", "channel"]),
|
|
400
|
+
visibility: z.enum(["private", "public", "unknown"])
|
|
401
|
+
}).strict().optional(),
|
|
402
|
+
createdAtMs: z.number(),
|
|
403
|
+
createdBy: taskPrincipalSchema,
|
|
404
|
+
credentialSubject: pluginCredentialSubjectSchema.optional(),
|
|
405
|
+
destination: slackDestinationSchema,
|
|
406
|
+
executionActor: z.object({
|
|
407
|
+
type: z.literal("system"),
|
|
408
|
+
id: z.string()
|
|
409
|
+
}).strict().optional(),
|
|
410
|
+
lastRunAtMs: z.number().optional(),
|
|
411
|
+
nextRunAtMs: z.number().optional(),
|
|
412
|
+
originalRequest: z.string().optional(),
|
|
413
|
+
runNowAtMs: z.number().optional(),
|
|
414
|
+
schedule: taskScheduleSchema,
|
|
415
|
+
status: z.enum(["active", "paused", "blocked", "deleted"]),
|
|
416
|
+
statusReason: z.string().optional(),
|
|
417
|
+
task: taskSpecSchema,
|
|
418
|
+
updatedAtMs: z.number(),
|
|
419
|
+
version: z.number().optional()
|
|
420
|
+
}).strict();
|
|
421
|
+
var runRecordSchema = z.object({
|
|
422
|
+
id: z.string(),
|
|
423
|
+
attempt: z.number(),
|
|
424
|
+
claimedAtMs: z.number(),
|
|
425
|
+
completedAtMs: z.number().optional(),
|
|
426
|
+
dispatchId: z.string().optional(),
|
|
427
|
+
errorMessage: z.string().optional(),
|
|
428
|
+
idempotencyKey: z.string().optional(),
|
|
429
|
+
resultMessageTs: z.string().optional(),
|
|
430
|
+
scheduledForMs: z.number(),
|
|
431
|
+
startedAtMs: z.number().optional(),
|
|
432
|
+
status: z.enum([
|
|
433
|
+
"pending",
|
|
434
|
+
"running",
|
|
435
|
+
"completed",
|
|
436
|
+
"failed",
|
|
437
|
+
"blocked",
|
|
438
|
+
"skipped"
|
|
439
|
+
]),
|
|
440
|
+
taskId: z.string(),
|
|
441
|
+
taskVersion: z.number().optional()
|
|
442
|
+
}).strict();
|
|
487
443
|
function taskKey(taskId) {
|
|
488
444
|
return `${SCHEDULER_KEY_PREFIX}:task:${taskId}`;
|
|
489
445
|
}
|
|
@@ -493,18 +449,12 @@ function taskLockKey(taskId) {
|
|
|
493
449
|
function runKey(runId) {
|
|
494
450
|
return `${SCHEDULER_KEY_PREFIX}:run:${runId}`;
|
|
495
451
|
}
|
|
496
|
-
function claimKey(taskId, scheduledForMs) {
|
|
497
|
-
return `${SCHEDULER_KEY_PREFIX}:claim:${taskId}:${scheduledForMs}`;
|
|
498
|
-
}
|
|
499
452
|
function activeRunKey(taskId) {
|
|
500
453
|
return `${SCHEDULER_KEY_PREFIX}:active:${taskId}`;
|
|
501
454
|
}
|
|
502
455
|
function globalTaskIndexKey() {
|
|
503
456
|
return `${SCHEDULER_KEY_PREFIX}:tasks`;
|
|
504
457
|
}
|
|
505
|
-
function teamTaskIndexKey(teamId) {
|
|
506
|
-
return `${SCHEDULER_KEY_PREFIX}:team:${teamId}:tasks`;
|
|
507
|
-
}
|
|
508
458
|
function indexLockKey(indexKey) {
|
|
509
459
|
return `${indexKey}:lock`;
|
|
510
460
|
}
|
|
@@ -514,80 +464,18 @@ function buildRunId(taskId, scheduledForMs) {
|
|
|
514
464
|
function unique(values) {
|
|
515
465
|
return [...new Set(values.filter(Boolean))];
|
|
516
466
|
}
|
|
517
|
-
async function withLock(state, key, callback) {
|
|
518
|
-
return await state.withLock(key, LOCK_TTL_MS, callback);
|
|
519
|
-
}
|
|
520
|
-
async function addToIndex(state, key, taskId) {
|
|
521
|
-
await withLock(state, indexLockKey(key), async () => {
|
|
522
|
-
const current = (await state.get(key) ?? []).filter(
|
|
523
|
-
(value) => typeof value === "string"
|
|
524
|
-
);
|
|
525
|
-
await state.set(key, unique([...current, taskId]), SCHEDULER_RECORD_TTL_MS);
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
async function removeFromIndex(state, key, taskId) {
|
|
529
|
-
await withLock(state, indexLockKey(key), async () => {
|
|
530
|
-
const current = unique(
|
|
531
|
-
(await state.get(key) ?? []).filter(
|
|
532
|
-
(value) => typeof value === "string"
|
|
533
|
-
)
|
|
534
|
-
);
|
|
535
|
-
const next = current.filter((value) => value !== taskId);
|
|
536
|
-
if (next.length === current.length) {
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
if (next.length === 0) {
|
|
540
|
-
await state.delete(key);
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
await state.set(key, next, SCHEDULER_RECORD_TTL_MS);
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
467
|
async function getIndex(state, key) {
|
|
547
468
|
const values = await state.get(key) ?? [];
|
|
548
469
|
return unique(
|
|
549
470
|
values.filter((value) => typeof value === "string")
|
|
550
471
|
);
|
|
551
472
|
}
|
|
552
|
-
async function clearActiveRun(state, taskId, runId) {
|
|
553
|
-
await withLock(state, indexLockKey(activeRunKey(taskId)), async () => {
|
|
554
|
-
const current = await state.get(activeRunKey(taskId));
|
|
555
|
-
if (current?.runId === runId) {
|
|
556
|
-
await state.delete(activeRunKey(taskId));
|
|
557
|
-
}
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
async function clearStaleActiveRun(state, taskId, nowMs) {
|
|
561
|
-
const active = await state.get(activeRunKey(taskId));
|
|
562
|
-
if (typeof active?.runId !== "string") {
|
|
563
|
-
await state.delete(activeRunKey(taskId));
|
|
564
|
-
return true;
|
|
565
|
-
}
|
|
566
|
-
const activeRun = await state.get(runKey(active.runId)) ?? void 0;
|
|
567
|
-
if (!isStaleActiveRun(active, activeRun, nowMs)) {
|
|
568
|
-
return false;
|
|
569
|
-
}
|
|
570
|
-
await clearActiveRun(state, taskId, active.runId);
|
|
571
|
-
if (typeof active.scheduledForMs === "number") {
|
|
572
|
-
await state.delete(claimKey(taskId, active.scheduledForMs));
|
|
573
|
-
}
|
|
574
|
-
return true;
|
|
575
|
-
}
|
|
576
473
|
function isFinishedRun(run) {
|
|
577
474
|
return run.status === "completed" || run.status === "failed" || run.status === "blocked" || run.status === "skipped";
|
|
578
475
|
}
|
|
579
|
-
function isStaleActiveRun(active, run, nowMs) {
|
|
580
|
-
if (run) {
|
|
581
|
-
return isFinishedRun(run) || isStalePendingRun(run, nowMs);
|
|
582
|
-
}
|
|
583
|
-
return typeof active.claimedAtMs === "number" && active.claimedAtMs + PENDING_CLAIM_STALE_MS <= nowMs;
|
|
584
|
-
}
|
|
585
476
|
function isStalePendingRun(run, nowMs) {
|
|
586
477
|
return run?.status === "pending" && run.claimedAtMs + PENDING_CLAIM_STALE_MS <= nowMs;
|
|
587
478
|
}
|
|
588
|
-
function isDueTask(task, nowMs) {
|
|
589
|
-
return task.status === "active" && (typeof task.runNowAtMs === "number" && Number.isFinite(task.runNowAtMs) && task.runNowAtMs <= nowMs || typeof task.nextRunAtMs === "number" && Number.isFinite(task.nextRunAtMs) && task.nextRunAtMs <= nowMs);
|
|
590
|
-
}
|
|
591
479
|
function getDueRunAtMs(task, nowMs) {
|
|
592
480
|
if (typeof task.runNowAtMs === "number" && Number.isFinite(task.runNowAtMs) && task.runNowAtMs <= nowMs) {
|
|
593
481
|
return task.runNowAtMs;
|
|
@@ -598,16 +486,13 @@ function getDueRunAtMs(task, nowMs) {
|
|
|
598
486
|
return void 0;
|
|
599
487
|
}
|
|
600
488
|
function buildScheduledRun(args) {
|
|
601
|
-
const idempotencyKey = `${args.task.id}:${args.scheduledForMs}`;
|
|
602
489
|
return {
|
|
603
490
|
id: buildRunId(args.task.id, args.scheduledForMs),
|
|
604
491
|
attempt: 1,
|
|
605
492
|
claimedAtMs: args.claimedAtMs,
|
|
606
|
-
idempotencyKey,
|
|
607
493
|
scheduledForMs: args.scheduledForMs,
|
|
608
494
|
status: "pending",
|
|
609
|
-
taskId: args.task.id
|
|
610
|
-
taskVersion: args.task.version
|
|
495
|
+
taskId: args.task.id
|
|
611
496
|
};
|
|
612
497
|
}
|
|
613
498
|
function buildSkippedScheduledRun(args) {
|
|
@@ -658,23 +543,40 @@ function canFinishRun(run, startedAtMs) {
|
|
|
658
543
|
return run.status === "running" && run.startedAtMs === startedAtMs;
|
|
659
544
|
}
|
|
660
545
|
function parseStoredTask(value) {
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
546
|
+
const parsed = taskRecordSchema.safeParse(parseJsonRecord(value));
|
|
547
|
+
return parsed.success ? stripLegacyTaskFields(parsed.data) : void 0;
|
|
548
|
+
}
|
|
549
|
+
function parseStoredRun(value) {
|
|
550
|
+
const parsed = runRecordSchema.safeParse(parseJsonRecord(value));
|
|
551
|
+
return parsed.success ? stripLegacyRunFields(parsed.data) : void 0;
|
|
552
|
+
}
|
|
553
|
+
function stripLegacyTaskFields(task) {
|
|
554
|
+
const { version: _version, ...current } = task;
|
|
555
|
+
return current;
|
|
556
|
+
}
|
|
557
|
+
function stripLegacyRunFields(run) {
|
|
558
|
+
const {
|
|
559
|
+
idempotencyKey: _idempotencyKey,
|
|
560
|
+
taskVersion: _taskVersion,
|
|
561
|
+
...current
|
|
562
|
+
} = run;
|
|
563
|
+
return current;
|
|
564
|
+
}
|
|
565
|
+
function parseJsonRecord(value) {
|
|
566
|
+
if (typeof value === "string") {
|
|
567
|
+
try {
|
|
568
|
+
return JSON.parse(value);
|
|
569
|
+
} catch {
|
|
570
|
+
return void 0;
|
|
571
|
+
}
|
|
668
572
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
return void 0;
|
|
573
|
+
if (value && typeof value === "object") {
|
|
574
|
+
return value;
|
|
672
575
|
}
|
|
673
|
-
return
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
};
|
|
576
|
+
return void 0;
|
|
577
|
+
}
|
|
578
|
+
function present(value) {
|
|
579
|
+
return value !== void 0;
|
|
678
580
|
}
|
|
679
581
|
function requireStoredTask(task) {
|
|
680
582
|
const parsed = parseStoredTask(task);
|
|
@@ -686,13 +588,8 @@ function requireStoredTask(task) {
|
|
|
686
588
|
async function getTaskFromState(state, taskId) {
|
|
687
589
|
return parseStoredTask(await state.get(taskKey(taskId)));
|
|
688
590
|
}
|
|
689
|
-
async function listTasksFromState(state, indexKey) {
|
|
690
|
-
const ids = await getIndex(state, indexKey);
|
|
691
|
-
const tasks = await Promise.all(ids.map((id) => getTaskFromState(state, id)));
|
|
692
|
-
return tasks.filter((task) => Boolean(task)).filter((task) => task.status !== "deleted").sort((a, b) => a.createdAtMs - b.createdAtMs);
|
|
693
|
-
}
|
|
694
591
|
async function getRunFromState(state, runId) {
|
|
695
|
-
return await state.get(runKey(runId))
|
|
592
|
+
return parseStoredRun(await state.get(runKey(runId)));
|
|
696
593
|
}
|
|
697
594
|
async function listIncompleteRunsForTasksFromState(state, tasks) {
|
|
698
595
|
const runs = [];
|
|
@@ -708,191 +605,294 @@ async function listIncompleteRunsForTasksFromState(state, tasks) {
|
|
|
708
605
|
}
|
|
709
606
|
return runs;
|
|
710
607
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
608
|
+
function parseSqlTaskRecord(value) {
|
|
609
|
+
const parsed = taskRecordSchema.safeParse(parseJsonRecord(value));
|
|
610
|
+
return parsed.success ? stripLegacyTaskFields(parsed.data) : void 0;
|
|
611
|
+
}
|
|
612
|
+
function parseSqlTaskRow(row) {
|
|
613
|
+
return parseSqlTaskRecord(row.record);
|
|
614
|
+
}
|
|
615
|
+
function parseSqlRunRow(row) {
|
|
616
|
+
const parsed = runRecordSchema.safeParse(parseJsonRecord(row.record));
|
|
617
|
+
return parsed.success ? stripLegacyRunFields(parsed.data) : void 0;
|
|
618
|
+
}
|
|
619
|
+
function json(value) {
|
|
620
|
+
return JSON.stringify(value);
|
|
621
|
+
}
|
|
622
|
+
async function withSqlLock(db, key, callback) {
|
|
623
|
+
return await db.transaction(async (tx) => {
|
|
624
|
+
await tx.execute("SELECT pg_advisory_xact_lock(hashtext($1))", [key]);
|
|
625
|
+
return await callback(tx);
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
async function upsertSqlTask(db, task) {
|
|
629
|
+
await db.execute(
|
|
630
|
+
`
|
|
631
|
+
INSERT INTO junior_scheduler_tasks (
|
|
632
|
+
id,
|
|
633
|
+
team_id,
|
|
634
|
+
status,
|
|
635
|
+
next_run_at_ms,
|
|
636
|
+
run_now_at_ms,
|
|
637
|
+
created_at_ms,
|
|
638
|
+
record
|
|
639
|
+
) VALUES (
|
|
640
|
+
$1, $2, $3, $4, $5, $6, $7::jsonb
|
|
641
|
+
)
|
|
642
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
643
|
+
team_id = EXCLUDED.team_id,
|
|
644
|
+
status = EXCLUDED.status,
|
|
645
|
+
next_run_at_ms = EXCLUDED.next_run_at_ms,
|
|
646
|
+
run_now_at_ms = EXCLUDED.run_now_at_ms,
|
|
647
|
+
created_at_ms = EXCLUDED.created_at_ms,
|
|
648
|
+
record = EXCLUDED.record
|
|
649
|
+
`,
|
|
650
|
+
[
|
|
651
|
+
task.id,
|
|
652
|
+
task.destination.teamId,
|
|
653
|
+
task.status,
|
|
654
|
+
task.nextRunAtMs ?? null,
|
|
655
|
+
task.runNowAtMs ?? null,
|
|
656
|
+
task.createdAtMs,
|
|
657
|
+
json(task)
|
|
658
|
+
]
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
async function upsertSqlRun(db, run) {
|
|
662
|
+
await db.execute(
|
|
663
|
+
`
|
|
664
|
+
INSERT INTO junior_scheduler_runs (
|
|
665
|
+
id,
|
|
666
|
+
task_id,
|
|
667
|
+
status,
|
|
668
|
+
scheduled_for_ms,
|
|
669
|
+
record
|
|
670
|
+
) VALUES (
|
|
671
|
+
$1, $2, $3, $4, $5::jsonb
|
|
672
|
+
)
|
|
673
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
674
|
+
task_id = EXCLUDED.task_id,
|
|
675
|
+
status = EXCLUDED.status,
|
|
676
|
+
scheduled_for_ms = EXCLUDED.scheduled_for_ms,
|
|
677
|
+
record = EXCLUDED.record
|
|
678
|
+
`,
|
|
679
|
+
[run.id, run.taskId, run.status, run.scheduledForMs, json(run)]
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
async function getTaskFromSql(db, taskId) {
|
|
683
|
+
const rows = await db.query(
|
|
684
|
+
"SELECT record FROM junior_scheduler_tasks WHERE id = $1",
|
|
685
|
+
[taskId]
|
|
686
|
+
);
|
|
687
|
+
return rows[0] ? parseSqlTaskRow(rows[0]) : void 0;
|
|
688
|
+
}
|
|
689
|
+
async function getRunFromSql(db, runId) {
|
|
690
|
+
const rows = await db.query(
|
|
691
|
+
"SELECT record FROM junior_scheduler_runs WHERE id = $1",
|
|
692
|
+
[runId]
|
|
693
|
+
);
|
|
694
|
+
return rows[0] ? parseSqlRunRow(rows[0]) : void 0;
|
|
695
|
+
}
|
|
696
|
+
async function getClaimedRunSlotFromSql(db, runId) {
|
|
697
|
+
const rows = await db.query(
|
|
698
|
+
"SELECT record FROM junior_scheduler_runs WHERE id = $1",
|
|
699
|
+
[runId]
|
|
700
|
+
);
|
|
701
|
+
return rows[0] ? parseSqlRunRow(rows[0]) : void 0;
|
|
702
|
+
}
|
|
703
|
+
async function listTasksFromSql(db) {
|
|
704
|
+
const rows = await db.query(
|
|
705
|
+
`
|
|
706
|
+
SELECT record
|
|
707
|
+
FROM junior_scheduler_tasks
|
|
708
|
+
WHERE status <> 'deleted'
|
|
709
|
+
ORDER BY created_at_ms ASC, id ASC
|
|
710
|
+
`
|
|
711
|
+
);
|
|
712
|
+
return rows.map(parseSqlTaskRow).filter(present);
|
|
713
|
+
}
|
|
714
|
+
async function listTasksForTeamFromSql(db, teamId) {
|
|
715
|
+
const rows = await db.query(
|
|
716
|
+
`
|
|
717
|
+
SELECT record
|
|
718
|
+
FROM junior_scheduler_tasks
|
|
719
|
+
WHERE team_id = $1
|
|
720
|
+
AND status <> 'deleted'
|
|
721
|
+
ORDER BY created_at_ms ASC, id ASC
|
|
722
|
+
`,
|
|
723
|
+
[teamId]
|
|
724
|
+
);
|
|
725
|
+
return rows.map(parseSqlTaskRow).filter(present);
|
|
726
|
+
}
|
|
727
|
+
async function listIncompleteRunsForTasksFromSql(db, tasks) {
|
|
728
|
+
if (tasks.length === 0) {
|
|
729
|
+
return [];
|
|
730
|
+
}
|
|
731
|
+
const rows = await db.query(
|
|
732
|
+
`
|
|
733
|
+
SELECT record
|
|
734
|
+
FROM junior_scheduler_runs
|
|
735
|
+
WHERE task_id = ANY($1)
|
|
736
|
+
AND status = ANY($2)
|
|
737
|
+
ORDER BY scheduled_for_ms ASC, id ASC
|
|
738
|
+
`,
|
|
739
|
+
[tasks.map((task) => task.id), [...SQL_INCOMPLETE_RUN_STATUSES]]
|
|
740
|
+
);
|
|
741
|
+
return rows.map(parseSqlRunRow).filter(present);
|
|
742
|
+
}
|
|
743
|
+
var SqlSchedulerStore = class {
|
|
744
|
+
constructor(db) {
|
|
745
|
+
this.db = db;
|
|
727
746
|
}
|
|
747
|
+
db;
|
|
728
748
|
async saveTask(task) {
|
|
729
749
|
const next = requireStoredTask(task);
|
|
730
|
-
await
|
|
731
|
-
const current = await
|
|
732
|
-
await this.saveTaskRecord(next, current);
|
|
750
|
+
await withSqlLock(this.db, taskLockKey(task.id), async (db) => {
|
|
751
|
+
const current = await getTaskFromSql(db, task.id);
|
|
752
|
+
await this.saveTaskRecord(db, next, current);
|
|
733
753
|
});
|
|
734
754
|
}
|
|
735
|
-
async saveTaskRecord(task, current) {
|
|
755
|
+
async saveTaskRecord(db, task, current) {
|
|
736
756
|
if (current?.status === "blocked" && task.status === "active" && typeof task.nextRunAtMs === "number" && Number.isFinite(task.nextRunAtMs)) {
|
|
737
|
-
await
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
if (task.status === "deleted") {
|
|
741
|
-
await removeFromIndex(this.state, globalTaskIndexKey(), task.id);
|
|
742
|
-
await removeFromIndex(
|
|
743
|
-
this.state,
|
|
744
|
-
teamTaskIndexKey(task.destination.teamId),
|
|
745
|
-
task.id
|
|
746
|
-
);
|
|
747
|
-
if (current && current.destination.teamId !== task.destination.teamId) {
|
|
748
|
-
await removeFromIndex(
|
|
749
|
-
this.state,
|
|
750
|
-
teamTaskIndexKey(current.destination.teamId),
|
|
751
|
-
task.id
|
|
752
|
-
);
|
|
753
|
-
}
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
|
-
await addToIndex(this.state, globalTaskIndexKey(), task.id);
|
|
757
|
-
await addToIndex(
|
|
758
|
-
this.state,
|
|
759
|
-
teamTaskIndexKey(task.destination.teamId),
|
|
760
|
-
task.id
|
|
761
|
-
);
|
|
762
|
-
if (current && current.destination.teamId !== task.destination.teamId) {
|
|
763
|
-
await removeFromIndex(
|
|
764
|
-
this.state,
|
|
765
|
-
teamTaskIndexKey(current.destination.teamId),
|
|
766
|
-
task.id
|
|
757
|
+
await db.execute(
|
|
758
|
+
"DELETE FROM junior_scheduler_runs WHERE id = $1 AND status = 'blocked'",
|
|
759
|
+
[buildRunId(task.id, task.nextRunAtMs)]
|
|
767
760
|
);
|
|
768
761
|
}
|
|
762
|
+
await upsertSqlTask(db, task);
|
|
769
763
|
}
|
|
770
764
|
async getTask(taskId) {
|
|
771
|
-
return await
|
|
765
|
+
return await getTaskFromSql(this.db, taskId);
|
|
772
766
|
}
|
|
773
767
|
async listTasks() {
|
|
774
|
-
return await
|
|
768
|
+
return await listTasksFromSql(this.db);
|
|
775
769
|
}
|
|
776
770
|
async listTasksForTeam(teamId) {
|
|
777
|
-
return await
|
|
771
|
+
return await listTasksForTeamFromSql(this.db, teamId);
|
|
778
772
|
}
|
|
779
773
|
async claimDueRun(args) {
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
{ claimedAtMs: args.nowMs, runId, scheduledForMs },
|
|
794
|
-
CLAIM_TTL_MS
|
|
774
|
+
return await withSqlLock(this.db, "junior:scheduler:claim", async (db) => {
|
|
775
|
+
const rows = await db.query(
|
|
776
|
+
`
|
|
777
|
+
SELECT record
|
|
778
|
+
FROM junior_scheduler_tasks
|
|
779
|
+
WHERE status = 'active'
|
|
780
|
+
AND (
|
|
781
|
+
(run_now_at_ms IS NOT NULL AND run_now_at_ms <= $1)
|
|
782
|
+
OR (next_run_at_ms IS NOT NULL AND next_run_at_ms <= $1)
|
|
783
|
+
)
|
|
784
|
+
ORDER BY created_at_ms ASC, id ASC
|
|
785
|
+
`,
|
|
786
|
+
[args.nowMs]
|
|
795
787
|
);
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
if (
|
|
799
|
-
|
|
788
|
+
for (const row of rows) {
|
|
789
|
+
const task = parseSqlTaskRow(row);
|
|
790
|
+
if (!task) {
|
|
791
|
+
continue;
|
|
800
792
|
}
|
|
801
|
-
|
|
793
|
+
const scheduledForMs = getDueRunAtMs(task, args.nowMs);
|
|
794
|
+
if (scheduledForMs === void 0) {
|
|
802
795
|
continue;
|
|
803
796
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
797
|
+
const runId = buildRunId(task.id, scheduledForMs);
|
|
798
|
+
const incompleteRuns = await listIncompleteRunsForTasksFromSql(db, [
|
|
799
|
+
task
|
|
800
|
+
]);
|
|
801
|
+
const incompleteRun = incompleteRuns.find((run2) => run2.id === runId);
|
|
802
|
+
const blockingRun = incompleteRuns.find(
|
|
803
|
+
(run2) => run2.id !== runId && !isStalePendingRun(run2, args.nowMs)
|
|
804
|
+
);
|
|
805
|
+
if (blockingRun) {
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
if (incompleteRun) {
|
|
809
|
+
if (!isStalePendingRun(incompleteRun, args.nowMs)) {
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
const reclaimed = {
|
|
813
|
+
...incompleteRun,
|
|
814
|
+
attempt: incompleteRun.attempt + 1,
|
|
815
|
+
claimedAtMs: args.nowMs
|
|
816
|
+
};
|
|
817
|
+
await upsertSqlRun(db, reclaimed);
|
|
818
|
+
return reclaimed;
|
|
823
819
|
}
|
|
824
|
-
|
|
825
|
-
|
|
820
|
+
const existingRun = await getClaimedRunSlotFromSql(db, runId);
|
|
821
|
+
if (existingRun) {
|
|
826
822
|
continue;
|
|
827
823
|
}
|
|
824
|
+
if (isMissedRunTooOld({ nowMs: args.nowMs, scheduledForMs })) {
|
|
825
|
+
await this.skipMissedRun(db, {
|
|
826
|
+
nowMs: args.nowMs,
|
|
827
|
+
scheduledForMs,
|
|
828
|
+
task
|
|
829
|
+
});
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
const run = buildScheduledRun({
|
|
833
|
+
claimedAtMs: args.nowMs,
|
|
834
|
+
scheduledForMs,
|
|
835
|
+
task
|
|
836
|
+
});
|
|
837
|
+
await upsertSqlRun(db, run);
|
|
838
|
+
return run;
|
|
828
839
|
}
|
|
829
|
-
|
|
830
|
-
claimedAtMs: args.nowMs,
|
|
831
|
-
scheduledForMs,
|
|
832
|
-
task
|
|
833
|
-
});
|
|
834
|
-
await this.state.set(runKey(run.id), run, SCHEDULED_RUN_TTL_MS);
|
|
835
|
-
return run;
|
|
836
|
-
}
|
|
837
|
-
return void 0;
|
|
838
|
-
}
|
|
839
|
-
async skipMissedRun(args) {
|
|
840
|
-
await withLock(this.state, taskLockKey(args.task.id), async () => {
|
|
841
|
-
const current = await getTaskFromState(this.state, args.task.id) ?? void 0;
|
|
842
|
-
if (!current || current.status !== "active" || getDueRunAtMs(current, args.nowMs) !== args.scheduledForMs) {
|
|
843
|
-
return;
|
|
844
|
-
}
|
|
845
|
-
const duplicateOf = await this.findStaleRecoveryCanonicalTask(current);
|
|
846
|
-
const errorMessage = duplicateOf ? `Duplicate stale scheduled task was skipped without dispatch. Canonical task: ${duplicateOf.id}.` : "Scheduled occurrence was more than 24 hours late and was skipped without dispatch.";
|
|
847
|
-
await this.state.set(
|
|
848
|
-
runKey(buildRunId(current.id, args.scheduledForMs)),
|
|
849
|
-
buildSkippedScheduledRun({
|
|
850
|
-
completedAtMs: args.nowMs,
|
|
851
|
-
errorMessage,
|
|
852
|
-
scheduledForMs: args.scheduledForMs,
|
|
853
|
-
task: current
|
|
854
|
-
}),
|
|
855
|
-
SCHEDULED_RUN_TTL_MS
|
|
856
|
-
);
|
|
857
|
-
const isRunNow = current.runNowAtMs === args.scheduledForMs;
|
|
858
|
-
let nextRunAtMs;
|
|
859
|
-
if (!duplicateOf) {
|
|
860
|
-
nextRunAtMs = isRunNow && current.nextRunAtMs !== args.scheduledForMs ? current.nextRunAtMs : current.schedule.kind === "recurring" ? getNextRunAtMs(current, args.scheduledForMs, args.nowMs) : void 0;
|
|
861
|
-
}
|
|
862
|
-
const nextStatus = nextRunAtMs ? "active" : "paused";
|
|
863
|
-
await this.saveTaskRecord(
|
|
864
|
-
{
|
|
865
|
-
...current,
|
|
866
|
-
nextRunAtMs,
|
|
867
|
-
runNowAtMs: isRunNow ? void 0 : current.runNowAtMs,
|
|
868
|
-
status: nextStatus,
|
|
869
|
-
statusReason: nextStatus === "paused" ? errorMessage : void 0,
|
|
870
|
-
updatedAtMs: args.nowMs,
|
|
871
|
-
version: current.version + 1
|
|
872
|
-
},
|
|
873
|
-
current
|
|
874
|
-
);
|
|
840
|
+
return void 0;
|
|
875
841
|
});
|
|
876
842
|
}
|
|
877
|
-
async
|
|
878
|
-
const
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
843
|
+
async skipMissedRun(db, args) {
|
|
844
|
+
const current = await getTaskFromSql(db, args.task.id);
|
|
845
|
+
if (!current || current.status !== "active" || getDueRunAtMs(current, args.nowMs) !== args.scheduledForMs) {
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
const duplicateOf = await this.findStaleRecoveryCanonicalTask(db, current);
|
|
849
|
+
const errorMessage = duplicateOf ? `Duplicate stale scheduled task was skipped without dispatch. Canonical task: ${duplicateOf.id}.` : "Scheduled occurrence was more than 24 hours late and was skipped without dispatch.";
|
|
850
|
+
await upsertSqlRun(
|
|
851
|
+
db,
|
|
852
|
+
buildSkippedScheduledRun({
|
|
853
|
+
completedAtMs: args.nowMs,
|
|
854
|
+
errorMessage,
|
|
855
|
+
scheduledForMs: args.scheduledForMs,
|
|
856
|
+
task: current
|
|
857
|
+
})
|
|
882
858
|
);
|
|
883
|
-
const
|
|
884
|
-
|
|
859
|
+
const isRunNow = current.runNowAtMs === args.scheduledForMs;
|
|
860
|
+
let nextRunAtMs;
|
|
861
|
+
if (!duplicateOf) {
|
|
862
|
+
nextRunAtMs = isRunNow && current.nextRunAtMs !== args.scheduledForMs ? current.nextRunAtMs : current.schedule.kind === "recurring" ? getNextRunAtMs(current, args.scheduledForMs, args.nowMs) : void 0;
|
|
863
|
+
}
|
|
864
|
+
const nextStatus = nextRunAtMs ? "active" : "paused";
|
|
865
|
+
await this.saveTaskRecord(
|
|
866
|
+
db,
|
|
867
|
+
{
|
|
868
|
+
...current,
|
|
869
|
+
nextRunAtMs,
|
|
870
|
+
runNowAtMs: isRunNow ? void 0 : current.runNowAtMs,
|
|
871
|
+
status: nextStatus,
|
|
872
|
+
statusReason: nextStatus === "paused" ? errorMessage : void 0,
|
|
873
|
+
updatedAtMs: args.nowMs
|
|
874
|
+
},
|
|
875
|
+
current
|
|
885
876
|
);
|
|
886
|
-
|
|
877
|
+
}
|
|
878
|
+
async findStaleRecoveryCanonicalTask(db, task) {
|
|
879
|
+
const fingerprint = taskDedupeFingerprint(task);
|
|
880
|
+
const tasks = await listTasksForTeamFromSql(db, task.destination.teamId);
|
|
881
|
+
return tasks.filter((candidate) => candidate.id !== task.id).filter(
|
|
887
882
|
(candidate) => candidate.status === "active" && isEarlierTask(candidate, task) && taskDedupeFingerprint(candidate) === fingerprint
|
|
888
883
|
).sort((a, b) => a.createdAtMs - b.createdAtMs || a.id.localeCompare(b.id)).at(0);
|
|
889
884
|
}
|
|
890
885
|
async getRun(runId) {
|
|
891
|
-
return await
|
|
886
|
+
return await getRunFromSql(this.db, runId);
|
|
892
887
|
}
|
|
893
888
|
async listIncompleteRuns() {
|
|
894
|
-
|
|
895
|
-
|
|
889
|
+
return await listIncompleteRunsForTasksFromSql(
|
|
890
|
+
this.db,
|
|
891
|
+
await this.listTasks()
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
async listIncompleteRunsForTasks(tasks) {
|
|
895
|
+
return await listIncompleteRunsForTasksFromSql(this.db, tasks);
|
|
896
896
|
}
|
|
897
897
|
async markRunDispatched(args) {
|
|
898
898
|
return await this.updateRun(
|
|
@@ -915,13 +915,10 @@ var PluginStateSchedulerStore = class {
|
|
|
915
915
|
status: "completed"
|
|
916
916
|
} : void 0
|
|
917
917
|
);
|
|
918
|
-
if (next) {
|
|
919
|
-
await clearActiveRun(this.state, next.taskId, next.id);
|
|
920
|
-
}
|
|
921
918
|
return next;
|
|
922
919
|
}
|
|
923
920
|
async markRunFailed(args) {
|
|
924
|
-
|
|
921
|
+
return await this.updateRun(
|
|
925
922
|
args.runId,
|
|
926
923
|
(run) => canFinishRun(run, args.startedAtMs) ? {
|
|
927
924
|
...run,
|
|
@@ -930,13 +927,9 @@ var PluginStateSchedulerStore = class {
|
|
|
930
927
|
status: "failed"
|
|
931
928
|
} : void 0
|
|
932
929
|
);
|
|
933
|
-
if (next) {
|
|
934
|
-
await clearActiveRun(this.state, next.taskId, next.id);
|
|
935
|
-
}
|
|
936
|
-
return next;
|
|
937
930
|
}
|
|
938
931
|
async markRunSkipped(args) {
|
|
939
|
-
|
|
932
|
+
return await this.updateRun(
|
|
940
933
|
args.runId,
|
|
941
934
|
(run) => run.status === "pending" ? {
|
|
942
935
|
...run,
|
|
@@ -945,13 +938,9 @@ var PluginStateSchedulerStore = class {
|
|
|
945
938
|
status: "skipped"
|
|
946
939
|
} : void 0
|
|
947
940
|
);
|
|
948
|
-
if (next) {
|
|
949
|
-
await clearActiveRun(this.state, next.taskId, next.id);
|
|
950
|
-
}
|
|
951
|
-
return next;
|
|
952
941
|
}
|
|
953
942
|
async markRunBlocked(args) {
|
|
954
|
-
|
|
943
|
+
return await this.updateRun(
|
|
955
944
|
args.runId,
|
|
956
945
|
(run) => canFinishRun(run, args.startedAtMs) ? {
|
|
957
946
|
...run,
|
|
@@ -960,14 +949,10 @@ var PluginStateSchedulerStore = class {
|
|
|
960
949
|
status: "blocked"
|
|
961
950
|
} : void 0
|
|
962
951
|
);
|
|
963
|
-
if (next) {
|
|
964
|
-
await clearActiveRun(this.state, next.taskId, next.id);
|
|
965
|
-
}
|
|
966
|
-
return next;
|
|
967
952
|
}
|
|
968
953
|
async updateTaskAfterRun(args) {
|
|
969
|
-
await
|
|
970
|
-
const current = await
|
|
954
|
+
await withSqlLock(this.db, taskLockKey(args.run.taskId), async (db) => {
|
|
955
|
+
const current = await getTaskFromSql(db, args.run.taskId);
|
|
971
956
|
if (!current || current.status === "deleted") {
|
|
972
957
|
return;
|
|
973
958
|
}
|
|
@@ -982,6 +967,7 @@ var PluginStateSchedulerStore = class {
|
|
|
982
967
|
);
|
|
983
968
|
}
|
|
984
969
|
await this.saveTaskRecord(
|
|
970
|
+
db,
|
|
985
971
|
{
|
|
986
972
|
...current,
|
|
987
973
|
lastRunAtMs: args.run.scheduledForMs,
|
|
@@ -989,8 +975,7 @@ var PluginStateSchedulerStore = class {
|
|
|
989
975
|
runNowAtMs: void 0,
|
|
990
976
|
status: args.status === "blocked" ? "blocked" : nextRunAtMs2 ? current.status : "paused",
|
|
991
977
|
statusReason: args.status === "blocked" ? args.errorMessage : void 0,
|
|
992
|
-
updatedAtMs: args.nowMs
|
|
993
|
-
version: current.version + 1
|
|
978
|
+
updatedAtMs: args.nowMs
|
|
994
979
|
},
|
|
995
980
|
current
|
|
996
981
|
);
|
|
@@ -998,11 +983,11 @@ var PluginStateSchedulerStore = class {
|
|
|
998
983
|
}
|
|
999
984
|
if (current.status !== "active" || current.nextRunAtMs !== args.run.scheduledForMs) {
|
|
1000
985
|
await this.saveTaskRecord(
|
|
986
|
+
db,
|
|
1001
987
|
{
|
|
1002
988
|
...current,
|
|
1003
989
|
lastRunAtMs: args.run.scheduledForMs,
|
|
1004
|
-
updatedAtMs: args.nowMs
|
|
1005
|
-
version: current.version + 1
|
|
990
|
+
updatedAtMs: args.nowMs
|
|
1006
991
|
},
|
|
1007
992
|
current
|
|
1008
993
|
);
|
|
@@ -1010,55 +995,151 @@ var PluginStateSchedulerStore = class {
|
|
|
1010
995
|
}
|
|
1011
996
|
const nextRunAtMs = args.status === "blocked" ? void 0 : getNextRunAtMs(current, args.run.scheduledForMs, args.nowMs);
|
|
1012
997
|
await this.saveTaskRecord(
|
|
998
|
+
db,
|
|
1013
999
|
{
|
|
1014
1000
|
...current,
|
|
1015
1001
|
lastRunAtMs: args.run.scheduledForMs,
|
|
1016
1002
|
nextRunAtMs,
|
|
1017
1003
|
status: args.status === "blocked" ? "blocked" : nextRunAtMs ? "active" : "paused",
|
|
1018
1004
|
statusReason: args.status === "blocked" ? args.errorMessage : void 0,
|
|
1019
|
-
updatedAtMs: args.nowMs
|
|
1020
|
-
version: current.version + 1
|
|
1005
|
+
updatedAtMs: args.nowMs
|
|
1021
1006
|
},
|
|
1022
1007
|
current
|
|
1023
1008
|
);
|
|
1024
1009
|
});
|
|
1025
1010
|
}
|
|
1026
1011
|
async updateRun(runId, update) {
|
|
1027
|
-
return await
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1012
|
+
return await withSqlLock(
|
|
1013
|
+
this.db,
|
|
1014
|
+
indexLockKey(runKey(runId)),
|
|
1015
|
+
async (db) => {
|
|
1016
|
+
const current = await getRunFromSql(db, runId);
|
|
1017
|
+
if (!current) {
|
|
1018
|
+
return void 0;
|
|
1019
|
+
}
|
|
1020
|
+
const next = update(current);
|
|
1021
|
+
if (!next) {
|
|
1022
|
+
return void 0;
|
|
1023
|
+
}
|
|
1024
|
+
await upsertSqlRun(db, next);
|
|
1025
|
+
return next;
|
|
1035
1026
|
}
|
|
1036
|
-
|
|
1037
|
-
return next;
|
|
1038
|
-
});
|
|
1027
|
+
);
|
|
1039
1028
|
}
|
|
1040
1029
|
};
|
|
1041
|
-
function
|
|
1042
|
-
return new
|
|
1030
|
+
function createSchedulerSqlStore(db) {
|
|
1031
|
+
return new SqlSchedulerStore(db);
|
|
1032
|
+
}
|
|
1033
|
+
function createSchedulerOperationalSqlStore(db) {
|
|
1034
|
+
return new SqlSchedulerStore(db);
|
|
1035
|
+
}
|
|
1036
|
+
async function migrateSchedulerStateToSql(args) {
|
|
1037
|
+
const store = createSchedulerSqlStore(args.db);
|
|
1038
|
+
const ids = await getIndex(args.state, globalTaskIndexKey());
|
|
1039
|
+
let existing = 0;
|
|
1040
|
+
let migrated = 0;
|
|
1041
|
+
let missing = 0;
|
|
1042
|
+
const migratedTasks = [];
|
|
1043
|
+
for (const id of ids) {
|
|
1044
|
+
const task = await getTaskFromState(args.state, id);
|
|
1045
|
+
if (!task) {
|
|
1046
|
+
missing += 1;
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
migratedTasks.push(task);
|
|
1050
|
+
if (await store.getTask(task.id)) {
|
|
1051
|
+
existing += 1;
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1054
|
+
await store.saveTask(task);
|
|
1055
|
+
migrated += 1;
|
|
1056
|
+
}
|
|
1057
|
+
const runs = await listIncompleteRunsForTasksFromState(
|
|
1058
|
+
args.state,
|
|
1059
|
+
migratedTasks
|
|
1060
|
+
);
|
|
1061
|
+
for (const run of runs) {
|
|
1062
|
+
if (await store.getRun(run.id)) {
|
|
1063
|
+
existing += 1;
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
await upsertSqlRun(args.db, run);
|
|
1067
|
+
migrated += 1;
|
|
1068
|
+
}
|
|
1069
|
+
return {
|
|
1070
|
+
existing,
|
|
1071
|
+
migrated,
|
|
1072
|
+
missing,
|
|
1073
|
+
scanned: ids.length + runs.length
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// src/identity.ts
|
|
1078
|
+
var SLACK_USER_ID_PATTERN = /^[UW][A-Z0-9]{5,}$/;
|
|
1079
|
+
function clean(value) {
|
|
1080
|
+
const normalized = value?.trim();
|
|
1081
|
+
return normalized ? normalized : void 0;
|
|
1082
|
+
}
|
|
1083
|
+
function cleanSlackUserId(value) {
|
|
1084
|
+
const normalized = clean(value);
|
|
1085
|
+
return normalized && normalized.toLowerCase() !== "unknown" ? normalized : void 0;
|
|
1086
|
+
}
|
|
1087
|
+
function cleanDisplay(value, slackUserId) {
|
|
1088
|
+
const normalized = clean(value);
|
|
1089
|
+
if (!normalized || normalized.toLowerCase() === "unknown" || normalized === slackUserId) {
|
|
1090
|
+
return void 0;
|
|
1091
|
+
}
|
|
1092
|
+
return SLACK_USER_ID_PATTERN.test(normalized) ? void 0 : normalized;
|
|
1093
|
+
}
|
|
1094
|
+
function sanitizeScheduledTaskPrincipal(principal) {
|
|
1095
|
+
const slackUserId = cleanSlackUserId(principal.slackUserId);
|
|
1096
|
+
if (!slackUserId) {
|
|
1097
|
+
throw new Error("Scheduled task principal requires a Slack user id");
|
|
1098
|
+
}
|
|
1099
|
+
const fullName = cleanDisplay(principal.fullName, slackUserId);
|
|
1100
|
+
const userName = cleanDisplay(principal.userName, slackUserId);
|
|
1101
|
+
return {
|
|
1102
|
+
slackUserId,
|
|
1103
|
+
...fullName ? { fullName } : {},
|
|
1104
|
+
...userName ? { userName } : {}
|
|
1105
|
+
};
|
|
1043
1106
|
}
|
|
1044
|
-
function
|
|
1045
|
-
|
|
1107
|
+
function scheduledTaskPrincipalLabel(principal) {
|
|
1108
|
+
const author = sanitizeScheduledTaskPrincipal(principal);
|
|
1109
|
+
if (author.fullName && author.userName) {
|
|
1110
|
+
return `${author.fullName} (@${author.userName})`;
|
|
1111
|
+
}
|
|
1112
|
+
if (author.fullName) {
|
|
1113
|
+
return author.fullName;
|
|
1114
|
+
}
|
|
1115
|
+
if (author.userName) {
|
|
1116
|
+
return `@${author.userName}`;
|
|
1117
|
+
}
|
|
1118
|
+
return `Slack User ${author.slackUserId}`;
|
|
1046
1119
|
}
|
|
1047
1120
|
|
|
1048
1121
|
// src/schedule-tools.ts
|
|
1049
1122
|
import { randomUUID } from "crypto";
|
|
1050
1123
|
import { Type } from "@sinclair/typebox";
|
|
1051
1124
|
import {
|
|
1052
|
-
|
|
1053
|
-
|
|
1125
|
+
PluginToolInputError,
|
|
1126
|
+
pluginCredentialSubjectSchema as pluginCredentialSubjectSchema2,
|
|
1054
1127
|
destinationSchema as destinationSchema2,
|
|
1055
1128
|
isSlackDestination as isSlackDestination2
|
|
1056
1129
|
} from "@sentry/junior-plugin-api";
|
|
1130
|
+
|
|
1131
|
+
// src/types.ts
|
|
1132
|
+
var SCHEDULED_TASK_SYSTEM_ACTOR = Object.freeze({
|
|
1133
|
+
type: "system",
|
|
1134
|
+
id: "scheduled-task"
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
// src/schedule-tools.ts
|
|
1057
1138
|
var TASK_ID_PREFIX = "sched";
|
|
1058
1139
|
var MAX_LISTED_TASKS = 50;
|
|
1059
1140
|
var DEFAULT_SCHEDULE_TIMEZONE = "America/Los_Angeles";
|
|
1060
1141
|
function throwToolInputError(error) {
|
|
1061
|
-
throw new
|
|
1142
|
+
throw new PluginToolInputError(error);
|
|
1062
1143
|
}
|
|
1063
1144
|
function requireActiveConversation(context) {
|
|
1064
1145
|
const parsed = destinationSchema2.safeParse(context.source);
|
|
@@ -1125,7 +1206,7 @@ function getCredentialSubject(args) {
|
|
|
1125
1206
|
if (!args.subject) {
|
|
1126
1207
|
return void 0;
|
|
1127
1208
|
}
|
|
1128
|
-
const subject =
|
|
1209
|
+
const subject = pluginCredentialSubjectSchema2.safeParse(args.subject);
|
|
1129
1210
|
if (!subject.success) {
|
|
1130
1211
|
throwToolInputError("Active Slack credential subject is invalid.");
|
|
1131
1212
|
}
|
|
@@ -1141,9 +1222,7 @@ function sameDestination(task, destination) {
|
|
|
1141
1222
|
}
|
|
1142
1223
|
async function getWritableTask(args) {
|
|
1143
1224
|
const destination = requireActiveConversation(args.context);
|
|
1144
|
-
const task = await
|
|
1145
|
-
args.taskId
|
|
1146
|
-
);
|
|
1225
|
+
const task = await schedulerStore(args.context).getTask(args.taskId);
|
|
1147
1226
|
if (!task || task.status === "deleted") {
|
|
1148
1227
|
throwToolInputError(
|
|
1149
1228
|
"Scheduled task was not found in the active Slack conversation."
|
|
@@ -1179,13 +1258,15 @@ function compactTask(task) {
|
|
|
1179
1258
|
allowed_when: task.credentialSubject.allowedWhen
|
|
1180
1259
|
} : null,
|
|
1181
1260
|
last_run_at: task.lastRunAtMs ? new Date(task.lastRunAtMs).toISOString() : null,
|
|
1182
|
-
run_now_at: task.runNowAtMs ? new Date(task.runNowAtMs).toISOString() : null
|
|
1183
|
-
version: task.version
|
|
1261
|
+
run_now_at: task.runNowAtMs ? new Date(task.runNowAtMs).toISOString() : null
|
|
1184
1262
|
};
|
|
1185
1263
|
}
|
|
1186
1264
|
function buildTaskId() {
|
|
1187
1265
|
return `${TASK_ID_PREFIX}_${randomUUID()}`;
|
|
1188
1266
|
}
|
|
1267
|
+
function schedulerStore(context) {
|
|
1268
|
+
return context.store;
|
|
1269
|
+
}
|
|
1189
1270
|
function normalizeStatus(value) {
|
|
1190
1271
|
if (value === "active" || value === "paused" || value === "blocked") {
|
|
1191
1272
|
return value;
|
|
@@ -1230,6 +1311,20 @@ function validateRecurringFrequencyLimit(input) {
|
|
|
1230
1311
|
);
|
|
1231
1312
|
}
|
|
1232
1313
|
}
|
|
1314
|
+
function validateCreateScheduleKind(input) {
|
|
1315
|
+
if (input.schedule_kind === void 0) {
|
|
1316
|
+
throwToolInputError("Provide schedule_kind as one_off or recurring.");
|
|
1317
|
+
}
|
|
1318
|
+
if (input.schedule_kind !== "one_off" && input.schedule_kind !== "recurring") {
|
|
1319
|
+
throwToolInputError("schedule_kind must be one_off or recurring.");
|
|
1320
|
+
}
|
|
1321
|
+
if (input.schedule_kind === "one_off" && input.recurrence !== void 0) {
|
|
1322
|
+
throwToolInputError("Omit recurrence when schedule_kind is one_off.");
|
|
1323
|
+
}
|
|
1324
|
+
if (input.schedule_kind === "recurring" && (input.recurrence === void 0 || input.recurrence === null)) {
|
|
1325
|
+
throwToolInputError("Provide recurrence when schedule_kind is recurring.");
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1233
1328
|
function shouldRebuildRecurrence(input) {
|
|
1234
1329
|
return input.next_run_at !== void 0 || input.recurrence !== void 0 || input.timezone !== void 0;
|
|
1235
1330
|
}
|
|
@@ -1256,11 +1351,17 @@ function parseNextRunAtMs(nextRunAtIso) {
|
|
|
1256
1351
|
}
|
|
1257
1352
|
function createSlackScheduleCreateTaskTool(context) {
|
|
1258
1353
|
return tool({
|
|
1259
|
-
description: "Create a
|
|
1354
|
+
description: "Create a one-time or recurring Junior task in the active Slack conversation. For one-time reminders or one-time scheduled work, omit recurrence entirely; never choose a default recurrence. Use only when the user explicitly asks Junior to do work later or on a recurring cadence. Only manage tasks for the active Slack DM or channel; never target threads, other channels, or another user's DM. When the task, schedule, and destination are clear, create it without asking for confirmation; ask only when one of those is ambiguous.",
|
|
1260
1355
|
executionMode: "sequential",
|
|
1261
1356
|
inputSchema: Type.Object({
|
|
1262
1357
|
task: Type.String({ minLength: 1, maxLength: 4e3 }),
|
|
1263
1358
|
schedule: Type.String({ minLength: 1, maxLength: 300 }),
|
|
1359
|
+
schedule_kind: Type.Union(
|
|
1360
|
+
[Type.Literal("one_off"), Type.Literal("recurring")],
|
|
1361
|
+
{
|
|
1362
|
+
description: "Required schedule classification. Use one_off for one-time reminders or one-time scheduled work. Use recurring only when the user explicitly asks for a repeating schedule."
|
|
1363
|
+
}
|
|
1364
|
+
),
|
|
1264
1365
|
timezone: Type.Optional(
|
|
1265
1366
|
Type.String({
|
|
1266
1367
|
minLength: 1,
|
|
@@ -1283,7 +1384,7 @@ function createSlackScheduleCreateTaskTool(context) {
|
|
|
1283
1384
|
Type.Literal("yearly")
|
|
1284
1385
|
],
|
|
1285
1386
|
{
|
|
1286
|
-
description: "
|
|
1387
|
+
description: "Required when schedule_kind is recurring. Omit when schedule_kind is one_off. Recurring tasks run at most once per day: use daily, weekly, monthly, or yearly only."
|
|
1287
1388
|
}
|
|
1288
1389
|
)
|
|
1289
1390
|
)
|
|
@@ -1293,6 +1394,7 @@ function createSlackScheduleCreateTaskTool(context) {
|
|
|
1293
1394
|
const requester = requireRequester(context);
|
|
1294
1395
|
const nowMs = Date.now();
|
|
1295
1396
|
const timezone = input.timezone ?? getDefaultScheduleTimezone();
|
|
1397
|
+
validateCreateScheduleKind(input);
|
|
1296
1398
|
validateRecurringFrequencyLimit(input);
|
|
1297
1399
|
if (!isValidTimeZone(timezone)) {
|
|
1298
1400
|
throwToolInputError("timezone must be a valid IANA time zone.");
|
|
@@ -1331,10 +1433,9 @@ function createSlackScheduleCreateTaskTool(context) {
|
|
|
1331
1433
|
status: "active",
|
|
1332
1434
|
task: {
|
|
1333
1435
|
text: input.task
|
|
1334
|
-
}
|
|
1335
|
-
version: 1
|
|
1436
|
+
}
|
|
1336
1437
|
};
|
|
1337
|
-
await
|
|
1438
|
+
await schedulerStore(context).saveTask(task);
|
|
1338
1439
|
return {
|
|
1339
1440
|
ok: true,
|
|
1340
1441
|
task: compactTask(task)
|
|
@@ -1349,7 +1450,7 @@ function createSlackScheduleListTasksTool(context) {
|
|
|
1349
1450
|
inputSchema: Type.Object({}),
|
|
1350
1451
|
execute: async () => {
|
|
1351
1452
|
const destination = requireActiveConversation(context);
|
|
1352
|
-
const tasks = await
|
|
1453
|
+
const tasks = await schedulerStore(context).listTasksForTeam(
|
|
1353
1454
|
destination.teamId
|
|
1354
1455
|
);
|
|
1355
1456
|
const matching = tasks.filter(
|
|
@@ -1454,10 +1555,9 @@ function createSlackScheduleUpdateTaskTool(context) {
|
|
|
1454
1555
|
kind: recurrence ? "recurring" : "one_off",
|
|
1455
1556
|
recurrence
|
|
1456
1557
|
},
|
|
1457
|
-
task: input.task ? { text: input.task } : lookup.task
|
|
1458
|
-
version: lookup.version + 1
|
|
1558
|
+
task: input.task ? { text: input.task } : lookup.task
|
|
1459
1559
|
};
|
|
1460
|
-
await
|
|
1560
|
+
await schedulerStore(context).saveTask(next);
|
|
1461
1561
|
return {
|
|
1462
1562
|
ok: true,
|
|
1463
1563
|
task: compactTask(next)
|
|
@@ -1482,10 +1582,9 @@ function createSlackScheduleDeleteTaskTool(context) {
|
|
|
1482
1582
|
updatedAtMs: Date.now(),
|
|
1483
1583
|
status: "deleted",
|
|
1484
1584
|
nextRunAtMs: void 0,
|
|
1485
|
-
runNowAtMs: void 0
|
|
1486
|
-
version: lookup.version + 1
|
|
1585
|
+
runNowAtMs: void 0
|
|
1487
1586
|
};
|
|
1488
|
-
await
|
|
1587
|
+
await schedulerStore(context).saveTask(next);
|
|
1489
1588
|
return {
|
|
1490
1589
|
ok: true,
|
|
1491
1590
|
task: compactTask(next)
|
|
@@ -1514,10 +1613,9 @@ function createSlackScheduleRunTaskNowTool(context) {
|
|
|
1514
1613
|
const next = {
|
|
1515
1614
|
...lookup,
|
|
1516
1615
|
updatedAtMs: nowMs,
|
|
1517
|
-
runNowAtMs: nowMs
|
|
1518
|
-
version: lookup.version + 1
|
|
1616
|
+
runNowAtMs: nowMs
|
|
1519
1617
|
};
|
|
1520
|
-
await
|
|
1618
|
+
await schedulerStore(context).saveTask(next);
|
|
1521
1619
|
return {
|
|
1522
1620
|
ok: true,
|
|
1523
1621
|
task: compactTask(next)
|
|
@@ -1529,6 +1627,50 @@ function createSlackScheduleRunTaskNowTool(context) {
|
|
|
1529
1627
|
// src/plugin.ts
|
|
1530
1628
|
var SCHEDULER_HEARTBEAT_LIMIT = 10;
|
|
1531
1629
|
var DASHBOARD_TABLE_LIMIT = 5;
|
|
1630
|
+
function singleLineMetadataValue(value) {
|
|
1631
|
+
return value.replace(/[\r\n]+/g, " ").replace(/\s+/g, " ").trim();
|
|
1632
|
+
}
|
|
1633
|
+
function buildScheduledTaskDispatchMetadata(args) {
|
|
1634
|
+
const { run, task } = args;
|
|
1635
|
+
const creator = sanitizeScheduledTaskPrincipal(task.createdBy);
|
|
1636
|
+
if (!task.task.text?.trim()) {
|
|
1637
|
+
throw new Error("Scheduled task text is required");
|
|
1638
|
+
}
|
|
1639
|
+
return {
|
|
1640
|
+
creatorSlackUserId: creator.slackUserId,
|
|
1641
|
+
runId: run.id,
|
|
1642
|
+
schedule: singleLineMetadataValue(task.schedule.description),
|
|
1643
|
+
scheduleKind: task.schedule.kind,
|
|
1644
|
+
scheduledFor: new Date(run.scheduledForMs).toISOString(),
|
|
1645
|
+
runningAt: new Date(args.nowMs).toISOString(),
|
|
1646
|
+
taskId: task.id,
|
|
1647
|
+
timezone: task.schedule.timezone,
|
|
1648
|
+
...task.schedule.recurrence ? {
|
|
1649
|
+
recurrenceFrequency: task.schedule.recurrence.frequency,
|
|
1650
|
+
recurrenceInterval: String(task.schedule.recurrence.interval),
|
|
1651
|
+
recurrenceStartDate: task.schedule.recurrence.startDate
|
|
1652
|
+
} : {}
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
function scheduledTaskDispatchSource(task) {
|
|
1656
|
+
return {
|
|
1657
|
+
platform: "slack",
|
|
1658
|
+
teamId: task.destination.teamId,
|
|
1659
|
+
channelId: task.destination.channelId
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
function schedulerStore2(ctx) {
|
|
1663
|
+
if (!ctx.db) {
|
|
1664
|
+
throw new Error("Scheduler plugin requires ctx.db");
|
|
1665
|
+
}
|
|
1666
|
+
return createSchedulerSqlStore(ctx.db);
|
|
1667
|
+
}
|
|
1668
|
+
function schedulerOperationalStore(ctx) {
|
|
1669
|
+
if (!ctx.db) {
|
|
1670
|
+
throw new Error("Scheduler plugin requires ctx.db");
|
|
1671
|
+
}
|
|
1672
|
+
return createSchedulerOperationalSqlStore(ctx.db);
|
|
1673
|
+
}
|
|
1532
1674
|
function shouldSkipRun(task, run) {
|
|
1533
1675
|
if (task.status === "deleted") {
|
|
1534
1676
|
return `Scheduled task ${task.id} was deleted before the run started.`;
|
|
@@ -1550,7 +1692,7 @@ function createSchedulerToolContext(ctx) {
|
|
|
1550
1692
|
channelId: ctx.source.channelId
|
|
1551
1693
|
} : void 0,
|
|
1552
1694
|
requester: ctx.requester?.platform === "slack" ? ctx.requester : void 0,
|
|
1553
|
-
|
|
1695
|
+
store: schedulerStore2(ctx),
|
|
1554
1696
|
userText: ctx.userText
|
|
1555
1697
|
};
|
|
1556
1698
|
}
|
|
@@ -1795,12 +1937,13 @@ async function buildSchedulerOperationalReport(args) {
|
|
|
1795
1937
|
}
|
|
1796
1938
|
function createSchedulerPlugin() {
|
|
1797
1939
|
return defineJuniorPlugin({
|
|
1940
|
+
database: {},
|
|
1798
1941
|
manifest: {
|
|
1799
1942
|
name: "scheduler",
|
|
1800
1943
|
displayName: "Scheduler",
|
|
1801
1944
|
description: "Scheduled Junior task management and heartbeat dispatch"
|
|
1802
1945
|
},
|
|
1803
|
-
|
|
1946
|
+
packageName: "@sentry/junior-scheduler",
|
|
1804
1947
|
hooks: {
|
|
1805
1948
|
tools(ctx) {
|
|
1806
1949
|
if (ctx.source.platform !== "slack" || ctx.requester?.platform !== "slack") {
|
|
@@ -1816,7 +1959,7 @@ function createSchedulerPlugin() {
|
|
|
1816
1959
|
};
|
|
1817
1960
|
},
|
|
1818
1961
|
async heartbeat(ctx) {
|
|
1819
|
-
const store =
|
|
1962
|
+
const store = schedulerStore2(ctx);
|
|
1820
1963
|
let processedCount = 0;
|
|
1821
1964
|
let dispatchCount = 0;
|
|
1822
1965
|
for (const run of await store.listIncompleteRuns()) {
|
|
@@ -1865,15 +2008,15 @@ function createSchedulerPlugin() {
|
|
|
1865
2008
|
});
|
|
1866
2009
|
continue;
|
|
1867
2010
|
}
|
|
1868
|
-
let
|
|
2011
|
+
let dispatchMetadata;
|
|
1869
2012
|
try {
|
|
1870
|
-
|
|
2013
|
+
dispatchMetadata = buildScheduledTaskDispatchMetadata({
|
|
1871
2014
|
nowMs: ctx.nowMs,
|
|
1872
2015
|
run,
|
|
1873
2016
|
task
|
|
1874
2017
|
});
|
|
1875
2018
|
} catch (error) {
|
|
1876
|
-
const errorMessage = error instanceof Error ? `Scheduled task
|
|
2019
|
+
const errorMessage = error instanceof Error ? `Scheduled task dispatch metadata could not be built: ${error.message}` : "Scheduled task dispatch metadata could not be built.";
|
|
1877
2020
|
await blockClaimedRun({
|
|
1878
2021
|
errorMessage,
|
|
1879
2022
|
nowMs: ctx.nowMs,
|
|
@@ -1888,11 +2031,9 @@ function createSchedulerPlugin() {
|
|
|
1888
2031
|
idempotencyKey: run.id,
|
|
1889
2032
|
...task.credentialSubject ? { credentialSubject: task.credentialSubject } : {},
|
|
1890
2033
|
destination: task.destination,
|
|
1891
|
-
input:
|
|
1892
|
-
metadata:
|
|
1893
|
-
|
|
1894
|
-
taskId: task.id
|
|
1895
|
-
}
|
|
2034
|
+
input: task.task.text,
|
|
2035
|
+
metadata: dispatchMetadata,
|
|
2036
|
+
source: scheduledTaskDispatchSource(task)
|
|
1896
2037
|
});
|
|
1897
2038
|
} catch (error) {
|
|
1898
2039
|
const errorMessage = error instanceof Error ? `Scheduled task dispatch could not be created: ${error.message}` : "Scheduled task dispatch could not be created.";
|
|
@@ -1917,7 +2058,16 @@ function createSchedulerPlugin() {
|
|
|
1917
2058
|
async operationalReport(ctx) {
|
|
1918
2059
|
return buildSchedulerOperationalReport({
|
|
1919
2060
|
nowMs: ctx.nowMs,
|
|
1920
|
-
store:
|
|
2061
|
+
store: schedulerOperationalStore(ctx)
|
|
2062
|
+
});
|
|
2063
|
+
},
|
|
2064
|
+
async migrateStorage(ctx) {
|
|
2065
|
+
if (!ctx.db) {
|
|
2066
|
+
throw new Error("Scheduler storage migration requires ctx.db");
|
|
2067
|
+
}
|
|
2068
|
+
return await migrateSchedulerStateToSql({
|
|
2069
|
+
db: ctx.db,
|
|
2070
|
+
state: ctx.state
|
|
1921
2071
|
});
|
|
1922
2072
|
}
|
|
1923
2073
|
}
|
|
@@ -1925,13 +2075,14 @@ function createSchedulerPlugin() {
|
|
|
1925
2075
|
}
|
|
1926
2076
|
var schedulerPlugin = createSchedulerPlugin;
|
|
1927
2077
|
export {
|
|
1928
|
-
|
|
2078
|
+
createSchedulerOperationalSqlStore,
|
|
1929
2079
|
createSchedulerPlugin,
|
|
1930
|
-
|
|
2080
|
+
createSchedulerSqlStore,
|
|
1931
2081
|
createSlackScheduleCreateTaskTool,
|
|
1932
2082
|
createSlackScheduleDeleteTaskTool,
|
|
1933
2083
|
createSlackScheduleListTasksTool,
|
|
1934
2084
|
createSlackScheduleRunTaskNowTool,
|
|
1935
2085
|
createSlackScheduleUpdateTaskTool,
|
|
2086
|
+
migrateSchedulerStateToSql,
|
|
1936
2087
|
schedulerPlugin
|
|
1937
2088
|
};
|