@sentry/junior-scheduler 0.107.1 → 0.109.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/index.js CHANGED
@@ -6,7 +6,6 @@ import {
6
6
 
7
7
  // src/store.ts
8
8
  import {
9
- pluginCredentialSubjectSchema,
10
9
  destinationSchema,
11
10
  isSlackDestination,
12
11
  slackActorSchema
@@ -25,26 +24,6 @@ import {
25
24
  import { z as z2 } from "zod";
26
25
 
27
26
  // src/cadence.ts
28
- function parseScheduleTimestamp(value) {
29
- const trimmed = value.trim();
30
- const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,9})?)?(Z|[+-]\d{2}:\d{2})$/.exec(
31
- trimmed
32
- );
33
- if (!match) {
34
- return void 0;
35
- }
36
- const year = Number(match[1]);
37
- const month = Number(match[2]);
38
- const day = Number(match[3]);
39
- const hour = Number(match[4]);
40
- const minute = Number(match[5]);
41
- const second = match[6] ? Number(match[6]) : 0;
42
- if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day) || !Number.isInteger(hour) || !Number.isInteger(minute) || !Number.isInteger(second) || month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month) || hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59) {
43
- return void 0;
44
- }
45
- const parsed = Date.parse(trimmed);
46
- return Number.isFinite(parsed) ? parsed : void 0;
47
- }
48
27
  var FORMATTERS = /* @__PURE__ */ new Map();
49
28
  function getFormatter(timezone) {
50
29
  const existing = FORMATTERS.get(timezone);
@@ -70,6 +49,9 @@ function normalizeHour(hour) {
70
49
  function getLocalDateWeekday(date) {
71
50
  return new Date(Date.UTC(date.year, date.month - 1, date.day)).getUTCDay();
72
51
  }
52
+ function getWeekStart(date) {
53
+ return addDays(date, -((getLocalDateWeekday(date) + 6) % 7));
54
+ }
73
55
  function getZonedDateTimeParts(timestampMs, timezone) {
74
56
  const parts = getFormatter(timezone).formatToParts(new Date(timestampMs));
75
57
  const values = new Map(parts.map((part) => [part.type, part.value]));
@@ -109,15 +91,163 @@ function localDateTimeToTimestampMs(args) {
109
91
  args.time.minute,
110
92
  0
111
93
  );
112
- let timestampMs = localAsUtcMs - getTimeZoneOffsetMs(localAsUtcMs, args.timezone);
113
- for (let index2 = 0; index2 < 3; index2 += 1) {
114
- const next = localAsUtcMs - getTimeZoneOffsetMs(timestampMs, args.timezone);
115
- if (next === timestampMs) {
116
- break;
94
+ const offsets = /* @__PURE__ */ new Set();
95
+ for (const probeDeltaMs of [
96
+ -36 * 60 * 60 * 1e3,
97
+ -12 * 60 * 60 * 1e3,
98
+ 0,
99
+ 12 * 60 * 60 * 1e3,
100
+ 36 * 60 * 60 * 1e3
101
+ ]) {
102
+ offsets.add(
103
+ getTimeZoneOffsetMs(localAsUtcMs + probeDeltaMs, args.timezone)
104
+ );
105
+ }
106
+ const matches = [...offsets].map((offsetMs) => localAsUtcMs - offsetMs).filter((timestampMs) => {
107
+ const parts = getZonedDateTimeParts(timestampMs, args.timezone);
108
+ return parts.year === args.date.year && parts.month === args.date.month && parts.day === args.date.day && parts.hour === args.time.hour && parts.minute === args.time.minute;
109
+ });
110
+ if (matches.length === 0) {
111
+ return void 0;
112
+ }
113
+ return Math.min(...matches);
114
+ }
115
+ function resolveLocalScheduleAtMs(args) {
116
+ const date = parseLocalDate(args.date);
117
+ if (!date) {
118
+ return void 0;
119
+ }
120
+ return localDateTimeToTimestampMs({
121
+ date,
122
+ time: args.time,
123
+ timezone: args.timezone
124
+ });
125
+ }
126
+ function daysBetween(left, right) {
127
+ return Math.floor(
128
+ (Date.UTC(right.year, right.month - 1, right.day) - Date.UTC(left.year, left.month - 1, left.day)) / (24 * 60 * 60 * 1e3)
129
+ );
130
+ }
131
+ function weeklyRecurrenceMatchesDate(date, start, recurrence) {
132
+ if (compareDate(date, start) < 0) {
133
+ return false;
134
+ }
135
+ return normalizeWeekdays(recurrence.weekdays).includes(
136
+ getLocalDateWeekday(date)
137
+ ) && Math.floor(daysBetween(getWeekStart(start), getWeekStart(date)) / 7) % recurrence.interval === 0;
138
+ }
139
+ function greatestCommonDivisor(left, right) {
140
+ let a = Math.abs(left);
141
+ let b = Math.abs(right);
142
+ while (b !== 0) {
143
+ [a, b] = [b, a % b];
144
+ }
145
+ return a;
146
+ }
147
+ function findNextRunAtMs(args) {
148
+ const start = parseLocalDate(args.recurrence.startDate);
149
+ const interval = args.recurrence.interval;
150
+ if (!start || !Number.isInteger(interval) || interval <= 0) {
151
+ return void 0;
152
+ }
153
+ const searchFrom = compareDate(args.searchFrom, start) < 0 ? start : args.searchFrom;
154
+ if (args.recurrence.frequency === "daily") {
155
+ const offsetDays = daysBetween(start, searchFrom);
156
+ let candidateDate = addDays(
157
+ start,
158
+ Math.ceil(offsetDays / interval) * interval
159
+ );
160
+ const gregorianCycleDays = 146097;
161
+ const candidateCount2 = gregorianCycleDays / greatestCommonDivisor(gregorianCycleDays, interval);
162
+ for (let attempts = 0; attempts < candidateCount2; attempts += 1) {
163
+ const candidate = buildCandidate({
164
+ date: candidateDate,
165
+ recurrence: args.recurrence,
166
+ timezone: args.timezone
167
+ });
168
+ if (candidate !== void 0 && candidate > args.afterMs) {
169
+ return candidate;
170
+ }
171
+ candidateDate = addDays(candidateDate, interval);
117
172
  }
118
- timestampMs = next;
173
+ return void 0;
174
+ }
175
+ if (args.recurrence.frequency === "weekly") {
176
+ if (normalizeWeekdays(args.recurrence.weekdays).length === 0) {
177
+ return void 0;
178
+ }
179
+ let candidateDate = searchFrom;
180
+ const gregorianCycleDays = 146097;
181
+ const cadenceDays = interval * 7;
182
+ const searchDays = gregorianCycleDays * cadenceDays / greatestCommonDivisor(gregorianCycleDays, cadenceDays);
183
+ for (let attempts = 0; attempts < searchDays; attempts += 1) {
184
+ if (weeklyRecurrenceMatchesDate(candidateDate, start, args.recurrence)) {
185
+ const candidate = buildCandidate({
186
+ date: candidateDate,
187
+ recurrence: args.recurrence,
188
+ timezone: args.timezone
189
+ });
190
+ if (candidate !== void 0 && candidate > args.afterMs) {
191
+ return candidate;
192
+ }
193
+ }
194
+ candidateDate = addDays(candidateDate, 1);
195
+ }
196
+ return void 0;
197
+ }
198
+ if (args.recurrence.frequency === "monthly") {
199
+ const startMonth = start.year * 12 + start.month - 1;
200
+ const searchMonth = searchFrom.year * 12 + searchFrom.month - 1;
201
+ let candidateMonth = startMonth + Math.ceil((searchMonth - startMonth) / interval) * interval;
202
+ const gregorianCycleMonths = 4800;
203
+ const candidateCount2 = gregorianCycleMonths / greatestCommonDivisor(gregorianCycleMonths, interval);
204
+ for (let attempts = 0; attempts < candidateCount2; attempts += 1) {
205
+ const candidateDate = {
206
+ year: Math.floor(candidateMonth / 12),
207
+ month: candidateMonth % 12 + 1,
208
+ day: args.recurrence.dayOfMonth ?? 0
209
+ };
210
+ if (compareDate(candidateDate, searchFrom) >= 0) {
211
+ const candidate = buildCandidate({
212
+ date: candidateDate,
213
+ recurrence: args.recurrence,
214
+ timezone: args.timezone
215
+ });
216
+ if (candidate !== void 0 && candidate > args.afterMs) {
217
+ return candidate;
218
+ }
219
+ }
220
+ candidateMonth += interval;
221
+ }
222
+ return void 0;
223
+ }
224
+ let candidateYear = start.year + Math.ceil((searchFrom.year - start.year) / interval) * interval;
225
+ const candidateCount = 400 / greatestCommonDivisor(400, interval);
226
+ for (let attempts = 0; attempts < candidateCount; attempts += 1) {
227
+ const candidateDate = {
228
+ year: candidateYear,
229
+ month: args.recurrence.month ?? 0,
230
+ day: args.recurrence.dayOfMonth ?? 0
231
+ };
232
+ if (compareDate(candidateDate, searchFrom) >= 0) {
233
+ const candidate = buildCandidate({
234
+ date: candidateDate,
235
+ recurrence: args.recurrence,
236
+ timezone: args.timezone
237
+ });
238
+ if (candidate !== void 0 && candidate > args.afterMs) {
239
+ return candidate;
240
+ }
241
+ }
242
+ candidateYear += interval;
119
243
  }
120
- return timestampMs;
244
+ return void 0;
245
+ }
246
+ function getFirstRunAtMs(args) {
247
+ return findNextRunAtMs({
248
+ ...args,
249
+ searchFrom: getLocalDate(args.afterMs, args.timezone)
250
+ });
121
251
  }
122
252
  function compareDate(left, right) {
123
253
  return Date.UTC(left.year, left.month - 1, left.day) - Date.UTC(right.year, right.month - 1, right.day);
@@ -146,13 +276,6 @@ function parseLocalDate(value) {
146
276
  }
147
277
  return { year, month, day };
148
278
  }
149
- function formatLocalDate(date) {
150
- return [
151
- String(date.year).padStart(4, "0"),
152
- String(date.month).padStart(2, "0"),
153
- String(date.day).padStart(2, "0")
154
- ].join("-");
155
- }
156
279
  function getLocalDate(timestampMs, timezone) {
157
280
  const parts = getZonedDateTimeParts(timestampMs, timezone);
158
281
  return { year: parts.year, month: parts.month, day: parts.day };
@@ -169,168 +292,6 @@ function buildCandidate(args) {
169
292
  timezone: args.timezone
170
293
  });
171
294
  }
172
- function getDailyNextRunAtMs(args) {
173
- const start = parseLocalDate(args.recurrence.startDate);
174
- if (!start) {
175
- return void 0;
176
- }
177
- let candidateDate = addDays(
178
- getLocalDate(args.scheduledForMs, args.timezone),
179
- args.recurrence.interval
180
- );
181
- if (compareDate(candidateDate, start) < 0) {
182
- candidateDate = start;
183
- }
184
- let candidate = buildCandidate({
185
- date: candidateDate,
186
- recurrence: args.recurrence,
187
- timezone: args.timezone
188
- });
189
- while (candidate <= args.afterMs) {
190
- candidateDate = addDays(candidateDate, args.recurrence.interval);
191
- candidate = buildCandidate({
192
- date: candidateDate,
193
- recurrence: args.recurrence,
194
- timezone: args.timezone
195
- });
196
- }
197
- return candidate;
198
- }
199
- function getWeeklyNextRunAtMs(args) {
200
- const start = parseLocalDate(args.recurrence.startDate);
201
- if (!start) {
202
- return void 0;
203
- }
204
- const weekdays = normalizeWeekdays(args.recurrence.weekdays);
205
- if (weekdays.length === 0) {
206
- return void 0;
207
- }
208
- let candidateDate = addDays(
209
- getLocalDate(args.scheduledForMs, args.timezone),
210
- 1
211
- );
212
- for (let attempts = 0; attempts < 3660; attempts += 1) {
213
- const weeksSinceStart = Math.floor(
214
- (Date.UTC(
215
- candidateDate.year,
216
- candidateDate.month - 1,
217
- candidateDate.day
218
- ) - Date.UTC(start.year, start.month - 1, start.day)) / (7 * 24 * 60 * 60 * 1e3)
219
- );
220
- const isInCycle = weeksSinceStart >= 0 && weeksSinceStart % args.recurrence.interval === 0;
221
- if (isInCycle && weekdays.includes(getLocalDateWeekday(candidateDate))) {
222
- const candidate = buildCandidate({
223
- date: candidateDate,
224
- recurrence: args.recurrence,
225
- timezone: args.timezone
226
- });
227
- if (candidate > args.afterMs) {
228
- return candidate;
229
- }
230
- }
231
- candidateDate = addDays(candidateDate, 1);
232
- }
233
- return void 0;
234
- }
235
- function getMonthlyNextRunAtMs(args) {
236
- const start = parseLocalDate(args.recurrence.startDate);
237
- const dayOfMonth = args.recurrence.dayOfMonth;
238
- if (!start || !dayOfMonth) {
239
- return void 0;
240
- }
241
- const scheduledDate = getLocalDate(args.scheduledForMs, args.timezone);
242
- let monthIndex = scheduledDate.year * 12 + scheduledDate.month - 1;
243
- const startMonthIndex = start.year * 12 + start.month - 1;
244
- for (let attempts = 0; attempts < 1200; attempts += 1) {
245
- monthIndex += args.recurrence.interval;
246
- if (monthIndex < startMonthIndex) {
247
- monthIndex = startMonthIndex;
248
- }
249
- const year = Math.floor(monthIndex / 12);
250
- const month = monthIndex % 12 + 1;
251
- if (dayOfMonth > daysInMonth(year, month)) {
252
- continue;
253
- }
254
- const candidate = buildCandidate({
255
- date: { year, month, day: dayOfMonth },
256
- recurrence: args.recurrence,
257
- timezone: args.timezone
258
- });
259
- if (candidate > args.afterMs) {
260
- return candidate;
261
- }
262
- }
263
- return void 0;
264
- }
265
- function getYearlyNextRunAtMs(args) {
266
- const start = parseLocalDate(args.recurrence.startDate);
267
- const month = args.recurrence.month;
268
- const dayOfMonth = args.recurrence.dayOfMonth;
269
- if (!start || !month || !dayOfMonth) {
270
- return void 0;
271
- }
272
- const scheduledDate = getLocalDate(args.scheduledForMs, args.timezone);
273
- let year = scheduledDate.year;
274
- for (let attempts = 0; attempts < 100; attempts += 1) {
275
- year += args.recurrence.interval;
276
- if (year < start.year) {
277
- year = start.year;
278
- }
279
- if (dayOfMonth > daysInMonth(year, month)) {
280
- continue;
281
- }
282
- const candidate = buildCandidate({
283
- date: { year, month, day: dayOfMonth },
284
- recurrence: args.recurrence,
285
- timezone: args.timezone
286
- });
287
- if (candidate > args.afterMs) {
288
- return candidate;
289
- }
290
- }
291
- return void 0;
292
- }
293
- function buildCalendarRecurrence(args) {
294
- const interval = args.interval && args.interval > 0 ? args.interval : 1;
295
- const parts = getZonedDateTimeParts(args.nextRunAtMs, args.timezone);
296
- const time = { hour: parts.hour, minute: parts.minute };
297
- const startDate = formatLocalDate(parts);
298
- if (args.frequency === "weekly") {
299
- const weekdays = normalizeWeekdays(args.weekdays);
300
- return {
301
- frequency: args.frequency,
302
- interval,
303
- startDate,
304
- time,
305
- weekdays: weekdays.length > 0 ? weekdays : [parts.weekday]
306
- };
307
- }
308
- if (args.frequency === "monthly") {
309
- return {
310
- dayOfMonth: parts.day,
311
- frequency: args.frequency,
312
- interval,
313
- startDate,
314
- time
315
- };
316
- }
317
- if (args.frequency === "yearly") {
318
- return {
319
- dayOfMonth: parts.day,
320
- frequency: args.frequency,
321
- interval,
322
- month: parts.month,
323
- startDate,
324
- time
325
- };
326
- }
327
- return {
328
- frequency: args.frequency,
329
- interval,
330
- startDate,
331
- time
332
- };
333
- }
334
295
  function getNextRunAtMs(task, scheduledForMs, afterMs = scheduledForMs) {
335
296
  if (task.schedule.kind !== "recurring") {
336
297
  return void 0;
@@ -339,35 +300,14 @@ function getNextRunAtMs(task, scheduledForMs, afterMs = scheduledForMs) {
339
300
  if (!recurrence || !Number.isFinite(recurrence.interval) || recurrence.interval <= 0) {
340
301
  return void 0;
341
302
  }
342
- if (recurrence.frequency === "daily") {
343
- return getDailyNextRunAtMs({
344
- recurrence,
345
- timezone: task.schedule.timezone,
346
- scheduledForMs,
347
- afterMs
348
- });
349
- }
350
- if (recurrence.frequency === "weekly") {
351
- return getWeeklyNextRunAtMs({
352
- recurrence,
353
- timezone: task.schedule.timezone,
354
- scheduledForMs,
355
- afterMs
356
- });
357
- }
358
- if (recurrence.frequency === "monthly") {
359
- return getMonthlyNextRunAtMs({
360
- recurrence,
361
- timezone: task.schedule.timezone,
362
- scheduledForMs,
363
- afterMs
364
- });
365
- }
366
- return getYearlyNextRunAtMs({
303
+ const timezone = task.schedule.timezone;
304
+ const afterDate = getLocalDate(afterMs, timezone);
305
+ const nextScheduledDate = addDays(getLocalDate(scheduledForMs, timezone), 1);
306
+ return findNextRunAtMs({
307
+ afterMs,
367
308
  recurrence,
368
- timezone: task.schedule.timezone,
369
- scheduledForMs,
370
- afterMs
309
+ searchFrom: compareDate(afterDate, nextScheduledDate) < 0 ? nextScheduledDate : afterDate,
310
+ timezone
371
311
  });
372
312
  }
373
313
 
@@ -488,10 +428,6 @@ var taskRecordSchema = z2.object({
488
428
  ...taskRecordFields,
489
429
  credentialMode: scheduledTaskCredentialModeSchema
490
430
  }).strict();
491
- var legacyTaskRecordSchema = z2.object({
492
- ...taskRecordFields,
493
- credentialSubject: pluginCredentialSubjectSchema.optional()
494
- }).strict();
495
431
  var runRecordSchema = z2.object({
496
432
  id: z2.string(),
497
433
  attempt: z2.number(),
@@ -523,30 +459,13 @@ function taskLockKey(taskId) {
523
459
  function runKey(runId) {
524
460
  return `${SCHEDULER_KEY_PREFIX}:run:${runId}`;
525
461
  }
526
- function activeRunKey(taskId) {
527
- return `${SCHEDULER_KEY_PREFIX}:active:${taskId}`;
528
- }
529
- function globalTaskIndexKey() {
530
- return `${SCHEDULER_KEY_PREFIX}:tasks`;
531
- }
532
462
  function indexLockKey(indexKey) {
533
463
  return `${indexKey}:lock`;
534
464
  }
535
465
  function buildRunId(taskId, scheduledForMs) {
536
466
  return `${taskId}:${scheduledForMs}`;
537
467
  }
538
- function unique(values) {
539
- return [...new Set(values.filter(Boolean))];
540
- }
541
- async function getIndex(state, key) {
542
- const values = await state.get(key) ?? [];
543
- return unique(
544
- values.filter((value) => typeof value === "string")
545
- );
546
- }
547
- function isFinishedRun(run) {
548
- return run.status === "completed" || run.status === "failed" || run.status === "blocked" || run.status === "skipped";
549
- }
468
+ var schedulerTaskIndexSchema = z2.array(z2.string().min(1));
550
469
  function isStalePendingRun(run, nowMs) {
551
470
  return run?.status === "pending" && run.claimedAtMs + PENDING_CLAIM_STALE_MS <= nowMs;
552
471
  }
@@ -622,25 +541,6 @@ function parseStoredTask(value) {
622
541
  const parsed = taskRecordSchema.safeParse(parseJsonRecord(value));
623
542
  return parsed.success ? stripLegacyTaskFields(parsed.data) : void 0;
624
543
  }
625
- function parseLegacyStoredTaskForMigration(value) {
626
- const parsed = legacyTaskRecordSchema.safeParse(parseJsonRecord(value));
627
- if (!parsed.success) {
628
- return void 0;
629
- }
630
- const {
631
- credentialSubject: _credentialSubject,
632
- version: _version,
633
- ...task
634
- } = parsed.data;
635
- return {
636
- ...task,
637
- credentialMode: "system"
638
- };
639
- }
640
- function parseStoredRun(value) {
641
- const parsed = runRecordSchema.safeParse(parseJsonRecord(value));
642
- return parsed.success ? stripLegacyRunFields(parsed.data) : void 0;
643
- }
644
544
  function stripLegacyTaskFields(task) {
645
545
  const { version: _version, ...current } = task;
646
546
  return current;
@@ -676,23 +576,6 @@ function requireStoredTask(task) {
676
576
  }
677
577
  return parsed;
678
578
  }
679
- async function getRunFromState(state, runId) {
680
- return parseStoredRun(await state.get(runKey(runId)));
681
- }
682
- async function listIncompleteRunsForTasksFromState(state, tasks) {
683
- const runs = [];
684
- for (const task of tasks) {
685
- const active = await state.get(activeRunKey(task.id));
686
- if (typeof active?.runId !== "string") {
687
- continue;
688
- }
689
- const run = await getRunFromState(state, active.runId);
690
- if (run && !isFinishedRun(run)) {
691
- runs.push(run);
692
- }
693
- }
694
- return runs;
695
- }
696
579
  function parseSqlTaskRecord(value) {
697
580
  const parsed = taskRecordSchema.safeParse(parseJsonRecord(value));
698
581
  return parsed.success ? stripLegacyTaskFields(parsed.data) : void 0;
@@ -798,6 +681,17 @@ var SqlSchedulerStore = class {
798
681
  this.db = db;
799
682
  }
800
683
  db;
684
+ async createTask(task) {
685
+ const next = requireStoredTask(task);
686
+ return await withSqlLock(this.db, taskLockKey(task.id), async (db) => {
687
+ const current = await getTaskFromSql(db, task.id);
688
+ if (current) {
689
+ return current;
690
+ }
691
+ await this.saveTaskRecord(db, next, void 0);
692
+ return next;
693
+ });
694
+ }
801
695
  async saveTask(task) {
802
696
  const next = requireStoredTask(task);
803
697
  await withSqlLock(this.db, taskLockKey(task.id), async (db) => {
@@ -1093,47 +987,6 @@ function createSchedulerSqlStore(db) {
1093
987
  function createSchedulerOperationalSqlStore(db) {
1094
988
  return new SqlSchedulerStore(db);
1095
989
  }
1096
- async function migrateSchedulerStateToSql(args) {
1097
- const store = createSchedulerSqlStore(args.db);
1098
- const ids = await getIndex(args.state, globalTaskIndexKey());
1099
- let existing = 0;
1100
- let migrated = 0;
1101
- let missing = 0;
1102
- const migratedTasks = [];
1103
- for (const id of ids) {
1104
- const rawTask = await args.state.get(taskKey(id));
1105
- const task = parseStoredTask(rawTask) ?? parseLegacyStoredTaskForMigration(rawTask);
1106
- if (!task) {
1107
- missing += 1;
1108
- continue;
1109
- }
1110
- migratedTasks.push(task);
1111
- if (await store.getTask(task.id)) {
1112
- existing += 1;
1113
- continue;
1114
- }
1115
- await store.saveTask(task);
1116
- migrated += 1;
1117
- }
1118
- const runs = await listIncompleteRunsForTasksFromState(
1119
- args.state,
1120
- migratedTasks
1121
- );
1122
- for (const run of runs) {
1123
- if (await store.getRun(run.id)) {
1124
- existing += 1;
1125
- continue;
1126
- }
1127
- await upsertSqlRun(args.db, run);
1128
- migrated += 1;
1129
- }
1130
- return {
1131
- existing,
1132
- migrated,
1133
- missing,
1134
- scanned: ids.length + runs.length
1135
- };
1136
- }
1137
990
 
1138
991
  // src/identity.ts
1139
992
  var SLACK_USER_ID_PATTERN = /^[UW][A-Z0-9]{5,}$/;
@@ -1181,55 +1034,304 @@ function scheduledTaskPrincipalLabel(principal) {
1181
1034
 
1182
1035
  // src/tools/create-task.ts
1183
1036
  import { definePluginTool } from "@sentry/junior-plugin-api";
1184
- import { z as z4 } from "zod";
1037
+ import { z as z5 } from "zod";
1038
+
1039
+ // src/schedule-intent.ts
1040
+ import { z as z3 } from "zod";
1041
+ var localDateSchema = z3.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Use a local date in YYYY-MM-DD format.");
1042
+ var localTimeSchema = z3.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/, "Use local time in HH:MM format.");
1043
+ var timezoneSchema = z3.string().min(1).max(80).describe(
1044
+ "IANA timezone, for example America/Los_Angeles. Omit or use null for the scheduler default."
1045
+ ).nullable().optional();
1046
+ var weekdaySchema = z3.enum([
1047
+ "sunday",
1048
+ "monday",
1049
+ "tuesday",
1050
+ "wednesday",
1051
+ "thursday",
1052
+ "friday",
1053
+ "saturday"
1054
+ ]);
1055
+ var oneOffScheduleIntentSchema = z3.object({
1056
+ kind: z3.literal("one_off").describe("A schedule that runs once."),
1057
+ timezone: timezoneSchema,
1058
+ timing: z3.discriminatedUnion("type", [
1059
+ z3.object({
1060
+ type: z3.literal("after").describe("Run after a relative delay from the server clock."),
1061
+ value: z3.number().int().positive().max(1e5),
1062
+ unit: z3.enum(["second", "minute", "hour", "day", "week"])
1063
+ }).strict(),
1064
+ z3.object({
1065
+ type: z3.literal("at").describe("Run at an explicit local calendar date and time."),
1066
+ date: localDateSchema.describe(
1067
+ "Requested local calendar date in YYYY-MM-DD format."
1068
+ ),
1069
+ time: localTimeSchema.describe(
1070
+ "Requested local wall-clock time in HH:MM format."
1071
+ )
1072
+ }).strict()
1073
+ ]).describe("Relative delay or explicit local time for the one-off run.")
1074
+ }).strict();
1075
+ var recurringScheduleIntentSchema = z3.object({
1076
+ kind: z3.literal("recurring").describe("A repeating calendar schedule."),
1077
+ frequency: z3.enum(["daily", "weekly", "monthly", "yearly"]).describe("Recurring tasks can run at most once per day."),
1078
+ interval: z3.number().int().positive().max(365).describe("Repeat every N frequency units. Defaults to 1.").nullable().optional(),
1079
+ time: localTimeSchema.describe(
1080
+ "Local wall-clock time for each occurrence in HH:MM format."
1081
+ ),
1082
+ weekdays: z3.array(weekdaySchema).min(1).max(7).describe("Required for weekly schedules.").nullable().optional(),
1083
+ day_of_month: z3.number().int().min(1).max(31).describe("Required for monthly and yearly schedules.").nullable().optional(),
1084
+ month: z3.number().int().min(1).max(12).describe("Required for yearly schedules, where January is 1.").nullable().optional(),
1085
+ start_date: localDateSchema.describe(
1086
+ "Optional local calendar date that anchors the recurrence. Omit to start with the next matching occurrence."
1087
+ ).nullable().optional(),
1088
+ timezone: timezoneSchema
1089
+ }).strict().superRefine((schedule, context) => {
1090
+ if (schedule.frequency === "weekly" && schedule.weekdays == null) {
1091
+ context.addIssue({
1092
+ code: "custom",
1093
+ message: "Weekly schedules require weekdays.",
1094
+ path: ["weekdays"]
1095
+ });
1096
+ }
1097
+ if ((schedule.frequency === "monthly" || schedule.frequency === "yearly") && schedule.day_of_month == null) {
1098
+ context.addIssue({
1099
+ code: "custom",
1100
+ message: `${schedule.frequency} schedules require day_of_month.`,
1101
+ path: ["day_of_month"]
1102
+ });
1103
+ }
1104
+ if (schedule.frequency === "yearly" && schedule.month == null) {
1105
+ context.addIssue({
1106
+ code: "custom",
1107
+ message: "Yearly schedules require month.",
1108
+ path: ["month"]
1109
+ });
1110
+ }
1111
+ if (schedule.frequency !== "weekly" && schedule.weekdays != null) {
1112
+ context.addIssue({
1113
+ code: "custom",
1114
+ message: "weekdays applies only to weekly schedules.",
1115
+ path: ["weekdays"]
1116
+ });
1117
+ }
1118
+ if (schedule.frequency !== "monthly" && schedule.frequency !== "yearly" && schedule.day_of_month != null) {
1119
+ context.addIssue({
1120
+ code: "custom",
1121
+ message: "day_of_month applies only to monthly or yearly schedules.",
1122
+ path: ["day_of_month"]
1123
+ });
1124
+ }
1125
+ if (schedule.frequency !== "yearly" && schedule.month != null) {
1126
+ context.addIssue({
1127
+ code: "custom",
1128
+ message: "month applies only to yearly schedules.",
1129
+ path: ["month"]
1130
+ });
1131
+ }
1132
+ });
1133
+ var scheduleIntentSchema = z3.union([
1134
+ oneOffScheduleIntentSchema,
1135
+ recurringScheduleIntentSchema
1136
+ ]);
1137
+ var WEEKDAY_INDEX = {
1138
+ sunday: 0,
1139
+ monday: 1,
1140
+ tuesday: 2,
1141
+ wednesday: 3,
1142
+ thursday: 4,
1143
+ friday: 5,
1144
+ saturday: 6
1145
+ };
1146
+ var DURATION_MS = {
1147
+ second: 1e3,
1148
+ minute: 60 * 1e3,
1149
+ hour: 60 * 60 * 1e3,
1150
+ day: 24 * 60 * 60 * 1e3,
1151
+ week: 7 * 24 * 60 * 60 * 1e3
1152
+ };
1153
+ function parseLocalTime(value) {
1154
+ const [hour, minute] = value.split(":").map(Number);
1155
+ return { hour, minute };
1156
+ }
1157
+ function localDateAt(timestampMs, timezone) {
1158
+ const parts = getZonedDateTimeParts(timestampMs, timezone);
1159
+ return [parts.year, parts.month, parts.day].map(
1160
+ (value, index2) => index2 === 0 ? String(value).padStart(4, "0") : String(value).padStart(2, "0")
1161
+ ).join("-");
1162
+ }
1163
+ function isValidTimeZone(timezone) {
1164
+ try {
1165
+ new Intl.DateTimeFormat("en-US", { timeZone: timezone }).format();
1166
+ return true;
1167
+ } catch {
1168
+ return false;
1169
+ }
1170
+ }
1171
+ function plural(value, unit) {
1172
+ return value === 1 ? unit : `${unit}s`;
1173
+ }
1174
+ function formatWeekdays(weekdays) {
1175
+ const labels = weekdays.map(
1176
+ (weekday) => `${weekday[0].toUpperCase()}${weekday.slice(1)}`
1177
+ );
1178
+ if (labels.length < 2) {
1179
+ return labels[0] ?? "";
1180
+ }
1181
+ return `${labels.slice(0, -1).join(", ")} and ${labels.at(-1)}`;
1182
+ }
1183
+ function recurringDescription(schedule, timezone) {
1184
+ const interval = schedule.interval ?? 1;
1185
+ const cadenceUnit = {
1186
+ daily: "day",
1187
+ weekly: "week",
1188
+ monthly: "month",
1189
+ yearly: "year"
1190
+ }[schedule.frequency];
1191
+ const cadence = interval === 1 ? cadenceUnit : `${interval} ${cadenceUnit}s`;
1192
+ let detail = "";
1193
+ if (schedule.frequency === "weekly") {
1194
+ detail = ` on ${formatWeekdays(schedule.weekdays)}`;
1195
+ } else if (schedule.frequency === "monthly") {
1196
+ detail = ` on day ${schedule.day_of_month}`;
1197
+ } else if (schedule.frequency === "yearly") {
1198
+ detail = ` on ${String(schedule.month).padStart(2, "0")}-${String(
1199
+ schedule.day_of_month
1200
+ ).padStart(2, "0")}`;
1201
+ }
1202
+ return `Every ${cadence}${detail} at ${schedule.time} (${timezone})`;
1203
+ }
1204
+ var ScheduleIntentError = class extends Error {
1205
+ };
1206
+ function compileScheduleIntent(args) {
1207
+ const timezone = args.intent.timezone ?? args.defaultTimezone;
1208
+ if (!isValidTimeZone(timezone)) {
1209
+ throw new ScheduleIntentError("timezone must be a valid IANA time zone.");
1210
+ }
1211
+ if (args.intent.kind === "one_off") {
1212
+ if (args.intent.timing.type === "after") {
1213
+ const nextRunAtMs3 = args.nowMs + args.intent.timing.value * DURATION_MS[args.intent.timing.unit];
1214
+ return {
1215
+ nextRunAtMs: nextRunAtMs3,
1216
+ schedule: {
1217
+ description: `In ${args.intent.timing.value} ${plural(
1218
+ args.intent.timing.value,
1219
+ args.intent.timing.unit
1220
+ )}`,
1221
+ kind: "one_off",
1222
+ timezone
1223
+ }
1224
+ };
1225
+ }
1226
+ const nextRunAtMs2 = resolveLocalScheduleAtMs({
1227
+ date: args.intent.timing.date,
1228
+ time: parseLocalTime(args.intent.timing.time),
1229
+ timezone
1230
+ });
1231
+ if (nextRunAtMs2 === void 0) {
1232
+ throw new ScheduleIntentError(
1233
+ "The requested local date or time does not exist in that timezone."
1234
+ );
1235
+ }
1236
+ if (nextRunAtMs2 <= args.nowMs) {
1237
+ throw new ScheduleIntentError(
1238
+ "The requested one-off schedule must be in the future."
1239
+ );
1240
+ }
1241
+ return {
1242
+ nextRunAtMs: nextRunAtMs2,
1243
+ schedule: {
1244
+ description: `Once on ${args.intent.timing.date} at ${args.intent.timing.time} (${timezone})`,
1245
+ kind: "one_off",
1246
+ timezone
1247
+ }
1248
+ };
1249
+ }
1250
+ const recurrence = {
1251
+ frequency: args.intent.frequency,
1252
+ interval: args.intent.interval ?? 1,
1253
+ startDate: args.intent.start_date ?? localDateAt(args.nowMs, timezone),
1254
+ time: parseLocalTime(args.intent.time),
1255
+ ...args.intent.frequency === "weekly" ? {
1256
+ weekdays: [
1257
+ ...new Set(
1258
+ args.intent.weekdays.map((weekday) => WEEKDAY_INDEX[weekday])
1259
+ )
1260
+ ].sort((left, right) => left - right)
1261
+ } : {},
1262
+ ...args.intent.frequency === "monthly" || args.intent.frequency === "yearly" ? { dayOfMonth: args.intent.day_of_month ?? void 0 } : {},
1263
+ ...args.intent.frequency === "yearly" ? { month: args.intent.month ?? void 0 } : {}
1264
+ };
1265
+ const searchRecurrence = args.intent.start_date ? recurrence : { ...recurrence, interval: 1 };
1266
+ const nextRunAtMs = getFirstRunAtMs({
1267
+ afterMs: args.nowMs,
1268
+ recurrence: searchRecurrence,
1269
+ timezone
1270
+ });
1271
+ if (nextRunAtMs === void 0) {
1272
+ throw new ScheduleIntentError(
1273
+ "The recurring schedule has no valid future occurrence."
1274
+ );
1275
+ }
1276
+ const materializedRecurrence = args.intent.start_date ? recurrence : { ...recurrence, startDate: localDateAt(nextRunAtMs, timezone) };
1277
+ return {
1278
+ nextRunAtMs,
1279
+ schedule: {
1280
+ description: recurringDescription(args.intent, timezone),
1281
+ kind: "recurring",
1282
+ recurrence: materializedRecurrence,
1283
+ timezone
1284
+ }
1285
+ };
1286
+ }
1185
1287
 
1186
1288
  // src/tool-support.ts
1187
- import { randomUUID } from "crypto";
1289
+ import { createHash } from "crypto";
1188
1290
  import {
1189
1291
  PluginToolInputError,
1190
1292
  pluginToolResultSchema,
1191
1293
  sourceSchema
1192
1294
  } from "@sentry/junior-plugin-api";
1193
- import { z as z3 } from "zod";
1295
+ import { z as z4 } from "zod";
1194
1296
  var TASK_ID_PREFIX = "sched";
1195
1297
  var MAX_LISTED_TASKS = 50;
1196
1298
  var DEFAULT_SCHEDULE_TIMEZONE = "America/Los_Angeles";
1197
- var compactTaskResultSchema = z3.object({
1198
- id: z3.string(),
1199
- status: z3.enum(["active", "paused", "blocked", "deleted"]),
1200
- task: z3.string(),
1201
- schedule: z3.string(),
1202
- timezone: z3.string(),
1203
- recurrence: z3.unknown().nullable(),
1204
- next_run_at: z3.string().nullable(),
1205
- conversation_access: z3.unknown().nullable(),
1206
- credential_mode: z3.enum(["system", "creator"]),
1207
- last_run_at: z3.string().nullable(),
1208
- run_now_at: z3.string().nullable()
1299
+ var compactTaskResultSchema = z4.object({
1300
+ id: z4.string(),
1301
+ status: z4.enum(["active", "paused", "blocked", "deleted"]),
1302
+ task: z4.string(),
1303
+ schedule: z4.string(),
1304
+ timezone: z4.string(),
1305
+ recurrence: z4.unknown().nullable(),
1306
+ next_run_at: z4.string().nullable(),
1307
+ conversation_access: z4.unknown().nullable(),
1308
+ credential_mode: z4.enum(["system", "creator"]),
1309
+ last_run_at: z4.string().nullable(),
1310
+ run_now_at: z4.string().nullable()
1209
1311
  }).strict();
1210
- var scheduleTaskResultDataSchema = z3.object({
1211
- ok: z3.literal(true),
1312
+ var scheduleTaskResultDataSchema = z4.object({
1313
+ ok: z4.literal(true),
1212
1314
  task: compactTaskResultSchema
1213
1315
  }).strict();
1214
1316
  var scheduleTaskToolResultSchema = pluginToolResultSchema.extend({
1215
- ok: z3.literal(true),
1216
- status: z3.literal("success"),
1217
- target: z3.string(),
1317
+ ok: z4.literal(true),
1318
+ status: z4.literal("success"),
1319
+ target: z4.string(),
1218
1320
  data: scheduleTaskResultDataSchema,
1219
1321
  task: compactTaskResultSchema
1220
1322
  });
1221
- var scheduleListResultDataSchema = z3.object({
1222
- ok: z3.literal(true),
1223
- tasks: z3.array(compactTaskResultSchema),
1224
- truncated: z3.boolean()
1323
+ var scheduleListResultDataSchema = z4.object({
1324
+ ok: z4.literal(true),
1325
+ tasks: z4.array(compactTaskResultSchema),
1326
+ truncated: z4.boolean()
1225
1327
  }).strict();
1226
1328
  var scheduleListToolResultSchema = pluginToolResultSchema.extend({
1227
- ok: z3.literal(true),
1228
- status: z3.literal("success"),
1229
- target: z3.string(),
1329
+ ok: z4.literal(true),
1330
+ status: z4.literal("success"),
1331
+ target: z4.string(),
1230
1332
  data: scheduleListResultDataSchema,
1231
- tasks: z3.array(compactTaskResultSchema),
1232
- truncated: z3.boolean()
1333
+ tasks: z4.array(compactTaskResultSchema),
1334
+ truncated: z4.boolean()
1233
1335
  });
1234
1336
  function throwToolInputError(error) {
1235
1337
  throw new PluginToolInputError(error);
@@ -1363,8 +1465,21 @@ function scheduleListToolResult(args) {
1363
1465
  truncated: args.truncated
1364
1466
  };
1365
1467
  }
1366
- function buildTaskId() {
1367
- return `${TASK_ID_PREFIX}_${randomUUID()}`;
1468
+ function buildTaskId(args) {
1469
+ const toolCallId = args.toolCallId?.trim();
1470
+ if (!toolCallId) {
1471
+ throw new Error("Scheduler task creation requires a tool-call identity.");
1472
+ }
1473
+ const digest = createHash("sha256").update(
1474
+ JSON.stringify({
1475
+ actor: args.actor.slackUserId,
1476
+ channel: args.destination.channelId,
1477
+ operation: toolCallId,
1478
+ platform: args.destination.platform,
1479
+ team: args.destination.teamId
1480
+ })
1481
+ ).digest("hex").slice(0, 32);
1482
+ return `${TASK_ID_PREFIX}_${digest}`;
1368
1483
  }
1369
1484
  function schedulerStore(context) {
1370
1485
  return context.store;
@@ -1375,133 +1490,64 @@ function normalizeStatus(value) {
1375
1490
  }
1376
1491
  return void 0;
1377
1492
  }
1378
- function normalizeFrequency(value) {
1379
- if (value === "daily" || value === "weekly" || value === "monthly" || value === "yearly") {
1380
- return value;
1381
- }
1382
- return void 0;
1383
- }
1384
- function buildRecurrence(args) {
1385
- if (args.input.recurrence === null) {
1386
- return void 0;
1387
- }
1388
- const frequency = normalizeFrequency(args.input.recurrence) ?? args.existing?.frequency;
1389
- if (!frequency) {
1390
- return void 0;
1391
- }
1392
- if (!args.nextRunAtMs) {
1393
- throwToolInputError("Recurring scheduled tasks require next_run_at.");
1394
- }
1395
- try {
1396
- return buildCalendarRecurrence({
1397
- frequency,
1398
- interval: args.existing?.interval,
1399
- nextRunAtMs: args.nextRunAtMs,
1400
- timezone: args.timezone,
1401
- weekdays: frequency === "weekly" ? args.existing?.weekdays : void 0
1402
- });
1403
- } catch (error) {
1404
- throwToolInputError(
1405
- error instanceof RangeError ? "timezone must be a valid IANA time zone." : error instanceof Error ? error.message : String(error)
1406
- );
1407
- }
1408
- }
1409
- function validateRecurringFrequencyLimit(input) {
1410
- if (input.recurrence !== void 0 && input.recurrence !== null && !normalizeFrequency(input.recurrence)) {
1411
- throwToolInputError(
1412
- "Recurring scheduled tasks can run at most once per day."
1413
- );
1414
- }
1415
- }
1416
- function validateCreateScheduleKind(input) {
1417
- if (input.schedule_kind === void 0) {
1418
- throwToolInputError("Provide schedule_kind as one_off or recurring.");
1419
- }
1420
- if (input.schedule_kind !== "one_off" && input.schedule_kind !== "recurring") {
1421
- throwToolInputError("schedule_kind must be one_off or recurring.");
1422
- }
1423
- if (input.schedule_kind === "one_off" && input.recurrence !== void 0 && input.recurrence !== null) {
1424
- throwToolInputError("Omit recurrence when schedule_kind is one_off.");
1425
- }
1426
- if (input.schedule_kind === "recurring" && (input.recurrence === void 0 || input.recurrence === null)) {
1427
- throwToolInputError("Provide recurrence when schedule_kind is recurring.");
1428
- }
1429
- }
1430
- function shouldRebuildRecurrence(input) {
1431
- return input.next_run_at !== void 0 || input.recurrence !== void 0 || input.timezone !== void 0;
1432
- }
1433
1493
  function getDefaultScheduleTimezone() {
1434
1494
  return process.env.JUNIOR_TIMEZONE?.trim() || DEFAULT_SCHEDULE_TIMEZONE;
1435
1495
  }
1436
- function isValidTimeZone(timezone) {
1437
- try {
1438
- new Intl.DateTimeFormat("en-US", { timeZone: timezone }).format();
1439
- return true;
1440
- } catch {
1441
- return false;
1442
- }
1443
- }
1444
- function parseNextRunAtMs(nextRunAtIso) {
1445
- try {
1446
- if (nextRunAtIso) {
1447
- return parseScheduleTimestamp(nextRunAtIso);
1448
- }
1449
- } catch {
1450
- return void 0;
1451
- }
1452
- return void 0;
1453
- }
1454
1496
 
1455
1497
  // src/tools/create-task.ts
1456
1498
  function createSlackScheduleCreateTaskTool(context) {
1457
1499
  return definePluginTool({
1458
- 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. Do not create scheduled polling tasks to watch provider resources when a subscribable tool result can deliver the requested events. Only manage tasks for the active Slack DM or channel; never target threads, other channels, or another user's DM. Set credential_mode to creator only when the current user explicitly authorizes future scheduled use of their connected credentials. If connected-credential use is needed but authorization is ambiguous, ask before creating the task. Otherwise omit credential_mode and use system credentials. When the task, schedule, destination, and credential mode are clear, create it without another confirmation.",
1500
+ description: "Create a one-time or recurring Junior task in the active Slack conversation when the user asks Junior to do work later or repeatedly.",
1459
1501
  executionMode: "sequential",
1460
- inputSchema: z4.object({
1461
- task: z4.string().min(1).max(4e3),
1462
- schedule: z4.string().min(1).max(300),
1463
- schedule_kind: z4.enum(["one_off", "recurring"]).describe(
1464
- "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."
1502
+ inputSchema: z5.object({
1503
+ task: z5.string().min(1).max(4e3),
1504
+ schedule: scheduleIntentSchema.describe(
1505
+ "When the task runs. The scheduler computes the exact next run from this intent and the server clock."
1465
1506
  ),
1466
- timezone: z4.string().min(1).max(80).describe(
1467
- "IANA timezone, e.g. 'America/Los_Angeles'. Defaults to the channel's configured timezone."
1468
- ).optional(),
1469
- next_run_at: z4.string().min(1).describe(
1470
- "Exact next run time as an ISO timestamp, computed from the user's requested schedule."
1471
- ).optional(),
1472
- recurrence: z4.enum(["daily", "weekly", "monthly", "yearly"]).nullable().describe(
1473
- "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."
1474
- ).optional(),
1475
- credential_mode: z4.enum(["system", "creator"]).nullable().describe(
1507
+ credential_mode: z5.enum(["system", "creator"]).nullable().describe(
1476
1508
  "Use creator only when the current user explicitly authorizes future scheduled use of their connected credentials. Omit or use system otherwise."
1477
1509
  ).optional()
1478
- }),
1510
+ }).strict(),
1479
1511
  outputSchema: scheduleTaskToolResultSchema,
1480
- execute: async (input) => {
1512
+ execute: async (input, options) => {
1481
1513
  const destination = requireActiveConversation(context);
1482
1514
  const actor = requireActor(context, destination);
1483
- const nowMs = Date.now();
1484
- const timezone = input.timezone ?? getDefaultScheduleTimezone();
1485
- validateCreateScheduleKind(input);
1486
- validateRecurringFrequencyLimit(input);
1487
- if (!isValidTimeZone(timezone)) {
1488
- throwToolInputError("timezone must be a valid IANA time zone.");
1515
+ const store = schedulerStore(context);
1516
+ const id = buildTaskId({
1517
+ actor,
1518
+ destination,
1519
+ toolCallId: options.toolCallId
1520
+ });
1521
+ const existing = await store.getTask(id);
1522
+ if (existing) {
1523
+ if (!sameDestination(existing, destination) || existing.createdBy.slackUserId !== actor.slackUserId) {
1524
+ throwToolInputError("Scheduled task operation identity is invalid.");
1525
+ }
1526
+ return scheduleTaskToolResult(
1527
+ "slackScheduleCreateTask",
1528
+ compactTask(existing)
1529
+ );
1489
1530
  }
1490
- const nextRunAtMs = parseNextRunAtMs(input.next_run_at);
1491
- if (!nextRunAtMs) {
1492
- throwToolInputError("Provide next_run_at as a valid ISO timestamp.");
1531
+ const nowMs = context.now?.() ?? Date.now();
1532
+ let compiled;
1533
+ try {
1534
+ compiled = compileScheduleIntent({
1535
+ defaultTimezone: getDefaultScheduleTimezone(),
1536
+ intent: input.schedule,
1537
+ nowMs
1538
+ });
1539
+ } catch (error) {
1540
+ if (error instanceof ScheduleIntentError) {
1541
+ throwToolInputError(error.message);
1542
+ }
1543
+ throw error;
1493
1544
  }
1494
- const recurrence = buildRecurrence({
1495
- input,
1496
- nextRunAtMs,
1497
- timezone
1498
- });
1499
1545
  const conversationAccess = getConversationAccess(
1500
1546
  destination,
1501
1547
  context.source
1502
1548
  );
1503
1549
  const task = {
1504
- id: buildTaskId(),
1550
+ id,
1505
1551
  createdAtMs: nowMs,
1506
1552
  updatedAtMs: nowMs,
1507
1553
  createdBy: actor,
@@ -1509,23 +1555,21 @@ function createSlackScheduleCreateTaskTool(context) {
1509
1555
  credentialMode: input.credential_mode ?? "system",
1510
1556
  destination,
1511
1557
  executionActor: SCHEDULED_TASK_SYSTEM_ACTOR,
1512
- nextRunAtMs,
1558
+ nextRunAtMs: compiled.nextRunAtMs,
1513
1559
  originalRequest: context.userText,
1514
- schedule: {
1515
- description: input.schedule,
1516
- timezone,
1517
- kind: recurrence ? "recurring" : "one_off",
1518
- recurrence
1519
- },
1560
+ schedule: compiled.schedule,
1520
1561
  status: "active",
1521
1562
  task: {
1522
1563
  text: input.task
1523
1564
  }
1524
1565
  };
1525
- await schedulerStore(context).saveTask(task);
1566
+ const committed = await store.createTask(task);
1567
+ if (!sameDestination(committed, destination) || committed.createdBy.slackUserId !== actor.slackUserId) {
1568
+ throwToolInputError("Scheduled task operation identity is invalid.");
1569
+ }
1526
1570
  return scheduleTaskToolResult(
1527
1571
  "slackScheduleCreateTask",
1528
- compactTask(task)
1572
+ compactTask(committed)
1529
1573
  );
1530
1574
  }
1531
1575
  });
@@ -1533,13 +1577,13 @@ function createSlackScheduleCreateTaskTool(context) {
1533
1577
 
1534
1578
  // src/tools/delete-task.ts
1535
1579
  import { definePluginTool as definePluginTool2 } from "@sentry/junior-plugin-api";
1536
- import { z as z5 } from "zod";
1580
+ import { z as z6 } from "zod";
1537
1581
  function createSlackScheduleDeleteTaskTool(context) {
1538
1582
  return definePluginTool2({
1539
1583
  description: "Delete one scheduled Junior task from the active Slack conversation. Use only task IDs returned for this conversation. Do not delete schedules from threads, other channels, or another user's DM.",
1540
1584
  executionMode: "sequential",
1541
- inputSchema: z5.object({
1542
- task_id: z5.string().min(1).describe(
1585
+ inputSchema: z6.object({
1586
+ task_id: z6.string().min(1).describe(
1543
1587
  "ID of the task to delete. Must be from this active Slack conversation."
1544
1588
  )
1545
1589
  }),
@@ -1564,12 +1608,12 @@ function createSlackScheduleDeleteTaskTool(context) {
1564
1608
 
1565
1609
  // src/tools/list-tasks.ts
1566
1610
  import { definePluginTool as definePluginTool3 } from "@sentry/junior-plugin-api";
1567
- import { z as z6 } from "zod";
1611
+ import { z as z7 } from "zod";
1568
1612
  function createSlackScheduleListTasksTool(context) {
1569
1613
  return definePluginTool3({
1570
1614
  description: "List scheduled Junior tasks for the active Slack conversation. Use when the user asks what is scheduled here, or when task IDs are needed before editing, deleting, or running schedules. Only manages tasks for the active Slack DM or channel.",
1571
1615
  annotations: { readOnlyHint: true, destructiveHint: false },
1572
- inputSchema: z6.object({}),
1616
+ inputSchema: z7.object({}),
1573
1617
  outputSchema: scheduleListToolResultSchema,
1574
1618
  execute: async () => {
1575
1619
  const destination = requireActiveConversation(context);
@@ -1591,13 +1635,13 @@ function createSlackScheduleListTasksTool(context) {
1591
1635
 
1592
1636
  // src/tools/run-task-now.ts
1593
1637
  import { definePluginTool as definePluginTool4 } from "@sentry/junior-plugin-api";
1594
- import { z as z7 } from "zod";
1638
+ import { z as z8 } from "zod";
1595
1639
  function createSlackScheduleRunTaskNowTool(context) {
1596
1640
  return definePluginTool4({
1597
1641
  description: "Queue an existing active scheduled Junior task to run as soon as possible, without changing its cadence. Use when the user asks to run an existing scheduled task now. Use only task IDs returned for this conversation.",
1598
1642
  executionMode: "sequential",
1599
- inputSchema: z7.object({
1600
- task_id: z7.string().min(1).describe(
1643
+ inputSchema: z8.object({
1644
+ task_id: z8.string().min(1).describe(
1601
1645
  "ID of the active task to run now. Must be from this active Slack conversation."
1602
1646
  )
1603
1647
  }),
@@ -1626,29 +1670,26 @@ function createSlackScheduleRunTaskNowTool(context) {
1626
1670
 
1627
1671
  // src/tools/update-task.ts
1628
1672
  import { definePluginTool as definePluginTool5 } from "@sentry/junior-plugin-api";
1629
- import { z as z8 } from "zod";
1673
+ import { z as z9 } from "zod";
1630
1674
  function createSlackScheduleUpdateTaskTool(context) {
1631
1675
  return definePluginTool5({
1632
- description: "Edit, pause, resume, reschedule, or change credential use for an existing Junior scheduled task in the active Slack conversation. Use only task IDs returned for this conversation. Do not move scheduled tasks across conversations. Set credential_mode to creator only when the task creator explicitly authorizes future scheduled use of their connected credentials. If creator credential use is requested but authorization is ambiguous, ask before enabling it. If another user changes the executable task text, creator credential use is cleared automatically.",
1676
+ description: "Edit, pause, resume, reschedule, or change credential use for an existing Junior scheduled task in the active Slack conversation.",
1633
1677
  executionMode: "sequential",
1634
- inputSchema: z8.object({
1635
- task_id: z8.string().min(1).describe(
1678
+ inputSchema: z9.object({
1679
+ task_id: z9.string().min(1).describe(
1636
1680
  "ID of the task to update. Must be from this active Slack conversation."
1637
1681
  ),
1638
- task: z8.string().min(1).max(4e3).optional(),
1639
- schedule: z8.string().min(1).max(300).optional(),
1640
- timezone: z8.string().min(1).max(80).optional(),
1641
- next_run_at: z8.string().min(1).describe("Exact ISO timestamp when changing the next run time.").optional(),
1642
- recurrence: z8.enum(["daily", "weekly", "monthly", "yearly"]).nullable().describe(
1643
- "Provide only for repeating schedules. Omit for one-time requests. Set to null to convert a recurring task to one-time."
1644
- ).optional(),
1645
- status: z8.enum(["active", "paused", "blocked"]).describe(
1682
+ task: z9.string().min(1).max(4e3).optional(),
1683
+ schedule: scheduleIntentSchema.describe(
1684
+ "Complete replacement schedule when rescheduling. Omit for task, status, or credential-only changes; the scheduler computes the next run."
1685
+ ).nullable().optional(),
1686
+ status: z9.enum(["active", "paused", "blocked"]).describe(
1646
1687
  "Set to active, paused, or blocked to resume, pause, or block the task."
1647
1688
  ).optional(),
1648
- credential_mode: z8.enum(["system", "creator"]).nullable().describe(
1689
+ credential_mode: z9.enum(["system", "creator"]).nullable().describe(
1649
1690
  "Set creator only when the current actor is the task creator and explicitly authorizes future scheduled credential use. Set system to disable delegation."
1650
1691
  ).optional()
1651
- }),
1692
+ }).strict(),
1652
1693
  outputSchema: scheduleTaskToolResultSchema,
1653
1694
  execute: async (input) => {
1654
1695
  const lookup = await getWritableTask({
@@ -1662,48 +1703,43 @@ function createSlackScheduleUpdateTaskTool(context) {
1662
1703
  "Only the scheduled task creator can enable creator credential use."
1663
1704
  );
1664
1705
  }
1665
- const timezone = input.timezone ?? lookup.schedule.timezone;
1666
- validateRecurringFrequencyLimit(input);
1667
- if (!isValidTimeZone(timezone)) {
1668
- throwToolInputError("timezone must be a valid IANA time zone.");
1669
- }
1670
- const parsedNextRunAtMs = parseNextRunAtMs(input.next_run_at);
1671
- const nextRunAtMs = input.next_run_at ? parsedNextRunAtMs : lookup.nextRunAtMs;
1672
- if (input.next_run_at && !nextRunAtMs) {
1673
- throwToolInputError("Provide next_run_at as a valid ISO timestamp.");
1706
+ const nowMs = context.now?.() ?? Date.now();
1707
+ let compiled;
1708
+ if (input.schedule) {
1709
+ try {
1710
+ compiled = compileScheduleIntent({
1711
+ defaultTimezone: lookup.schedule.timezone || getDefaultScheduleTimezone(),
1712
+ intent: input.schedule,
1713
+ nowMs
1714
+ });
1715
+ } catch (error) {
1716
+ if (error instanceof ScheduleIntentError) {
1717
+ throwToolInputError(error.message);
1718
+ }
1719
+ throw error;
1720
+ }
1674
1721
  }
1722
+ const nextRunAtMs = compiled?.nextRunAtMs ?? lookup.nextRunAtMs;
1675
1723
  const status = normalizeStatus(input.status);
1676
1724
  if (input.status && !status) {
1677
1725
  throwToolInputError("status must be active, paused, or blocked.");
1678
1726
  }
1679
1727
  if (status === "active" && !nextRunAtMs) {
1680
1728
  throwToolInputError(
1681
- "Active scheduled tasks require next_run_at when no next run is stored."
1729
+ "Active scheduled tasks require a schedule with a future occurrence."
1682
1730
  );
1683
1731
  }
1684
- const recurrence = shouldRebuildRecurrence(input) ? buildRecurrence({
1685
- existing: lookup.schedule.recurrence,
1686
- input,
1687
- nextRunAtMs,
1688
- timezone
1689
- }) : lookup.schedule.recurrence;
1690
1732
  const nextStatus = status ?? lookup.status;
1691
1733
  const credentialMode = input.task !== void 0 && input.task !== lookup.task.text && !isCreator ? "system" : input.credential_mode ?? lookup.credentialMode;
1692
1734
  const next = {
1693
1735
  ...lookup,
1694
1736
  credentialMode,
1695
- updatedAtMs: Date.now(),
1737
+ updatedAtMs: nowMs,
1696
1738
  nextRunAtMs,
1697
- runNowAtMs: nextStatus === "active" ? lookup.runNowAtMs : void 0,
1739
+ runNowAtMs: nextStatus === "active" && !compiled ? lookup.runNowAtMs : void 0,
1698
1740
  status: nextStatus,
1699
1741
  statusReason: nextStatus === "blocked" ? lookup.statusReason : void 0,
1700
- schedule: {
1701
- ...lookup.schedule,
1702
- description: input.schedule ?? lookup.schedule.description,
1703
- timezone,
1704
- kind: recurrence ? "recurring" : "one_off",
1705
- recurrence
1706
- },
1742
+ schedule: compiled?.schedule ?? lookup.schedule,
1707
1743
  task: input.task ? { text: input.task } : lookup.task
1708
1744
  };
1709
1745
  await schedulerStore(context).saveTask(next);
@@ -2156,12 +2192,6 @@ function createSchedulerPlugin() {
2156
2192
  nowMs: ctx.nowMs,
2157
2193
  store: schedulerOperationalStore(ctx)
2158
2194
  });
2159
- },
2160
- async migrateStorage(ctx) {
2161
- return await migrateSchedulerStateToSql({
2162
- db: ctx.db,
2163
- state: ctx.state
2164
- });
2165
2195
  }
2166
2196
  }
2167
2197
  });
@@ -2176,6 +2206,5 @@ export {
2176
2206
  createSlackScheduleListTasksTool,
2177
2207
  createSlackScheduleRunTaskNowTool,
2178
2208
  createSlackScheduleUpdateTaskTool,
2179
- migrateSchedulerStateToSql,
2180
2209
  schedulerPlugin
2181
2210
  };