@sentry/junior-scheduler 0.74.1 → 0.76.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/plugin.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  /** Create Junior's built-in trusted scheduler plugin. */
2
- export declare function createSchedulerPlugin(): import("@sentry/junior-plugin-api").JuniorPluginRegistration;
3
- /** Register trusted scheduler runtime hooks for scheduled Junior tasks. */
2
+ export declare function createSchedulerPlugin(): import("@sentry/junior-plugin-api").PluginRegistration;
3
+ /** Register scheduler runtime hooks for scheduled Junior tasks. */
4
4
  export declare const schedulerPlugin: typeof createSchedulerPlugin;
@@ -1,18 +1,19 @@
1
- import { type AgentPluginCredentialSubject, type AgentPluginState, type AgentPluginToolDefinition, type SlackDestination, type SlackRequester } from "@sentry/junior-plugin-api";
1
+ import { type PluginCredentialSubject, type PluginToolDefinition, type SlackRequester, type SlackSource } from "@sentry/junior-plugin-api";
2
+ import { type SchedulerStore } from "./store";
2
3
  export interface SchedulerToolContext {
3
- credentialSubject?: AgentPluginCredentialSubject;
4
+ credentialSubject?: PluginCredentialSubject;
4
5
  requester?: SlackRequester;
5
- source?: SlackDestination;
6
- state: AgentPluginState;
6
+ source?: SlackSource;
7
+ store: SchedulerStore;
7
8
  userText?: string;
8
9
  }
9
10
  /** Create a tool that stores a scheduled task for the active Slack context. */
10
- export declare function createSlackScheduleCreateTaskTool(context: SchedulerToolContext): AgentPluginToolDefinition<any>;
11
+ export declare function createSlackScheduleCreateTaskTool(context: SchedulerToolContext): PluginToolDefinition<any>;
11
12
  /** Create a tool that lists scheduled tasks for the active Slack conversation. */
12
- export declare function createSlackScheduleListTasksTool(context: SchedulerToolContext): AgentPluginToolDefinition<any>;
13
+ export declare function createSlackScheduleListTasksTool(context: SchedulerToolContext): PluginToolDefinition<any>;
13
14
  /** Create a tool that edits a scheduled task in the active Slack conversation. */
14
- export declare function createSlackScheduleUpdateTaskTool(context: SchedulerToolContext): AgentPluginToolDefinition<any>;
15
+ export declare function createSlackScheduleUpdateTaskTool(context: SchedulerToolContext): PluginToolDefinition<any>;
15
16
  /** Create a tool that removes a scheduled task from the active Slack conversation. */
16
- export declare function createSlackScheduleDeleteTaskTool(context: SchedulerToolContext): AgentPluginToolDefinition<any>;
17
+ export declare function createSlackScheduleDeleteTaskTool(context: SchedulerToolContext): PluginToolDefinition<any>;
17
18
  /** Create a tool that marks an existing scheduled task due immediately. */
18
- export declare function createSlackScheduleRunTaskNowTool(context: SchedulerToolContext): AgentPluginToolDefinition<any>;
19
+ export declare function createSlackScheduleRunTaskNowTool(context: SchedulerToolContext): PluginToolDefinition<any>;
package/dist/store.d.ts CHANGED
@@ -1,5 +1,9 @@
1
- import { type AgentPluginReadState, type AgentPluginState } from "@sentry/junior-plugin-api";
1
+ import { type PluginReadState, type PluginState } from "@sentry/junior-plugin-api";
2
+ import type { PgDatabase } from "drizzle-orm/pg-core";
3
+ import type { PgQueryResultHKT } from "drizzle-orm/pg-core/session";
4
+ import * as schedulerSqlSchema from "./db/schema";
2
5
  import type { ScheduledRun, ScheduledTask } from "./types";
6
+ export type SchedulerDb = PgDatabase<PgQueryResultHKT, typeof schedulerSqlSchema>;
3
7
  export interface SchedulerStore {
4
8
  claimDueRun(args: {
5
9
  nowMs: number;
@@ -51,6 +55,20 @@ export interface SchedulerOperationalStore {
51
55
  listTasks(): Promise<ScheduledTask[]>;
52
56
  }
53
57
  /** Create a scheduler store backed by this plugin's durable state namespace. */
54
- export declare function createSchedulerStore(state: AgentPluginState): SchedulerStore;
58
+ export declare function createSchedulerStore(state: PluginState): SchedulerStore;
55
59
  /** Create a read-only scheduler store for operational reporting. */
56
- export declare function createSchedulerOperationalStore(state: AgentPluginReadState): SchedulerOperationalStore;
60
+ export declare function createSchedulerOperationalStore(state: PluginReadState): SchedulerOperationalStore;
61
+ /** Create a scheduler store backed by the plugin SQL database. */
62
+ export declare function createSchedulerSqlStore(db: SchedulerDb): SchedulerStore;
63
+ /** Create a read-only scheduler operational store backed by SQL. */
64
+ export declare function createSchedulerOperationalSqlStore(db: SchedulerDb): SchedulerOperationalStore;
65
+ /** Copy retained scheduler plugin-state records into the scheduler SQL tables. */
66
+ export declare function migrateSchedulerStateToSql(args: {
67
+ db: SchedulerDb;
68
+ state: PluginState;
69
+ }): Promise<{
70
+ existing: number;
71
+ migrated: number;
72
+ missing: number;
73
+ scanned: number;
74
+ }>;
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AgentPluginCredentialSubject, SlackDestination } from "@sentry/junior-plugin-api";
1
+ import type { PluginCredentialSubject, SlackDestination } 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 {
@@ -46,7 +46,7 @@ export interface ScheduledTask {
46
46
  createdAtMs: number;
47
47
  createdBy: ScheduledTaskPrincipal;
48
48
  conversationAccess?: ScheduledTaskConversationAccess;
49
- credentialSubject?: AgentPluginCredentialSubject;
49
+ credentialSubject?: PluginCredentialSubject;
50
50
  destination: SlackDestination;
51
51
  executionActor?: ScheduledTaskExecutionActor;
52
52
  lastRunAtMs?: number;
@@ -58,7 +58,6 @@ export interface ScheduledTask {
58
58
  statusReason?: string;
59
59
  task: ScheduledTaskSpec;
60
60
  updatedAtMs: number;
61
- version: number;
62
61
  }
63
62
  export interface ScheduledRun {
64
63
  id: string;
@@ -67,11 +66,9 @@ export interface ScheduledRun {
67
66
  completedAtMs?: number;
68
67
  dispatchId?: string;
69
68
  errorMessage?: string;
70
- idempotencyKey: string;
71
69
  resultMessageTs?: string;
72
70
  scheduledForMs: number;
73
71
  startedAtMs?: number;
74
72
  status: ScheduledRunStatus;
75
73
  taskId: string;
76
- taskVersion: number;
77
74
  }
@@ -0,0 +1,35 @@
1
+ CREATE TABLE IF NOT EXISTS junior_scheduler_tasks (
2
+ id TEXT PRIMARY KEY,
3
+ team_id TEXT NOT NULL,
4
+ status TEXT NOT NULL,
5
+ next_run_at_ms BIGINT,
6
+ run_now_at_ms BIGINT,
7
+ created_at_ms BIGINT NOT NULL,
8
+ record JSONB NOT NULL
9
+ );
10
+
11
+ CREATE INDEX IF NOT EXISTS junior_scheduler_tasks_team_status_idx
12
+ ON junior_scheduler_tasks (team_id, created_at_ms, id)
13
+ WHERE status <> 'deleted';
14
+
15
+ CREATE INDEX IF NOT EXISTS junior_scheduler_tasks_run_now_due_idx
16
+ ON junior_scheduler_tasks (run_now_at_ms, created_at_ms, id)
17
+ WHERE status = 'active' AND run_now_at_ms IS NOT NULL;
18
+
19
+ CREATE INDEX IF NOT EXISTS junior_scheduler_tasks_next_run_due_idx
20
+ ON junior_scheduler_tasks (next_run_at_ms, created_at_ms, id)
21
+ WHERE status = 'active' AND next_run_at_ms IS NOT NULL;
22
+
23
+ CREATE TABLE IF NOT EXISTS junior_scheduler_runs (
24
+ id TEXT PRIMARY KEY,
25
+ task_id TEXT NOT NULL,
26
+ status TEXT NOT NULL,
27
+ scheduled_for_ms BIGINT NOT NULL,
28
+ record JSONB NOT NULL
29
+ );
30
+
31
+ CREATE INDEX IF NOT EXISTS junior_scheduler_runs_task_status_idx
32
+ ON junior_scheduler_runs (task_id, status, scheduled_for_ms);
33
+
34
+ CREATE INDEX IF NOT EXISTS junior_scheduler_runs_status_idx
35
+ ON junior_scheduler_runs (status, scheduled_for_ms);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-scheduler",
3
- "version": "0.74.1",
3
+ "version": "0.76.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -19,11 +19,14 @@
19
19
  },
20
20
  "files": [
21
21
  "dist",
22
+ "migrations",
22
23
  "src"
23
24
  ],
24
25
  "dependencies": {
25
26
  "@sinclair/typebox": "^0.34.49",
26
- "@sentry/junior-plugin-api": "0.74.1"
27
+ "drizzle-orm": "^0.45.2",
28
+ "zod": "^4.4.3",
29
+ "@sentry/junior-plugin-api": "0.76.0"
27
30
  },
28
31
  "devDependencies": {
29
32
  "@types/node": "^25.9.1",
@@ -32,6 +35,7 @@
32
35
  },
33
36
  "scripts": {
34
37
  "build": "tsup && tsc -p tsconfig.build.json --emitDeclarationOnly",
38
+ "db:generate": "pnpm dlx drizzle-kit@0.31.10 generate --config drizzle.config.ts",
35
39
  "typecheck": "tsc --noEmit"
36
40
  }
37
41
  }
@@ -0,0 +1,53 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { bigint, index, jsonb, pgTable, text } from "drizzle-orm/pg-core";
3
+ import type { ScheduledRun, ScheduledTask } from "../types";
4
+
5
+ export const juniorSchedulerTasks = pgTable(
6
+ "junior_scheduler_tasks",
7
+ {
8
+ id: text("id").primaryKey(),
9
+ teamId: text("team_id").notNull(),
10
+ status: text("status").notNull(),
11
+ nextRunAtMs: bigint("next_run_at_ms", { mode: "number" }),
12
+ runNowAtMs: bigint("run_now_at_ms", { mode: "number" }),
13
+ createdAtMs: bigint("created_at_ms", { mode: "number" }).notNull(),
14
+ record: jsonb("record").$type<ScheduledTask>().notNull(),
15
+ },
16
+ (table) => [
17
+ index("junior_scheduler_tasks_team_status_idx")
18
+ .on(table.teamId, table.createdAtMs, table.id)
19
+ .where(sql`${table.status} <> 'deleted'`),
20
+ index("junior_scheduler_tasks_run_now_due_idx")
21
+ .on(table.runNowAtMs, table.createdAtMs, table.id)
22
+ .where(
23
+ sql`${table.status} = 'active' AND ${table.runNowAtMs} IS NOT NULL`,
24
+ ),
25
+ index("junior_scheduler_tasks_next_run_due_idx")
26
+ .on(table.nextRunAtMs, table.createdAtMs, table.id)
27
+ .where(
28
+ sql`${table.status} = 'active' AND ${table.nextRunAtMs} IS NOT NULL`,
29
+ ),
30
+ ],
31
+ );
32
+
33
+ export const juniorSchedulerRuns = pgTable(
34
+ "junior_scheduler_runs",
35
+ {
36
+ id: text("id").primaryKey(),
37
+ taskId: text("task_id").notNull(),
38
+ status: text("status").notNull(),
39
+ scheduledForMs: bigint("scheduled_for_ms", { mode: "number" }).notNull(),
40
+ record: jsonb("record").$type<ScheduledRun>().notNull(),
41
+ },
42
+ (table) => [
43
+ index("junior_scheduler_runs_task_status_idx").on(
44
+ table.taskId,
45
+ table.status,
46
+ table.scheduledForMs,
47
+ ),
48
+ index("junior_scheduler_runs_status_idx").on(
49
+ table.status,
50
+ table.scheduledForMs,
51
+ ),
52
+ ],
53
+ );
package/src/index.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  export { createSchedulerPlugin, schedulerPlugin } from "./plugin";
2
- export { buildScheduledTaskRunPrompt } from "./prompt";
3
2
  export {
4
3
  createSlackScheduleCreateTaskTool,
5
4
  createSlackScheduleDeleteTaskTool,
@@ -8,7 +7,12 @@ export {
8
7
  createSlackScheduleUpdateTaskTool,
9
8
  type SchedulerToolContext,
10
9
  } from "./schedule-tools";
11
- export { createSchedulerStore } from "./store";
10
+ export {
11
+ createSchedulerOperationalSqlStore,
12
+ createSchedulerSqlStore,
13
+ migrateSchedulerStateToSql,
14
+ type SchedulerDb,
15
+ } from "./store";
12
16
  export type {
13
17
  ScheduledCalendarFrequency,
14
18
  ScheduledLocalTime,
package/src/plugin.ts CHANGED
@@ -1,19 +1,27 @@
1
1
  import {
2
+ createSlackSource,
2
3
  defineJuniorPlugin,
3
4
  type Dispatch,
4
- type AgentPluginToolDefinition,
5
+ type PluginToolDefinition,
5
6
  type PluginOperationalReportContent,
7
+ type PluginReadState,
8
+ type PluginState,
6
9
  type SlackDestination,
10
+ type Source,
7
11
  type ToolRegistrationHookContext,
8
12
  } from "@sentry/junior-plugin-api";
9
- import { buildScheduledTaskRunPrompt } from "./prompt";
10
13
  import {
11
- createSchedulerOperationalStore,
12
- createSchedulerStore,
14
+ createSchedulerOperationalSqlStore,
15
+ createSchedulerSqlStore,
16
+ migrateSchedulerStateToSql,
17
+ type SchedulerDb,
13
18
  type SchedulerOperationalStore,
14
19
  type SchedulerStore,
15
20
  } from "./store";
16
- import { scheduledTaskPrincipalLabel } from "./identity";
21
+ import {
22
+ sanitizeScheduledTaskPrincipal,
23
+ scheduledTaskPrincipalLabel,
24
+ } from "./identity";
17
25
  import type {
18
26
  ScheduledRun,
19
27
  ScheduledTask,
@@ -31,6 +39,60 @@ import {
31
39
  const SCHEDULER_HEARTBEAT_LIMIT = 10;
32
40
  const DASHBOARD_TABLE_LIMIT = 5;
33
41
 
42
+ function singleLineMetadataValue(value: string): string {
43
+ return value
44
+ .replace(/[\r\n]+/g, " ")
45
+ .replace(/\s+/g, " ")
46
+ .trim();
47
+ }
48
+
49
+ function buildScheduledTaskDispatchMetadata(args: {
50
+ nowMs: number;
51
+ run: ScheduledRun;
52
+ task: ScheduledTask;
53
+ }): Record<string, string> {
54
+ const { run, task } = args;
55
+ const creator = sanitizeScheduledTaskPrincipal(task.createdBy);
56
+ if (!task.task.text?.trim()) {
57
+ throw new Error("Scheduled task text is required");
58
+ }
59
+
60
+ return {
61
+ creatorSlackUserId: creator.slackUserId,
62
+ runId: run.id,
63
+ schedule: singleLineMetadataValue(task.schedule.description),
64
+ scheduleKind: task.schedule.kind,
65
+ scheduledFor: new Date(run.scheduledForMs).toISOString(),
66
+ runningAt: new Date(args.nowMs).toISOString(),
67
+ taskId: task.id,
68
+ timezone: task.schedule.timezone,
69
+ ...(task.schedule.recurrence
70
+ ? {
71
+ recurrenceFrequency: task.schedule.recurrence.frequency,
72
+ recurrenceInterval: String(task.schedule.recurrence.interval),
73
+ recurrenceStartDate: task.schedule.recurrence.startDate,
74
+ }
75
+ : {}),
76
+ };
77
+ }
78
+
79
+ function scheduledTaskDispatchSource(task: ScheduledTask): Source {
80
+ return createSlackSource({
81
+ teamId: task.destination.teamId,
82
+ channelId: task.destination.channelId,
83
+ });
84
+ }
85
+
86
+ function schedulerStore(ctx: { db: unknown }): SchedulerStore {
87
+ return createSchedulerSqlStore(ctx.db as SchedulerDb);
88
+ }
89
+
90
+ function schedulerOperationalStore(ctx: {
91
+ db: unknown;
92
+ }): SchedulerOperationalStore {
93
+ return createSchedulerOperationalSqlStore(ctx.db as SchedulerDb);
94
+ }
95
+
34
96
  function shouldSkipRun(
35
97
  task: ScheduledTask,
36
98
  run: ScheduledRun,
@@ -55,16 +117,9 @@ function createSchedulerToolContext(
55
117
  ): SchedulerToolContext {
56
118
  return {
57
119
  credentialSubject: ctx.slack?.credentialSubject,
58
- source:
59
- ctx.source.platform === "slack"
60
- ? {
61
- platform: "slack",
62
- teamId: ctx.source.teamId,
63
- channelId: ctx.source.channelId,
64
- }
65
- : undefined,
120
+ source: ctx.source.platform === "slack" ? ctx.source : undefined,
66
121
  requester: ctx.requester?.platform === "slack" ? ctx.requester : undefined,
67
- state: ctx.state,
122
+ store: schedulerStore(ctx),
68
123
  userText: ctx.userText,
69
124
  };
70
125
  }
@@ -73,7 +128,7 @@ async function applyDispatchResult(args: {
73
128
  dispatch: Dispatch;
74
129
  nowMs: number;
75
130
  run: ScheduledRun;
76
- store: ReturnType<typeof createSchedulerStore>;
131
+ store: SchedulerStore;
77
132
  }): Promise<boolean> {
78
133
  if (args.dispatch.status === "completed") {
79
134
  const completed = await args.store.markRunCompleted({
@@ -367,14 +422,14 @@ export function createSchedulerPlugin() {
367
422
  displayName: "Scheduler",
368
423
  description: "Scheduled Junior task management and heartbeat dispatch",
369
424
  },
370
- legacyStatePrefixes: ["junior:scheduler"],
425
+ packageName: "@sentry/junior-scheduler",
371
426
  hooks: {
372
427
  tools(ctx) {
373
428
  if (
374
429
  ctx.source.platform !== "slack" ||
375
430
  ctx.requester?.platform !== "slack"
376
431
  ) {
377
- return {} as Record<string, AgentPluginToolDefinition<any>>;
432
+ return {} as Record<string, PluginToolDefinition<any>>;
378
433
  }
379
434
  const context = createSchedulerToolContext(ctx);
380
435
  return {
@@ -383,10 +438,10 @@ export function createSchedulerPlugin() {
383
438
  slackScheduleUpdateTask: createSlackScheduleUpdateTaskTool(context),
384
439
  slackScheduleDeleteTask: createSlackScheduleDeleteTaskTool(context),
385
440
  slackScheduleRunTaskNow: createSlackScheduleRunTaskNowTool(context),
386
- } satisfies Record<string, AgentPluginToolDefinition<any>>;
441
+ } satisfies Record<string, PluginToolDefinition<any>>;
387
442
  },
388
443
  async heartbeat(ctx) {
389
- const store = createSchedulerStore(ctx.state);
444
+ const store = schedulerStore(ctx);
390
445
  let processedCount = 0;
391
446
  let dispatchCount = 0;
392
447
  for (const run of await store.listIncompleteRuns()) {
@@ -443,9 +498,9 @@ export function createSchedulerPlugin() {
443
498
  continue;
444
499
  }
445
500
 
446
- let prompt: string;
501
+ let dispatchMetadata: Record<string, string>;
447
502
  try {
448
- prompt = buildScheduledTaskRunPrompt({
503
+ dispatchMetadata = buildScheduledTaskDispatchMetadata({
449
504
  nowMs: ctx.nowMs,
450
505
  run,
451
506
  task,
@@ -453,8 +508,8 @@ export function createSchedulerPlugin() {
453
508
  } catch (error) {
454
509
  const errorMessage =
455
510
  error instanceof Error
456
- ? `Scheduled task prompt could not be built: ${error.message}`
457
- : "Scheduled task prompt could not be built.";
511
+ ? `Scheduled task dispatch metadata could not be built: ${error.message}`
512
+ : "Scheduled task dispatch metadata could not be built.";
458
513
  await blockClaimedRun({
459
514
  errorMessage,
460
515
  nowMs: ctx.nowMs,
@@ -471,11 +526,9 @@ export function createSchedulerPlugin() {
471
526
  ? { credentialSubject: task.credentialSubject }
472
527
  : {}),
473
528
  destination: task.destination,
474
- input: prompt,
475
- metadata: {
476
- runId: run.id,
477
- taskId: task.id,
478
- },
529
+ input: task.task.text,
530
+ metadata: dispatchMetadata,
531
+ source: scheduledTaskDispatchSource(task),
479
532
  });
480
533
  } catch (error) {
481
534
  const errorMessage =
@@ -504,12 +557,18 @@ export function createSchedulerPlugin() {
504
557
  async operationalReport(ctx) {
505
558
  return buildSchedulerOperationalReport({
506
559
  nowMs: ctx.nowMs,
507
- store: createSchedulerOperationalStore(ctx.state),
560
+ store: schedulerOperationalStore(ctx),
561
+ });
562
+ },
563
+ async migrateStorage(ctx) {
564
+ return await migrateSchedulerStateToSql({
565
+ db: ctx.db as SchedulerDb,
566
+ state: ctx.state,
508
567
  });
509
568
  },
510
569
  },
511
570
  });
512
571
  }
513
572
 
514
- /** Register trusted scheduler runtime hooks for scheduled Junior tasks. */
573
+ /** Register scheduler runtime hooks for scheduled Junior tasks. */
515
574
  export const schedulerPlugin = createSchedulerPlugin;