@sentry/junior-scheduler 0.107.0 → 0.108.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
@@ -25,26 +25,6 @@ import {
25
25
  import { z as z2 } from "zod";
26
26
 
27
27
  // 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
28
  var FORMATTERS = /* @__PURE__ */ new Map();
49
29
  function getFormatter(timezone) {
50
30
  const existing = FORMATTERS.get(timezone);
@@ -70,6 +50,9 @@ function normalizeHour(hour) {
70
50
  function getLocalDateWeekday(date) {
71
51
  return new Date(Date.UTC(date.year, date.month - 1, date.day)).getUTCDay();
72
52
  }
53
+ function getWeekStart(date) {
54
+ return addDays(date, -((getLocalDateWeekday(date) + 6) % 7));
55
+ }
73
56
  function getZonedDateTimeParts(timestampMs, timezone) {
74
57
  const parts = getFormatter(timezone).formatToParts(new Date(timestampMs));
75
58
  const values = new Map(parts.map((part) => [part.type, part.value]));
@@ -109,15 +92,163 @@ function localDateTimeToTimestampMs(args) {
109
92
  args.time.minute,
110
93
  0
111
94
  );
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;
95
+ const offsets = /* @__PURE__ */ new Set();
96
+ for (const probeDeltaMs of [
97
+ -36 * 60 * 60 * 1e3,
98
+ -12 * 60 * 60 * 1e3,
99
+ 0,
100
+ 12 * 60 * 60 * 1e3,
101
+ 36 * 60 * 60 * 1e3
102
+ ]) {
103
+ offsets.add(
104
+ getTimeZoneOffsetMs(localAsUtcMs + probeDeltaMs, args.timezone)
105
+ );
106
+ }
107
+ const matches = [...offsets].map((offsetMs) => localAsUtcMs - offsetMs).filter((timestampMs) => {
108
+ const parts = getZonedDateTimeParts(timestampMs, args.timezone);
109
+ 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;
110
+ });
111
+ if (matches.length === 0) {
112
+ return void 0;
113
+ }
114
+ return Math.min(...matches);
115
+ }
116
+ function resolveLocalScheduleAtMs(args) {
117
+ const date = parseLocalDate(args.date);
118
+ if (!date) {
119
+ return void 0;
120
+ }
121
+ return localDateTimeToTimestampMs({
122
+ date,
123
+ time: args.time,
124
+ timezone: args.timezone
125
+ });
126
+ }
127
+ function daysBetween(left, right) {
128
+ return Math.floor(
129
+ (Date.UTC(right.year, right.month - 1, right.day) - Date.UTC(left.year, left.month - 1, left.day)) / (24 * 60 * 60 * 1e3)
130
+ );
131
+ }
132
+ function weeklyRecurrenceMatchesDate(date, start, recurrence) {
133
+ if (compareDate(date, start) < 0) {
134
+ return false;
135
+ }
136
+ return normalizeWeekdays(recurrence.weekdays).includes(
137
+ getLocalDateWeekday(date)
138
+ ) && Math.floor(daysBetween(getWeekStart(start), getWeekStart(date)) / 7) % recurrence.interval === 0;
139
+ }
140
+ function greatestCommonDivisor(left, right) {
141
+ let a = Math.abs(left);
142
+ let b = Math.abs(right);
143
+ while (b !== 0) {
144
+ [a, b] = [b, a % b];
145
+ }
146
+ return a;
147
+ }
148
+ function findNextRunAtMs(args) {
149
+ const start = parseLocalDate(args.recurrence.startDate);
150
+ const interval = args.recurrence.interval;
151
+ if (!start || !Number.isInteger(interval) || interval <= 0) {
152
+ return void 0;
153
+ }
154
+ const searchFrom = compareDate(args.searchFrom, start) < 0 ? start : args.searchFrom;
155
+ if (args.recurrence.frequency === "daily") {
156
+ const offsetDays = daysBetween(start, searchFrom);
157
+ let candidateDate = addDays(
158
+ start,
159
+ Math.ceil(offsetDays / interval) * interval
160
+ );
161
+ const gregorianCycleDays = 146097;
162
+ const candidateCount2 = gregorianCycleDays / greatestCommonDivisor(gregorianCycleDays, interval);
163
+ for (let attempts = 0; attempts < candidateCount2; attempts += 1) {
164
+ const candidate = buildCandidate({
165
+ date: candidateDate,
166
+ recurrence: args.recurrence,
167
+ timezone: args.timezone
168
+ });
169
+ if (candidate !== void 0 && candidate > args.afterMs) {
170
+ return candidate;
171
+ }
172
+ candidateDate = addDays(candidateDate, interval);
117
173
  }
118
- timestampMs = next;
174
+ return void 0;
119
175
  }
120
- return timestampMs;
176
+ if (args.recurrence.frequency === "weekly") {
177
+ if (normalizeWeekdays(args.recurrence.weekdays).length === 0) {
178
+ return void 0;
179
+ }
180
+ let candidateDate = searchFrom;
181
+ const gregorianCycleDays = 146097;
182
+ const cadenceDays = interval * 7;
183
+ const searchDays = gregorianCycleDays * cadenceDays / greatestCommonDivisor(gregorianCycleDays, cadenceDays);
184
+ for (let attempts = 0; attempts < searchDays; attempts += 1) {
185
+ if (weeklyRecurrenceMatchesDate(candidateDate, start, args.recurrence)) {
186
+ const candidate = buildCandidate({
187
+ date: candidateDate,
188
+ recurrence: args.recurrence,
189
+ timezone: args.timezone
190
+ });
191
+ if (candidate !== void 0 && candidate > args.afterMs) {
192
+ return candidate;
193
+ }
194
+ }
195
+ candidateDate = addDays(candidateDate, 1);
196
+ }
197
+ return void 0;
198
+ }
199
+ if (args.recurrence.frequency === "monthly") {
200
+ const startMonth = start.year * 12 + start.month - 1;
201
+ const searchMonth = searchFrom.year * 12 + searchFrom.month - 1;
202
+ let candidateMonth = startMonth + Math.ceil((searchMonth - startMonth) / interval) * interval;
203
+ const gregorianCycleMonths = 4800;
204
+ const candidateCount2 = gregorianCycleMonths / greatestCommonDivisor(gregorianCycleMonths, interval);
205
+ for (let attempts = 0; attempts < candidateCount2; attempts += 1) {
206
+ const candidateDate = {
207
+ year: Math.floor(candidateMonth / 12),
208
+ month: candidateMonth % 12 + 1,
209
+ day: args.recurrence.dayOfMonth ?? 0
210
+ };
211
+ if (compareDate(candidateDate, searchFrom) >= 0) {
212
+ const candidate = buildCandidate({
213
+ date: candidateDate,
214
+ recurrence: args.recurrence,
215
+ timezone: args.timezone
216
+ });
217
+ if (candidate !== void 0 && candidate > args.afterMs) {
218
+ return candidate;
219
+ }
220
+ }
221
+ candidateMonth += interval;
222
+ }
223
+ return void 0;
224
+ }
225
+ let candidateYear = start.year + Math.ceil((searchFrom.year - start.year) / interval) * interval;
226
+ const candidateCount = 400 / greatestCommonDivisor(400, interval);
227
+ for (let attempts = 0; attempts < candidateCount; attempts += 1) {
228
+ const candidateDate = {
229
+ year: candidateYear,
230
+ month: args.recurrence.month ?? 0,
231
+ day: args.recurrence.dayOfMonth ?? 0
232
+ };
233
+ if (compareDate(candidateDate, searchFrom) >= 0) {
234
+ const candidate = buildCandidate({
235
+ date: candidateDate,
236
+ recurrence: args.recurrence,
237
+ timezone: args.timezone
238
+ });
239
+ if (candidate !== void 0 && candidate > args.afterMs) {
240
+ return candidate;
241
+ }
242
+ }
243
+ candidateYear += interval;
244
+ }
245
+ return void 0;
246
+ }
247
+ function getFirstRunAtMs(args) {
248
+ return findNextRunAtMs({
249
+ ...args,
250
+ searchFrom: getLocalDate(args.afterMs, args.timezone)
251
+ });
121
252
  }
122
253
  function compareDate(left, right) {
123
254
  return Date.UTC(left.year, left.month - 1, left.day) - Date.UTC(right.year, right.month - 1, right.day);
@@ -146,13 +277,6 @@ function parseLocalDate(value) {
146
277
  }
147
278
  return { year, month, day };
148
279
  }
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
280
  function getLocalDate(timestampMs, timezone) {
157
281
  const parts = getZonedDateTimeParts(timestampMs, timezone);
158
282
  return { year: parts.year, month: parts.month, day: parts.day };
@@ -169,168 +293,6 @@ function buildCandidate(args) {
169
293
  timezone: args.timezone
170
294
  });
171
295
  }
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
296
  function getNextRunAtMs(task, scheduledForMs, afterMs = scheduledForMs) {
335
297
  if (task.schedule.kind !== "recurring") {
336
298
  return void 0;
@@ -339,35 +301,14 @@ function getNextRunAtMs(task, scheduledForMs, afterMs = scheduledForMs) {
339
301
  if (!recurrence || !Number.isFinite(recurrence.interval) || recurrence.interval <= 0) {
340
302
  return void 0;
341
303
  }
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({
304
+ const timezone = task.schedule.timezone;
305
+ const afterDate = getLocalDate(afterMs, timezone);
306
+ const nextScheduledDate = addDays(getLocalDate(scheduledForMs, timezone), 1);
307
+ return findNextRunAtMs({
308
+ afterMs,
367
309
  recurrence,
368
- timezone: task.schedule.timezone,
369
- scheduledForMs,
370
- afterMs
310
+ searchFrom: compareDate(afterDate, nextScheduledDate) < 0 ? nextScheduledDate : afterDate,
311
+ timezone
371
312
  });
372
313
  }
373
314
 
@@ -538,11 +479,12 @@ function buildRunId(taskId, scheduledForMs) {
538
479
  function unique(values) {
539
480
  return [...new Set(values.filter(Boolean))];
540
481
  }
482
+ var schedulerTaskIndexSchema = z2.array(z2.string().min(1));
483
+ function parseStringIndex(value) {
484
+ return value === void 0 ? [] : schedulerTaskIndexSchema.parse(value);
485
+ }
541
486
  async function getIndex(state, key) {
542
- const values = await state.get(key) ?? [];
543
- return unique(
544
- values.filter((value) => typeof value === "string")
545
- );
487
+ return unique(parseStringIndex(await state.get(key)));
546
488
  }
547
489
  function isFinishedRun(run) {
548
490
  return run.status === "completed" || run.status === "failed" || run.status === "blocked" || run.status === "skipped";
@@ -798,6 +740,17 @@ var SqlSchedulerStore = class {
798
740
  this.db = db;
799
741
  }
800
742
  db;
743
+ async createTask(task) {
744
+ const next = requireStoredTask(task);
745
+ return await withSqlLock(this.db, taskLockKey(task.id), async (db) => {
746
+ const current = await getTaskFromSql(db, task.id);
747
+ if (current) {
748
+ return current;
749
+ }
750
+ await this.saveTaskRecord(db, next, void 0);
751
+ return next;
752
+ });
753
+ }
801
754
  async saveTask(task) {
802
755
  const next = requireStoredTask(task);
803
756
  await withSqlLock(this.db, taskLockKey(task.id), async (db) => {
@@ -1181,55 +1134,304 @@ function scheduledTaskPrincipalLabel(principal) {
1181
1134
 
1182
1135
  // src/tools/create-task.ts
1183
1136
  import { definePluginTool } from "@sentry/junior-plugin-api";
1184
- import { z as z4 } from "zod";
1137
+ import { z as z5 } from "zod";
1138
+
1139
+ // src/schedule-intent.ts
1140
+ import { z as z3 } from "zod";
1141
+ var localDateSchema = z3.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Use a local date in YYYY-MM-DD format.");
1142
+ var localTimeSchema = z3.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/, "Use local time in HH:MM format.");
1143
+ var timezoneSchema = z3.string().min(1).max(80).describe(
1144
+ "IANA timezone, for example America/Los_Angeles. Omit or use null for the scheduler default."
1145
+ ).nullable().optional();
1146
+ var weekdaySchema = z3.enum([
1147
+ "sunday",
1148
+ "monday",
1149
+ "tuesday",
1150
+ "wednesday",
1151
+ "thursday",
1152
+ "friday",
1153
+ "saturday"
1154
+ ]);
1155
+ var oneOffScheduleIntentSchema = z3.object({
1156
+ kind: z3.literal("one_off").describe("A schedule that runs once."),
1157
+ timezone: timezoneSchema,
1158
+ timing: z3.discriminatedUnion("type", [
1159
+ z3.object({
1160
+ type: z3.literal("after").describe("Run after a relative delay from the server clock."),
1161
+ value: z3.number().int().positive().max(1e5),
1162
+ unit: z3.enum(["second", "minute", "hour", "day", "week"])
1163
+ }).strict(),
1164
+ z3.object({
1165
+ type: z3.literal("at").describe("Run at an explicit local calendar date and time."),
1166
+ date: localDateSchema.describe(
1167
+ "Requested local calendar date in YYYY-MM-DD format."
1168
+ ),
1169
+ time: localTimeSchema.describe(
1170
+ "Requested local wall-clock time in HH:MM format."
1171
+ )
1172
+ }).strict()
1173
+ ]).describe("Relative delay or explicit local time for the one-off run.")
1174
+ }).strict();
1175
+ var recurringScheduleIntentSchema = z3.object({
1176
+ kind: z3.literal("recurring").describe("A repeating calendar schedule."),
1177
+ frequency: z3.enum(["daily", "weekly", "monthly", "yearly"]).describe("Recurring tasks can run at most once per day."),
1178
+ interval: z3.number().int().positive().max(365).describe("Repeat every N frequency units. Defaults to 1.").nullable().optional(),
1179
+ time: localTimeSchema.describe(
1180
+ "Local wall-clock time for each occurrence in HH:MM format."
1181
+ ),
1182
+ weekdays: z3.array(weekdaySchema).min(1).max(7).describe("Required for weekly schedules.").nullable().optional(),
1183
+ day_of_month: z3.number().int().min(1).max(31).describe("Required for monthly and yearly schedules.").nullable().optional(),
1184
+ month: z3.number().int().min(1).max(12).describe("Required for yearly schedules, where January is 1.").nullable().optional(),
1185
+ start_date: localDateSchema.describe(
1186
+ "Optional local calendar date that anchors the recurrence. Omit to start with the next matching occurrence."
1187
+ ).nullable().optional(),
1188
+ timezone: timezoneSchema
1189
+ }).strict().superRefine((schedule, context) => {
1190
+ if (schedule.frequency === "weekly" && schedule.weekdays == null) {
1191
+ context.addIssue({
1192
+ code: "custom",
1193
+ message: "Weekly schedules require weekdays.",
1194
+ path: ["weekdays"]
1195
+ });
1196
+ }
1197
+ if ((schedule.frequency === "monthly" || schedule.frequency === "yearly") && schedule.day_of_month == null) {
1198
+ context.addIssue({
1199
+ code: "custom",
1200
+ message: `${schedule.frequency} schedules require day_of_month.`,
1201
+ path: ["day_of_month"]
1202
+ });
1203
+ }
1204
+ if (schedule.frequency === "yearly" && schedule.month == null) {
1205
+ context.addIssue({
1206
+ code: "custom",
1207
+ message: "Yearly schedules require month.",
1208
+ path: ["month"]
1209
+ });
1210
+ }
1211
+ if (schedule.frequency !== "weekly" && schedule.weekdays != null) {
1212
+ context.addIssue({
1213
+ code: "custom",
1214
+ message: "weekdays applies only to weekly schedules.",
1215
+ path: ["weekdays"]
1216
+ });
1217
+ }
1218
+ if (schedule.frequency !== "monthly" && schedule.frequency !== "yearly" && schedule.day_of_month != null) {
1219
+ context.addIssue({
1220
+ code: "custom",
1221
+ message: "day_of_month applies only to monthly or yearly schedules.",
1222
+ path: ["day_of_month"]
1223
+ });
1224
+ }
1225
+ if (schedule.frequency !== "yearly" && schedule.month != null) {
1226
+ context.addIssue({
1227
+ code: "custom",
1228
+ message: "month applies only to yearly schedules.",
1229
+ path: ["month"]
1230
+ });
1231
+ }
1232
+ });
1233
+ var scheduleIntentSchema = z3.union([
1234
+ oneOffScheduleIntentSchema,
1235
+ recurringScheduleIntentSchema
1236
+ ]);
1237
+ var WEEKDAY_INDEX = {
1238
+ sunday: 0,
1239
+ monday: 1,
1240
+ tuesday: 2,
1241
+ wednesday: 3,
1242
+ thursday: 4,
1243
+ friday: 5,
1244
+ saturday: 6
1245
+ };
1246
+ var DURATION_MS = {
1247
+ second: 1e3,
1248
+ minute: 60 * 1e3,
1249
+ hour: 60 * 60 * 1e3,
1250
+ day: 24 * 60 * 60 * 1e3,
1251
+ week: 7 * 24 * 60 * 60 * 1e3
1252
+ };
1253
+ function parseLocalTime(value) {
1254
+ const [hour, minute] = value.split(":").map(Number);
1255
+ return { hour, minute };
1256
+ }
1257
+ function localDateAt(timestampMs, timezone) {
1258
+ const parts = getZonedDateTimeParts(timestampMs, timezone);
1259
+ return [parts.year, parts.month, parts.day].map(
1260
+ (value, index2) => index2 === 0 ? String(value).padStart(4, "0") : String(value).padStart(2, "0")
1261
+ ).join("-");
1262
+ }
1263
+ function isValidTimeZone(timezone) {
1264
+ try {
1265
+ new Intl.DateTimeFormat("en-US", { timeZone: timezone }).format();
1266
+ return true;
1267
+ } catch {
1268
+ return false;
1269
+ }
1270
+ }
1271
+ function plural(value, unit) {
1272
+ return value === 1 ? unit : `${unit}s`;
1273
+ }
1274
+ function formatWeekdays(weekdays) {
1275
+ const labels = weekdays.map(
1276
+ (weekday) => `${weekday[0].toUpperCase()}${weekday.slice(1)}`
1277
+ );
1278
+ if (labels.length < 2) {
1279
+ return labels[0] ?? "";
1280
+ }
1281
+ return `${labels.slice(0, -1).join(", ")} and ${labels.at(-1)}`;
1282
+ }
1283
+ function recurringDescription(schedule, timezone) {
1284
+ const interval = schedule.interval ?? 1;
1285
+ const cadenceUnit = {
1286
+ daily: "day",
1287
+ weekly: "week",
1288
+ monthly: "month",
1289
+ yearly: "year"
1290
+ }[schedule.frequency];
1291
+ const cadence = interval === 1 ? cadenceUnit : `${interval} ${cadenceUnit}s`;
1292
+ let detail = "";
1293
+ if (schedule.frequency === "weekly") {
1294
+ detail = ` on ${formatWeekdays(schedule.weekdays)}`;
1295
+ } else if (schedule.frequency === "monthly") {
1296
+ detail = ` on day ${schedule.day_of_month}`;
1297
+ } else if (schedule.frequency === "yearly") {
1298
+ detail = ` on ${String(schedule.month).padStart(2, "0")}-${String(
1299
+ schedule.day_of_month
1300
+ ).padStart(2, "0")}`;
1301
+ }
1302
+ return `Every ${cadence}${detail} at ${schedule.time} (${timezone})`;
1303
+ }
1304
+ var ScheduleIntentError = class extends Error {
1305
+ };
1306
+ function compileScheduleIntent(args) {
1307
+ const timezone = args.intent.timezone ?? args.defaultTimezone;
1308
+ if (!isValidTimeZone(timezone)) {
1309
+ throw new ScheduleIntentError("timezone must be a valid IANA time zone.");
1310
+ }
1311
+ if (args.intent.kind === "one_off") {
1312
+ if (args.intent.timing.type === "after") {
1313
+ const nextRunAtMs3 = args.nowMs + args.intent.timing.value * DURATION_MS[args.intent.timing.unit];
1314
+ return {
1315
+ nextRunAtMs: nextRunAtMs3,
1316
+ schedule: {
1317
+ description: `In ${args.intent.timing.value} ${plural(
1318
+ args.intent.timing.value,
1319
+ args.intent.timing.unit
1320
+ )}`,
1321
+ kind: "one_off",
1322
+ timezone
1323
+ }
1324
+ };
1325
+ }
1326
+ const nextRunAtMs2 = resolveLocalScheduleAtMs({
1327
+ date: args.intent.timing.date,
1328
+ time: parseLocalTime(args.intent.timing.time),
1329
+ timezone
1330
+ });
1331
+ if (nextRunAtMs2 === void 0) {
1332
+ throw new ScheduleIntentError(
1333
+ "The requested local date or time does not exist in that timezone."
1334
+ );
1335
+ }
1336
+ if (nextRunAtMs2 <= args.nowMs) {
1337
+ throw new ScheduleIntentError(
1338
+ "The requested one-off schedule must be in the future."
1339
+ );
1340
+ }
1341
+ return {
1342
+ nextRunAtMs: nextRunAtMs2,
1343
+ schedule: {
1344
+ description: `Once on ${args.intent.timing.date} at ${args.intent.timing.time} (${timezone})`,
1345
+ kind: "one_off",
1346
+ timezone
1347
+ }
1348
+ };
1349
+ }
1350
+ const recurrence = {
1351
+ frequency: args.intent.frequency,
1352
+ interval: args.intent.interval ?? 1,
1353
+ startDate: args.intent.start_date ?? localDateAt(args.nowMs, timezone),
1354
+ time: parseLocalTime(args.intent.time),
1355
+ ...args.intent.frequency === "weekly" ? {
1356
+ weekdays: [
1357
+ ...new Set(
1358
+ args.intent.weekdays.map((weekday) => WEEKDAY_INDEX[weekday])
1359
+ )
1360
+ ].sort((left, right) => left - right)
1361
+ } : {},
1362
+ ...args.intent.frequency === "monthly" || args.intent.frequency === "yearly" ? { dayOfMonth: args.intent.day_of_month ?? void 0 } : {},
1363
+ ...args.intent.frequency === "yearly" ? { month: args.intent.month ?? void 0 } : {}
1364
+ };
1365
+ const searchRecurrence = args.intent.start_date ? recurrence : { ...recurrence, interval: 1 };
1366
+ const nextRunAtMs = getFirstRunAtMs({
1367
+ afterMs: args.nowMs,
1368
+ recurrence: searchRecurrence,
1369
+ timezone
1370
+ });
1371
+ if (nextRunAtMs === void 0) {
1372
+ throw new ScheduleIntentError(
1373
+ "The recurring schedule has no valid future occurrence."
1374
+ );
1375
+ }
1376
+ const materializedRecurrence = args.intent.start_date ? recurrence : { ...recurrence, startDate: localDateAt(nextRunAtMs, timezone) };
1377
+ return {
1378
+ nextRunAtMs,
1379
+ schedule: {
1380
+ description: recurringDescription(args.intent, timezone),
1381
+ kind: "recurring",
1382
+ recurrence: materializedRecurrence,
1383
+ timezone
1384
+ }
1385
+ };
1386
+ }
1185
1387
 
1186
1388
  // src/tool-support.ts
1187
- import { randomUUID } from "crypto";
1389
+ import { createHash } from "crypto";
1188
1390
  import {
1189
1391
  PluginToolInputError,
1190
1392
  pluginToolResultSchema,
1191
1393
  sourceSchema
1192
1394
  } from "@sentry/junior-plugin-api";
1193
- import { z as z3 } from "zod";
1395
+ import { z as z4 } from "zod";
1194
1396
  var TASK_ID_PREFIX = "sched";
1195
1397
  var MAX_LISTED_TASKS = 50;
1196
1398
  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()
1399
+ var compactTaskResultSchema = z4.object({
1400
+ id: z4.string(),
1401
+ status: z4.enum(["active", "paused", "blocked", "deleted"]),
1402
+ task: z4.string(),
1403
+ schedule: z4.string(),
1404
+ timezone: z4.string(),
1405
+ recurrence: z4.unknown().nullable(),
1406
+ next_run_at: z4.string().nullable(),
1407
+ conversation_access: z4.unknown().nullable(),
1408
+ credential_mode: z4.enum(["system", "creator"]),
1409
+ last_run_at: z4.string().nullable(),
1410
+ run_now_at: z4.string().nullable()
1209
1411
  }).strict();
1210
- var scheduleTaskResultDataSchema = z3.object({
1211
- ok: z3.literal(true),
1412
+ var scheduleTaskResultDataSchema = z4.object({
1413
+ ok: z4.literal(true),
1212
1414
  task: compactTaskResultSchema
1213
1415
  }).strict();
1214
1416
  var scheduleTaskToolResultSchema = pluginToolResultSchema.extend({
1215
- ok: z3.literal(true),
1216
- status: z3.literal("success"),
1217
- target: z3.string(),
1417
+ ok: z4.literal(true),
1418
+ status: z4.literal("success"),
1419
+ target: z4.string(),
1218
1420
  data: scheduleTaskResultDataSchema,
1219
1421
  task: compactTaskResultSchema
1220
1422
  });
1221
- var scheduleListResultDataSchema = z3.object({
1222
- ok: z3.literal(true),
1223
- tasks: z3.array(compactTaskResultSchema),
1224
- truncated: z3.boolean()
1423
+ var scheduleListResultDataSchema = z4.object({
1424
+ ok: z4.literal(true),
1425
+ tasks: z4.array(compactTaskResultSchema),
1426
+ truncated: z4.boolean()
1225
1427
  }).strict();
1226
1428
  var scheduleListToolResultSchema = pluginToolResultSchema.extend({
1227
- ok: z3.literal(true),
1228
- status: z3.literal("success"),
1229
- target: z3.string(),
1429
+ ok: z4.literal(true),
1430
+ status: z4.literal("success"),
1431
+ target: z4.string(),
1230
1432
  data: scheduleListResultDataSchema,
1231
- tasks: z3.array(compactTaskResultSchema),
1232
- truncated: z3.boolean()
1433
+ tasks: z4.array(compactTaskResultSchema),
1434
+ truncated: z4.boolean()
1233
1435
  });
1234
1436
  function throwToolInputError(error) {
1235
1437
  throw new PluginToolInputError(error);
@@ -1363,8 +1565,21 @@ function scheduleListToolResult(args) {
1363
1565
  truncated: args.truncated
1364
1566
  };
1365
1567
  }
1366
- function buildTaskId() {
1367
- return `${TASK_ID_PREFIX}_${randomUUID()}`;
1568
+ function buildTaskId(args) {
1569
+ const toolCallId = args.toolCallId?.trim();
1570
+ if (!toolCallId) {
1571
+ throw new Error("Scheduler task creation requires a tool-call identity.");
1572
+ }
1573
+ const digest = createHash("sha256").update(
1574
+ JSON.stringify({
1575
+ actor: args.actor.slackUserId,
1576
+ channel: args.destination.channelId,
1577
+ operation: toolCallId,
1578
+ platform: args.destination.platform,
1579
+ team: args.destination.teamId
1580
+ })
1581
+ ).digest("hex").slice(0, 32);
1582
+ return `${TASK_ID_PREFIX}_${digest}`;
1368
1583
  }
1369
1584
  function schedulerStore(context) {
1370
1585
  return context.store;
@@ -1375,133 +1590,64 @@ function normalizeStatus(value) {
1375
1590
  }
1376
1591
  return void 0;
1377
1592
  }
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
1593
  function getDefaultScheduleTimezone() {
1434
1594
  return process.env.JUNIOR_TIMEZONE?.trim() || DEFAULT_SCHEDULE_TIMEZONE;
1435
1595
  }
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
1596
 
1455
1597
  // src/tools/create-task.ts
1456
1598
  function createSlackScheduleCreateTaskTool(context) {
1457
1599
  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.",
1600
+ 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
1601
  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."
1602
+ inputSchema: z5.object({
1603
+ task: z5.string().min(1).max(4e3),
1604
+ schedule: scheduleIntentSchema.describe(
1605
+ "When the task runs. The scheduler computes the exact next run from this intent and the server clock."
1465
1606
  ),
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(
1607
+ credential_mode: z5.enum(["system", "creator"]).nullable().describe(
1476
1608
  "Use creator only when the current user explicitly authorizes future scheduled use of their connected credentials. Omit or use system otherwise."
1477
1609
  ).optional()
1478
- }),
1610
+ }).strict(),
1479
1611
  outputSchema: scheduleTaskToolResultSchema,
1480
- execute: async (input) => {
1612
+ execute: async (input, options) => {
1481
1613
  const destination = requireActiveConversation(context);
1482
1614
  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.");
1615
+ const store = schedulerStore(context);
1616
+ const id = buildTaskId({
1617
+ actor,
1618
+ destination,
1619
+ toolCallId: options.toolCallId
1620
+ });
1621
+ const existing = await store.getTask(id);
1622
+ if (existing) {
1623
+ if (!sameDestination(existing, destination) || existing.createdBy.slackUserId !== actor.slackUserId) {
1624
+ throwToolInputError("Scheduled task operation identity is invalid.");
1625
+ }
1626
+ return scheduleTaskToolResult(
1627
+ "slackScheduleCreateTask",
1628
+ compactTask(existing)
1629
+ );
1489
1630
  }
1490
- const nextRunAtMs = parseNextRunAtMs(input.next_run_at);
1491
- if (!nextRunAtMs) {
1492
- throwToolInputError("Provide next_run_at as a valid ISO timestamp.");
1631
+ const nowMs = context.now?.() ?? Date.now();
1632
+ let compiled;
1633
+ try {
1634
+ compiled = compileScheduleIntent({
1635
+ defaultTimezone: getDefaultScheduleTimezone(),
1636
+ intent: input.schedule,
1637
+ nowMs
1638
+ });
1639
+ } catch (error) {
1640
+ if (error instanceof ScheduleIntentError) {
1641
+ throwToolInputError(error.message);
1642
+ }
1643
+ throw error;
1493
1644
  }
1494
- const recurrence = buildRecurrence({
1495
- input,
1496
- nextRunAtMs,
1497
- timezone
1498
- });
1499
1645
  const conversationAccess = getConversationAccess(
1500
1646
  destination,
1501
1647
  context.source
1502
1648
  );
1503
1649
  const task = {
1504
- id: buildTaskId(),
1650
+ id,
1505
1651
  createdAtMs: nowMs,
1506
1652
  updatedAtMs: nowMs,
1507
1653
  createdBy: actor,
@@ -1509,23 +1655,21 @@ function createSlackScheduleCreateTaskTool(context) {
1509
1655
  credentialMode: input.credential_mode ?? "system",
1510
1656
  destination,
1511
1657
  executionActor: SCHEDULED_TASK_SYSTEM_ACTOR,
1512
- nextRunAtMs,
1658
+ nextRunAtMs: compiled.nextRunAtMs,
1513
1659
  originalRequest: context.userText,
1514
- schedule: {
1515
- description: input.schedule,
1516
- timezone,
1517
- kind: recurrence ? "recurring" : "one_off",
1518
- recurrence
1519
- },
1660
+ schedule: compiled.schedule,
1520
1661
  status: "active",
1521
1662
  task: {
1522
1663
  text: input.task
1523
1664
  }
1524
1665
  };
1525
- await schedulerStore(context).saveTask(task);
1666
+ const committed = await store.createTask(task);
1667
+ if (!sameDestination(committed, destination) || committed.createdBy.slackUserId !== actor.slackUserId) {
1668
+ throwToolInputError("Scheduled task operation identity is invalid.");
1669
+ }
1526
1670
  return scheduleTaskToolResult(
1527
1671
  "slackScheduleCreateTask",
1528
- compactTask(task)
1672
+ compactTask(committed)
1529
1673
  );
1530
1674
  }
1531
1675
  });
@@ -1533,13 +1677,13 @@ function createSlackScheduleCreateTaskTool(context) {
1533
1677
 
1534
1678
  // src/tools/delete-task.ts
1535
1679
  import { definePluginTool as definePluginTool2 } from "@sentry/junior-plugin-api";
1536
- import { z as z5 } from "zod";
1680
+ import { z as z6 } from "zod";
1537
1681
  function createSlackScheduleDeleteTaskTool(context) {
1538
1682
  return definePluginTool2({
1539
1683
  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
1684
  executionMode: "sequential",
1541
- inputSchema: z5.object({
1542
- task_id: z5.string().min(1).describe(
1685
+ inputSchema: z6.object({
1686
+ task_id: z6.string().min(1).describe(
1543
1687
  "ID of the task to delete. Must be from this active Slack conversation."
1544
1688
  )
1545
1689
  }),
@@ -1564,12 +1708,12 @@ function createSlackScheduleDeleteTaskTool(context) {
1564
1708
 
1565
1709
  // src/tools/list-tasks.ts
1566
1710
  import { definePluginTool as definePluginTool3 } from "@sentry/junior-plugin-api";
1567
- import { z as z6 } from "zod";
1711
+ import { z as z7 } from "zod";
1568
1712
  function createSlackScheduleListTasksTool(context) {
1569
1713
  return definePluginTool3({
1570
1714
  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
1715
  annotations: { readOnlyHint: true, destructiveHint: false },
1572
- inputSchema: z6.object({}),
1716
+ inputSchema: z7.object({}),
1573
1717
  outputSchema: scheduleListToolResultSchema,
1574
1718
  execute: async () => {
1575
1719
  const destination = requireActiveConversation(context);
@@ -1591,13 +1735,13 @@ function createSlackScheduleListTasksTool(context) {
1591
1735
 
1592
1736
  // src/tools/run-task-now.ts
1593
1737
  import { definePluginTool as definePluginTool4 } from "@sentry/junior-plugin-api";
1594
- import { z as z7 } from "zod";
1738
+ import { z as z8 } from "zod";
1595
1739
  function createSlackScheduleRunTaskNowTool(context) {
1596
1740
  return definePluginTool4({
1597
1741
  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
1742
  executionMode: "sequential",
1599
- inputSchema: z7.object({
1600
- task_id: z7.string().min(1).describe(
1743
+ inputSchema: z8.object({
1744
+ task_id: z8.string().min(1).describe(
1601
1745
  "ID of the active task to run now. Must be from this active Slack conversation."
1602
1746
  )
1603
1747
  }),
@@ -1626,29 +1770,26 @@ function createSlackScheduleRunTaskNowTool(context) {
1626
1770
 
1627
1771
  // src/tools/update-task.ts
1628
1772
  import { definePluginTool as definePluginTool5 } from "@sentry/junior-plugin-api";
1629
- import { z as z8 } from "zod";
1773
+ import { z as z9 } from "zod";
1630
1774
  function createSlackScheduleUpdateTaskTool(context) {
1631
1775
  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.",
1776
+ description: "Edit, pause, resume, reschedule, or change credential use for an existing Junior scheduled task in the active Slack conversation.",
1633
1777
  executionMode: "sequential",
1634
- inputSchema: z8.object({
1635
- task_id: z8.string().min(1).describe(
1778
+ inputSchema: z9.object({
1779
+ task_id: z9.string().min(1).describe(
1636
1780
  "ID of the task to update. Must be from this active Slack conversation."
1637
1781
  ),
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(
1782
+ task: z9.string().min(1).max(4e3).optional(),
1783
+ schedule: scheduleIntentSchema.describe(
1784
+ "Complete replacement schedule when rescheduling. Omit for task, status, or credential-only changes; the scheduler computes the next run."
1785
+ ).nullable().optional(),
1786
+ status: z9.enum(["active", "paused", "blocked"]).describe(
1646
1787
  "Set to active, paused, or blocked to resume, pause, or block the task."
1647
1788
  ).optional(),
1648
- credential_mode: z8.enum(["system", "creator"]).nullable().describe(
1789
+ credential_mode: z9.enum(["system", "creator"]).nullable().describe(
1649
1790
  "Set creator only when the current actor is the task creator and explicitly authorizes future scheduled credential use. Set system to disable delegation."
1650
1791
  ).optional()
1651
- }),
1792
+ }).strict(),
1652
1793
  outputSchema: scheduleTaskToolResultSchema,
1653
1794
  execute: async (input) => {
1654
1795
  const lookup = await getWritableTask({
@@ -1662,48 +1803,43 @@ function createSlackScheduleUpdateTaskTool(context) {
1662
1803
  "Only the scheduled task creator can enable creator credential use."
1663
1804
  );
1664
1805
  }
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.");
1806
+ const nowMs = context.now?.() ?? Date.now();
1807
+ let compiled;
1808
+ if (input.schedule) {
1809
+ try {
1810
+ compiled = compileScheduleIntent({
1811
+ defaultTimezone: lookup.schedule.timezone || getDefaultScheduleTimezone(),
1812
+ intent: input.schedule,
1813
+ nowMs
1814
+ });
1815
+ } catch (error) {
1816
+ if (error instanceof ScheduleIntentError) {
1817
+ throwToolInputError(error.message);
1818
+ }
1819
+ throw error;
1820
+ }
1674
1821
  }
1822
+ const nextRunAtMs = compiled?.nextRunAtMs ?? lookup.nextRunAtMs;
1675
1823
  const status = normalizeStatus(input.status);
1676
1824
  if (input.status && !status) {
1677
1825
  throwToolInputError("status must be active, paused, or blocked.");
1678
1826
  }
1679
1827
  if (status === "active" && !nextRunAtMs) {
1680
1828
  throwToolInputError(
1681
- "Active scheduled tasks require next_run_at when no next run is stored."
1829
+ "Active scheduled tasks require a schedule with a future occurrence."
1682
1830
  );
1683
1831
  }
1684
- const recurrence = shouldRebuildRecurrence(input) ? buildRecurrence({
1685
- existing: lookup.schedule.recurrence,
1686
- input,
1687
- nextRunAtMs,
1688
- timezone
1689
- }) : lookup.schedule.recurrence;
1690
1832
  const nextStatus = status ?? lookup.status;
1691
1833
  const credentialMode = input.task !== void 0 && input.task !== lookup.task.text && !isCreator ? "system" : input.credential_mode ?? lookup.credentialMode;
1692
1834
  const next = {
1693
1835
  ...lookup,
1694
1836
  credentialMode,
1695
- updatedAtMs: Date.now(),
1837
+ updatedAtMs: nowMs,
1696
1838
  nextRunAtMs,
1697
- runNowAtMs: nextStatus === "active" ? lookup.runNowAtMs : void 0,
1839
+ runNowAtMs: nextStatus === "active" && !compiled ? lookup.runNowAtMs : void 0,
1698
1840
  status: nextStatus,
1699
1841
  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
- },
1842
+ schedule: compiled?.schedule ?? lookup.schedule,
1707
1843
  task: input.task ? { text: input.task } : lookup.task
1708
1844
  };
1709
1845
  await schedulerStore(context).saveTask(next);