@sentry/junior-scheduler 0.68.0 → 0.70.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.d.ts CHANGED
@@ -2,4 +2,4 @@ export { createSchedulerPlugin, schedulerPlugin } from "./plugin";
2
2
  export { buildScheduledTaskRunPrompt } from "./prompt";
3
3
  export { createSlackScheduleCreateTaskTool, createSlackScheduleDeleteTaskTool, createSlackScheduleListTasksTool, createSlackScheduleRunTaskNowTool, createSlackScheduleUpdateTaskTool, type SchedulerToolContext, } from "./schedule-tools";
4
4
  export { createSchedulerStore } from "./store";
5
- export type { ScheduledCalendarFrequency, ScheduledLocalTime, ScheduledRun, ScheduledRunStatus, ScheduledTask, ScheduledTaskConversationAccess, ScheduledTaskCredentialSubject, ScheduledTaskDestination, ScheduledTaskExecutionActor, ScheduledTaskPrincipal, ScheduledTaskRecurrence, ScheduledTaskSchedule, ScheduledTaskSpec, ScheduledTaskStatus, } from "./types";
5
+ export type { ScheduledCalendarFrequency, ScheduledLocalTime, ScheduledRun, ScheduledRunStatus, ScheduledTask, ScheduledTaskConversationAccess, ScheduledTaskExecutionActor, ScheduledTaskPrincipal, ScheduledTaskRecurrence, ScheduledTaskSchedule, ScheduledTaskSpec, ScheduledTaskStatus, } from "./types";
package/dist/index.js CHANGED
@@ -122,6 +122,12 @@ function buildScheduledTaskRunPrompt(args) {
122
122
  ].join("\n");
123
123
  }
124
124
 
125
+ // src/store.ts
126
+ import {
127
+ agentPluginCredentialSubjectSchema,
128
+ destinationSchema
129
+ } from "@sentry/junior-plugin-api";
130
+
125
131
  // src/cadence.ts
126
132
  function parseScheduleTimestamp(value) {
127
133
  const trimmed = value.trim();
@@ -650,8 +656,34 @@ function canFinishRun(run, startedAtMs) {
650
656
  }
651
657
  return run.status === "running" && run.startedAtMs === startedAtMs;
652
658
  }
659
+ function parseStoredTask(value) {
660
+ if (!value || typeof value !== "object") {
661
+ return void 0;
662
+ }
663
+ const record = value;
664
+ const destination = destinationSchema.safeParse(record.destination);
665
+ if (!destination.success) {
666
+ return void 0;
667
+ }
668
+ const credentialSubject = record.credentialSubject === void 0 ? void 0 : agentPluginCredentialSubjectSchema.safeParse(record.credentialSubject);
669
+ if (credentialSubject && !credentialSubject.success) {
670
+ return void 0;
671
+ }
672
+ return {
673
+ ...record,
674
+ destination: destination.data,
675
+ ...credentialSubject ? { credentialSubject: credentialSubject.data } : {}
676
+ };
677
+ }
678
+ function requireStoredTask(task) {
679
+ const parsed = parseStoredTask(task);
680
+ if (!parsed) {
681
+ throw new Error("Scheduled task routing context is invalid.");
682
+ }
683
+ return parsed;
684
+ }
653
685
  async function getTaskFromState(state, taskId) {
654
- return await state.get(taskKey(taskId)) ?? void 0;
686
+ return parseStoredTask(await state.get(taskKey(taskId)));
655
687
  }
656
688
  async function listTasksFromState(state, indexKey) {
657
689
  const ids = await getIndex(state, indexKey);
@@ -693,9 +725,10 @@ var PluginStateSchedulerStore = class {
693
725
  this.state = state;
694
726
  }
695
727
  async saveTask(task) {
728
+ const next = requireStoredTask(task);
696
729
  await withLock(this.state, taskLockKey(task.id), async () => {
697
- const current = await this.state.get(taskKey(task.id)) ?? void 0;
698
- await this.saveTaskRecord(task, current);
730
+ const current = await getTaskFromState(this.state, task.id);
731
+ await this.saveTaskRecord(next, current);
699
732
  });
700
733
  }
701
734
  async saveTaskRecord(task, current) {
@@ -804,7 +837,7 @@ var PluginStateSchedulerStore = class {
804
837
  }
805
838
  async skipMissedRun(args) {
806
839
  await withLock(this.state, taskLockKey(args.task.id), async () => {
807
- const current = await this.state.get(taskKey(args.task.id)) ?? void 0;
840
+ const current = await getTaskFromState(this.state, args.task.id) ?? void 0;
808
841
  if (!current || current.status !== "active" || getDueRunAtMs(current, args.nowMs) !== args.scheduledForMs) {
809
842
  return;
810
843
  }
@@ -933,7 +966,7 @@ var PluginStateSchedulerStore = class {
933
966
  }
934
967
  async updateTaskAfterRun(args) {
935
968
  await withLock(this.state, taskLockKey(args.run.taskId), async () => {
936
- const current = await this.state.get(taskKey(args.run.taskId)) ?? void 0;
969
+ const current = await getTaskFromState(this.state, args.run.taskId) ?? void 0;
937
970
  if (!current || current.status === "deleted") {
938
971
  return;
939
972
  }
@@ -1015,7 +1048,9 @@ function createSchedulerOperationalStore(state) {
1015
1048
  import { randomUUID } from "crypto";
1016
1049
  import { Type } from "@sinclair/typebox";
1017
1050
  import {
1018
- AgentPluginToolInputError
1051
+ AgentPluginToolInputError,
1052
+ agentPluginCredentialSubjectSchema as agentPluginCredentialSubjectSchema2,
1053
+ destinationSchema as destinationSchema2
1019
1054
  } from "@sentry/junior-plugin-api";
1020
1055
  var TASK_ID_PREFIX = "sched";
1021
1056
  var MAX_LISTED_TASKS = 50;
@@ -1024,21 +1059,27 @@ function throwToolInputError(error) {
1024
1059
  throw new AgentPluginToolInputError(error);
1025
1060
  }
1026
1061
  function requireActiveDestination(context) {
1027
- const channelId = normalizeSlackConversationId(context.channelId);
1028
- if (!channelId) {
1029
- throwToolInputError("No active Slack channel context is available.");
1030
- }
1031
- if (!context.teamId) {
1032
- throwToolInputError("No active Slack workspace context is available.");
1033
- }
1034
- if (!isSlackTeamId(context.teamId)) {
1035
- throwToolInputError("Active Slack workspace context is invalid.");
1062
+ const parsed = destinationSchema2.safeParse(context.destination);
1063
+ if (!parsed.success) {
1064
+ const destination = context.destination;
1065
+ const issues = parsed.error.issues;
1066
+ if (!destination || destination.platform !== "slack") {
1067
+ throwToolInputError("No active Slack destination is available.");
1068
+ }
1069
+ if (issues.some((issue) => issue.code === "unrecognized_keys")) {
1070
+ throwToolInputError(
1071
+ "Active Slack destination must not include unknown fields."
1072
+ );
1073
+ }
1074
+ if (issues.some((issue) => issue.path[0] === "channelId")) {
1075
+ throwToolInputError("Active Slack destination channel is invalid.");
1076
+ }
1077
+ if (issues.some((issue) => issue.path[0] === "teamId")) {
1078
+ throwToolInputError("Active Slack destination workspace is invalid.");
1079
+ }
1080
+ throwToolInputError("No active Slack destination is available.");
1036
1081
  }
1037
- return {
1038
- platform: "slack",
1039
- teamId: context.teamId,
1040
- channelId
1041
- };
1082
+ return parsed.data;
1042
1083
  }
1043
1084
  function requireRequester(context) {
1044
1085
  const userId = context.requester?.userId?.trim();
@@ -1054,19 +1095,8 @@ function requireRequester(context) {
1054
1095
  function tool(definition) {
1055
1096
  return definition;
1056
1097
  }
1057
- function normalizeSlackConversationId(value) {
1058
- if (!value) return void 0;
1059
- const trimmed = value.trim();
1060
- if (!trimmed) return void 0;
1061
- if (!trimmed.startsWith("slack:")) return trimmed;
1062
- const parts = trimmed.split(":");
1063
- return parts[1]?.trim() || void 0;
1064
- }
1065
1098
  function isDmChannel(channelId) {
1066
- return normalizeSlackConversationId(channelId)?.startsWith("D") ?? false;
1067
- }
1068
- function isSlackTeamId(value) {
1069
- return /^T[A-Z0-9]+$/.test(value);
1099
+ return channelId.startsWith("D");
1070
1100
  }
1071
1101
  function getConversationAccess(destination) {
1072
1102
  if (isDmChannel(destination.channelId)) {
@@ -1087,14 +1117,19 @@ function getCredentialSubject(args) {
1087
1117
  if (!args.subject) {
1088
1118
  return void 0;
1089
1119
  }
1120
+ const subject = agentPluginCredentialSubjectSchema2.safeParse(args.subject);
1121
+ if (!subject.success) {
1122
+ throwToolInputError("Active Slack credential subject is invalid.");
1123
+ }
1090
1124
  return {
1091
- type: args.subject.type,
1092
- userId: args.subject.userId,
1093
- allowedWhen: args.subject.allowedWhen
1125
+ type: subject.data.type,
1126
+ userId: subject.data.userId,
1127
+ allowedWhen: subject.data.allowedWhen
1094
1128
  };
1095
1129
  }
1096
1130
  function sameDestination(task, destination) {
1097
- return task.destination.platform === destination.platform && task.destination.teamId === destination.teamId && task.destination.channelId === destination.channelId;
1131
+ const taskDestination = task.destination;
1132
+ return taskDestination.platform === "slack" && taskDestination.teamId === destination.teamId && taskDestination.channelId === destination.channelId;
1098
1133
  }
1099
1134
  async function getWritableTask(args) {
1100
1135
  const destination = requireActiveDestination(args.context);
@@ -1500,18 +1535,10 @@ function shouldSkipRun(task, run) {
1500
1535
  }
1501
1536
  function createSchedulerToolContext(ctx) {
1502
1537
  return {
1503
- channelCapabilities: ctx.channelCapabilities ?? {
1504
- canAddReactions: false,
1505
- canCreateCanvas: false,
1506
- canPostToChannel: false
1507
- },
1508
- channelId: ctx.channelId,
1509
1538
  credentialSubject: ctx.credentialSubject,
1510
- messageTs: ctx.messageTs,
1539
+ destination: ctx.destination?.platform === "slack" ? ctx.destination : void 0,
1511
1540
  requester: ctx.requester,
1512
1541
  state: ctx.state,
1513
- teamId: ctx.teamId,
1514
- threadTs: ctx.threadTs,
1515
1542
  userText: ctx.userText
1516
1543
  };
1517
1544
  }
@@ -1763,7 +1790,7 @@ function createSchedulerPlugin() {
1763
1790
  legacyStatePrefixes: ["junior:scheduler"],
1764
1791
  hooks: {
1765
1792
  tools(ctx) {
1766
- if (!ctx.channelId || !ctx.teamId || !ctx.requester?.userId) {
1793
+ if (!ctx.destination || ctx.destination.platform !== "slack" || !ctx.requester?.userId) {
1767
1794
  return {};
1768
1795
  }
1769
1796
  const context = createSchedulerToolContext(ctx);
@@ -1,18 +1,9 @@
1
- import { type AgentPluginRequester, type AgentPluginState, type AgentPluginToolDefinition } from "@sentry/junior-plugin-api";
2
- import type { ScheduledTaskCredentialSubject } from "./types";
1
+ import { type AgentPluginCredentialSubject, type Destination, type AgentPluginRequester, type AgentPluginState, type AgentPluginToolDefinition } from "@sentry/junior-plugin-api";
3
2
  export interface SchedulerToolContext {
4
- channelCapabilities: {
5
- canAddReactions: boolean;
6
- canCreateCanvas: boolean;
7
- canPostToChannel: boolean;
8
- };
9
- channelId?: string;
10
- credentialSubject?: ScheduledTaskCredentialSubject;
11
- messageTs?: string;
3
+ credentialSubject?: AgentPluginCredentialSubject;
4
+ destination?: Destination;
12
5
  requester?: AgentPluginRequester;
13
6
  state: AgentPluginState;
14
- teamId?: string;
15
- threadTs?: string;
16
7
  userText?: string;
17
8
  }
18
9
  /** Create a tool that stores a scheduled task for the active Slack context. */
package/dist/store.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AgentPluginReadState, AgentPluginState } from "@sentry/junior-plugin-api";
1
+ import { type AgentPluginReadState, type AgentPluginState } from "@sentry/junior-plugin-api";
2
2
  import type { ScheduledRun, ScheduledTask } from "./types";
3
3
  export interface SchedulerStore {
4
4
  claimDueRun(args: {
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AgentPluginCredentialSubject } from "@sentry/junior-plugin-api";
1
+ import type { AgentPluginCredentialSubject, Destination } from "@sentry/junior-plugin-api";
2
2
  export type ScheduledTaskStatus = "active" | "paused" | "blocked" | "deleted";
3
3
  export type ScheduledRunStatus = "pending" | "running" | "completed" | "failed" | "blocked" | "skipped";
4
4
  export interface ScheduledTaskPrincipal {
@@ -14,16 +14,10 @@ export declare const SCHEDULED_TASK_SYSTEM_ACTOR: Readonly<{
14
14
  type: "system";
15
15
  id: string;
16
16
  }>;
17
- export interface ScheduledTaskDestination {
18
- platform: "slack";
19
- teamId: string;
20
- channelId: string;
21
- }
22
17
  export interface ScheduledTaskConversationAccess {
23
18
  audience: "direct" | "group" | "channel";
24
19
  visibility: "private" | "public" | "unknown";
25
20
  }
26
- export type ScheduledTaskCredentialSubject = AgentPluginCredentialSubject;
27
21
  export type ScheduledCalendarFrequency = "daily" | "weekly" | "monthly" | "yearly";
28
22
  export interface ScheduledLocalTime {
29
23
  hour: number;
@@ -52,8 +46,8 @@ export interface ScheduledTask {
52
46
  createdAtMs: number;
53
47
  createdBy: ScheduledTaskPrincipal;
54
48
  conversationAccess?: ScheduledTaskConversationAccess;
55
- credentialSubject?: ScheduledTaskCredentialSubject;
56
- destination: ScheduledTaskDestination;
49
+ credentialSubject?: AgentPluginCredentialSubject;
50
+ destination: Destination;
57
51
  executionActor?: ScheduledTaskExecutionActor;
58
52
  lastRunAtMs?: number;
59
53
  nextRunAtMs?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-scheduler",
3
- "version": "0.68.0",
3
+ "version": "0.70.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -23,7 +23,7 @@
23
23
  ],
24
24
  "dependencies": {
25
25
  "@sinclair/typebox": "^0.34.49",
26
- "@sentry/junior-plugin-api": "0.68.0"
26
+ "@sentry/junior-plugin-api": "0.70.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^25.9.1",
package/src/index.ts CHANGED
@@ -16,8 +16,6 @@ export type {
16
16
  ScheduledRunStatus,
17
17
  ScheduledTask,
18
18
  ScheduledTaskConversationAccess,
19
- ScheduledTaskCredentialSubject,
20
- ScheduledTaskDestination,
21
19
  ScheduledTaskExecutionActor,
22
20
  ScheduledTaskPrincipal,
23
21
  ScheduledTaskRecurrence,
package/src/plugin.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  defineJuniorPlugin,
3
+ type Destination,
3
4
  type Dispatch,
4
5
  type AgentPluginToolDefinition,
5
6
  type PluginOperationalReportContent,
@@ -53,18 +54,11 @@ function createSchedulerToolContext(
53
54
  ctx: ToolRegistrationHookContext,
54
55
  ): SchedulerToolContext {
55
56
  return {
56
- channelCapabilities: ctx.channelCapabilities ?? {
57
- canAddReactions: false,
58
- canCreateCanvas: false,
59
- canPostToChannel: false,
60
- },
61
- channelId: ctx.channelId,
62
57
  credentialSubject: ctx.credentialSubject,
63
- messageTs: ctx.messageTs,
58
+ destination:
59
+ ctx.destination?.platform === "slack" ? ctx.destination : undefined,
64
60
  requester: ctx.requester,
65
61
  state: ctx.state,
66
- teamId: ctx.teamId,
67
- threadTs: ctx.threadTs,
68
62
  userText: ctx.userText,
69
63
  };
70
64
  }
@@ -189,7 +183,7 @@ function formatTimestamp(timestampMs: number | undefined): string {
189
183
  : "none";
190
184
  }
191
185
 
192
- function destinationLabel(destination: ScheduledTask["destination"]): string {
186
+ function destinationLabel(destination: Destination): string {
193
187
  if (destination.channelId.startsWith("D")) {
194
188
  return "Direct Message";
195
189
  }
@@ -369,7 +363,11 @@ export function createSchedulerPlugin() {
369
363
  legacyStatePrefixes: ["junior:scheduler"],
370
364
  hooks: {
371
365
  tools(ctx) {
372
- if (!ctx.channelId || !ctx.teamId || !ctx.requester?.userId) {
366
+ if (
367
+ !ctx.destination ||
368
+ ctx.destination.platform !== "slack" ||
369
+ !ctx.requester?.userId
370
+ ) {
373
371
  return {} as Record<string, AgentPluginToolDefinition<any>>;
374
372
  }
375
373
  const context = createSchedulerToolContext(ctx);
@@ -2,6 +2,10 @@ import { randomUUID } from "node:crypto";
2
2
  import { Type } from "@sinclair/typebox";
3
3
  import {
4
4
  AgentPluginToolInputError,
5
+ agentPluginCredentialSubjectSchema,
6
+ destinationSchema,
7
+ type AgentPluginCredentialSubject,
8
+ type Destination,
5
9
  type AgentPluginRequester,
6
10
  type AgentPluginState,
7
11
  type AgentPluginToolDefinition,
@@ -14,26 +18,16 @@ import type {
14
18
  ScheduledCalendarFrequency,
15
19
  ScheduledTask,
16
20
  ScheduledTaskConversationAccess,
17
- ScheduledTaskCredentialSubject,
18
- ScheduledTaskDestination,
19
21
  ScheduledTaskPrincipal,
20
22
  ScheduledTaskRecurrence,
21
23
  ScheduledTaskStatus,
22
24
  } from "./types";
23
25
 
24
26
  export interface SchedulerToolContext {
25
- channelCapabilities: {
26
- canAddReactions: boolean;
27
- canCreateCanvas: boolean;
28
- canPostToChannel: boolean;
29
- };
30
- channelId?: string;
31
- credentialSubject?: ScheduledTaskCredentialSubject;
32
- messageTs?: string;
27
+ credentialSubject?: AgentPluginCredentialSubject;
28
+ destination?: Destination;
33
29
  requester?: AgentPluginRequester;
34
30
  state: AgentPluginState;
35
- teamId?: string;
36
- threadTs?: string;
37
31
  userText?: string;
38
32
  }
39
33
 
@@ -41,29 +35,38 @@ const TASK_ID_PREFIX = "sched";
41
35
  const MAX_LISTED_TASKS = 50;
42
36
  const DEFAULT_SCHEDULE_TIMEZONE = "America/Los_Angeles";
43
37
 
38
+ type SchemaIssue = {
39
+ code: string;
40
+ path: readonly PropertyKey[];
41
+ };
42
+
44
43
  function throwToolInputError(error: string): never {
45
44
  throw new AgentPluginToolInputError(error);
46
45
  }
47
46
 
48
- function requireActiveDestination(
49
- context: SchedulerToolContext,
50
- ): ScheduledTaskDestination {
51
- const channelId = normalizeSlackConversationId(context.channelId);
52
- if (!channelId) {
53
- throwToolInputError("No active Slack channel context is available.");
54
- }
55
- if (!context.teamId) {
56
- throwToolInputError("No active Slack workspace context is available.");
57
- }
58
- if (!isSlackTeamId(context.teamId)) {
59
- throwToolInputError("Active Slack workspace context is invalid.");
47
+ function requireActiveDestination(context: SchedulerToolContext): Destination {
48
+ const parsed = destinationSchema.safeParse(context.destination);
49
+ if (!parsed.success) {
50
+ const destination = context.destination as Partial<Destination> | undefined;
51
+ const issues = parsed.error.issues as readonly SchemaIssue[];
52
+ if (!destination || destination.platform !== "slack") {
53
+ throwToolInputError("No active Slack destination is available.");
54
+ }
55
+ if (issues.some((issue) => issue.code === "unrecognized_keys")) {
56
+ throwToolInputError(
57
+ "Active Slack destination must not include unknown fields.",
58
+ );
59
+ }
60
+ if (issues.some((issue) => issue.path[0] === "channelId")) {
61
+ throwToolInputError("Active Slack destination channel is invalid.");
62
+ }
63
+ if (issues.some((issue) => issue.path[0] === "teamId")) {
64
+ throwToolInputError("Active Slack destination workspace is invalid.");
65
+ }
66
+ throwToolInputError("No active Slack destination is available.");
60
67
  }
61
68
 
62
- return {
63
- platform: "slack",
64
- teamId: context.teamId,
65
- channelId,
66
- };
69
+ return parsed.data;
67
70
  }
68
71
 
69
72
  function requireRequester(
@@ -91,28 +94,12 @@ function tool<TInput = any>(
91
94
  return definition;
92
95
  }
93
96
 
94
- function normalizeSlackConversationId(
95
- value: string | undefined,
96
- ): string | undefined {
97
- if (!value) return undefined;
98
- const trimmed = value.trim();
99
- if (!trimmed) return undefined;
100
- if (!trimmed.startsWith("slack:")) return trimmed;
101
-
102
- const parts = trimmed.split(":");
103
- return parts[1]?.trim() || undefined;
104
- }
105
-
106
97
  function isDmChannel(channelId: string): boolean {
107
- return normalizeSlackConversationId(channelId)?.startsWith("D") ?? false;
108
- }
109
-
110
- function isSlackTeamId(value: string): boolean {
111
- return /^T[A-Z0-9]+$/.test(value);
98
+ return channelId.startsWith("D");
112
99
  }
113
100
 
114
101
  function getConversationAccess(
115
- destination: ScheduledTaskDestination,
102
+ destination: Destination,
116
103
  ): ScheduledTaskConversationAccess {
117
104
  if (isDmChannel(destination.channelId)) {
118
105
  return { audience: "direct", visibility: "private" };
@@ -128,8 +115,8 @@ function getConversationAccess(
128
115
 
129
116
  function getCredentialSubject(args: {
130
117
  access: ScheduledTaskConversationAccess;
131
- subject: ScheduledTaskCredentialSubject | undefined;
132
- }): ScheduledTaskCredentialSubject | undefined {
118
+ subject: AgentPluginCredentialSubject | undefined;
119
+ }): AgentPluginCredentialSubject | undefined {
133
120
  if (
134
121
  args.access.audience !== "direct" ||
135
122
  args.access.visibility !== "private"
@@ -139,21 +126,26 @@ function getCredentialSubject(args: {
139
126
  if (!args.subject) {
140
127
  return undefined;
141
128
  }
129
+ const subject = agentPluginCredentialSubjectSchema.safeParse(args.subject);
130
+ if (!subject.success) {
131
+ throwToolInputError("Active Slack credential subject is invalid.");
132
+ }
142
133
  return {
143
- type: args.subject.type,
144
- userId: args.subject.userId,
145
- allowedWhen: args.subject.allowedWhen,
134
+ type: subject.data.type,
135
+ userId: subject.data.userId,
136
+ allowedWhen: subject.data.allowedWhen,
146
137
  };
147
138
  }
148
139
 
149
140
  function sameDestination(
150
141
  task: ScheduledTask,
151
- destination: ScheduledTaskDestination,
142
+ destination: Destination,
152
143
  ): boolean {
144
+ const taskDestination = task.destination;
153
145
  return (
154
- task.destination.platform === destination.platform &&
155
- task.destination.teamId === destination.teamId &&
156
- task.destination.channelId === destination.channelId
146
+ taskDestination.platform === "slack" &&
147
+ taskDestination.teamId === destination.teamId &&
148
+ taskDestination.channelId === destination.channelId
157
149
  );
158
150
  }
159
151
 
package/src/store.ts CHANGED
@@ -1,6 +1,8 @@
1
- import type {
2
- AgentPluginReadState,
3
- AgentPluginState,
1
+ import {
2
+ agentPluginCredentialSubjectSchema,
3
+ destinationSchema,
4
+ type AgentPluginReadState,
5
+ type AgentPluginState,
4
6
  } from "@sentry/junior-plugin-api";
5
7
  import { getNextRunAtMs } from "./cadence";
6
8
  import type { ScheduledRun, ScheduledTask } from "./types";
@@ -355,11 +357,42 @@ function canFinishRun(
355
357
  return run.status === "running" && run.startedAtMs === startedAtMs;
356
358
  }
357
359
 
360
+ function parseStoredTask(value: unknown): ScheduledTask | undefined {
361
+ if (!value || typeof value !== "object") {
362
+ return undefined;
363
+ }
364
+ const record = value as Partial<ScheduledTask>;
365
+ const destination = destinationSchema.safeParse(record.destination);
366
+ if (!destination.success) {
367
+ return undefined;
368
+ }
369
+ const credentialSubject =
370
+ record.credentialSubject === undefined
371
+ ? undefined
372
+ : agentPluginCredentialSubjectSchema.safeParse(record.credentialSubject);
373
+ if (credentialSubject && !credentialSubject.success) {
374
+ return undefined;
375
+ }
376
+ return {
377
+ ...(record as ScheduledTask),
378
+ destination: destination.data,
379
+ ...(credentialSubject ? { credentialSubject: credentialSubject.data } : {}),
380
+ };
381
+ }
382
+
383
+ function requireStoredTask(task: ScheduledTask): ScheduledTask {
384
+ const parsed = parseStoredTask(task);
385
+ if (!parsed) {
386
+ throw new Error("Scheduled task routing context is invalid.");
387
+ }
388
+ return parsed;
389
+ }
390
+
358
391
  async function getTaskFromState(
359
392
  state: AgentPluginReadState,
360
393
  taskId: string,
361
394
  ): Promise<ScheduledTask | undefined> {
362
- return (await state.get<ScheduledTask>(taskKey(taskId))) ?? undefined;
395
+ return parseStoredTask(await state.get(taskKey(taskId)));
363
396
  }
364
397
 
365
398
  async function listTasksFromState(
@@ -425,10 +458,10 @@ class PluginStateSchedulerStore implements SchedulerStore {
425
458
  }
426
459
 
427
460
  async saveTask(task: ScheduledTask): Promise<void> {
461
+ const next = requireStoredTask(task);
428
462
  await withLock(this.state, taskLockKey(task.id), async () => {
429
- const current =
430
- (await this.state.get<ScheduledTask>(taskKey(task.id))) ?? undefined;
431
- await this.saveTaskRecord(task, current);
463
+ const current = await getTaskFromState(this.state, task.id);
464
+ await this.saveTaskRecord(next, current);
432
465
  });
433
466
  }
434
467
 
@@ -570,8 +603,7 @@ class PluginStateSchedulerStore implements SchedulerStore {
570
603
  }): Promise<void> {
571
604
  await withLock(this.state, taskLockKey(args.task.id), async () => {
572
605
  const current =
573
- (await this.state.get<ScheduledTask>(taskKey(args.task.id))) ??
574
- undefined;
606
+ (await getTaskFromState(this.state, args.task.id)) ?? undefined;
575
607
  if (
576
608
  !current ||
577
609
  current.status !== "active" ||
@@ -767,8 +799,7 @@ class PluginStateSchedulerStore implements SchedulerStore {
767
799
  }): Promise<void> {
768
800
  await withLock(this.state, taskLockKey(args.run.taskId), async () => {
769
801
  const current =
770
- (await this.state.get<ScheduledTask>(taskKey(args.run.taskId))) ??
771
- undefined;
802
+ (await getTaskFromState(this.state, args.run.taskId)) ?? undefined;
772
803
  if (!current || current.status === "deleted") {
773
804
  return;
774
805
  }
package/src/types.ts CHANGED
@@ -1,4 +1,7 @@
1
- import type { AgentPluginCredentialSubject } from "@sentry/junior-plugin-api";
1
+ import type {
2
+ AgentPluginCredentialSubject,
3
+ Destination,
4
+ } from "@sentry/junior-plugin-api";
2
5
 
3
6
  export type ScheduledTaskStatus = "active" | "paused" | "blocked" | "deleted";
4
7
 
@@ -26,19 +29,11 @@ export const SCHEDULED_TASK_SYSTEM_ACTOR = Object.freeze({
26
29
  id: "scheduled-task",
27
30
  } satisfies ScheduledTaskExecutionActor);
28
31
 
29
- export interface ScheduledTaskDestination {
30
- platform: "slack";
31
- teamId: string;
32
- channelId: string;
33
- }
34
-
35
32
  export interface ScheduledTaskConversationAccess {
36
33
  audience: "direct" | "group" | "channel";
37
34
  visibility: "private" | "public" | "unknown";
38
35
  }
39
36
 
40
- export type ScheduledTaskCredentialSubject = AgentPluginCredentialSubject;
41
-
42
37
  export type ScheduledCalendarFrequency =
43
38
  | "daily"
44
39
  | "weekly"
@@ -76,8 +71,8 @@ export interface ScheduledTask {
76
71
  createdAtMs: number;
77
72
  createdBy: ScheduledTaskPrincipal;
78
73
  conversationAccess?: ScheduledTaskConversationAccess;
79
- credentialSubject?: ScheduledTaskCredentialSubject;
80
- destination: ScheduledTaskDestination;
74
+ credentialSubject?: AgentPluginCredentialSubject;
75
+ destination: Destination;
81
76
  executionActor?: ScheduledTaskExecutionActor;
82
77
  lastRunAtMs?: number;
83
78
  nextRunAtMs?: number;