@sentry/junior-scheduler 0.74.1 → 0.75.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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;
2
+ export declare function createSchedulerPlugin(): import("@sentry/junior-plugin-api").PluginRegistration;
3
3
  /** Register trusted 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 SlackDestination, type SlackRequester } 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
6
  source?: SlackDestination;
6
- state: AgentPluginState;
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,4 +1,4 @@
1
- import { type AgentPluginReadState, type AgentPluginState } from "@sentry/junior-plugin-api";
1
+ import { type PluginDb, type PluginReadState, type PluginState } from "@sentry/junior-plugin-api";
2
2
  import type { ScheduledRun, ScheduledTask } from "./types";
3
3
  export interface SchedulerStore {
4
4
  claimDueRun(args: {
@@ -51,6 +51,20 @@ export interface SchedulerOperationalStore {
51
51
  listTasks(): Promise<ScheduledTask[]>;
52
52
  }
53
53
  /** Create a scheduler store backed by this plugin's durable state namespace. */
54
- export declare function createSchedulerStore(state: AgentPluginState): SchedulerStore;
54
+ export declare function createSchedulerStore(state: PluginState): SchedulerStore;
55
55
  /** Create a read-only scheduler store for operational reporting. */
56
- export declare function createSchedulerOperationalStore(state: AgentPluginReadState): SchedulerOperationalStore;
56
+ export declare function createSchedulerOperationalStore(state: PluginReadState): SchedulerOperationalStore;
57
+ /** Create a scheduler store backed by the plugin SQL database. */
58
+ export declare function createSchedulerSqlStore(db: PluginDb): SchedulerStore;
59
+ /** Create a read-only scheduler operational store backed by SQL. */
60
+ export declare function createSchedulerOperationalSqlStore(db: PluginDb): SchedulerOperationalStore;
61
+ /** Copy retained scheduler plugin-state records into the scheduler SQL tables. */
62
+ export declare function migrateSchedulerStateToSql(args: {
63
+ db: PluginDb;
64
+ state: PluginState;
65
+ }): Promise<{
66
+ existing: number;
67
+ migrated: number;
68
+ missing: number;
69
+ scanned: number;
70
+ }>;
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.75.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.75.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,11 @@ 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
+ } from "./store";
12
15
  export type {
13
16
  ScheduledCalendarFrequency,
14
17
  ScheduledLocalTime,
package/src/plugin.ts CHANGED
@@ -1,19 +1,26 @@
1
1
  import {
2
2
  defineJuniorPlugin,
3
3
  type Dispatch,
4
- type AgentPluginToolDefinition,
4
+ type PluginDb,
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,
13
17
  type SchedulerOperationalStore,
14
18
  type SchedulerStore,
15
19
  } from "./store";
16
- import { scheduledTaskPrincipalLabel } from "./identity";
20
+ import {
21
+ sanitizeScheduledTaskPrincipal,
22
+ scheduledTaskPrincipalLabel,
23
+ } from "./identity";
17
24
  import type {
18
25
  ScheduledRun,
19
26
  ScheduledTask,
@@ -31,6 +38,67 @@ import {
31
38
  const SCHEDULER_HEARTBEAT_LIMIT = 10;
32
39
  const DASHBOARD_TABLE_LIMIT = 5;
33
40
 
41
+ function singleLineMetadataValue(value: string): string {
42
+ return value
43
+ .replace(/[\r\n]+/g, " ")
44
+ .replace(/\s+/g, " ")
45
+ .trim();
46
+ }
47
+
48
+ function buildScheduledTaskDispatchMetadata(args: {
49
+ nowMs: number;
50
+ run: ScheduledRun;
51
+ task: ScheduledTask;
52
+ }): Record<string, string> {
53
+ const { run, task } = args;
54
+ const creator = sanitizeScheduledTaskPrincipal(task.createdBy);
55
+ if (!task.task.text?.trim()) {
56
+ throw new Error("Scheduled task text is required");
57
+ }
58
+
59
+ return {
60
+ creatorSlackUserId: creator.slackUserId,
61
+ runId: run.id,
62
+ schedule: singleLineMetadataValue(task.schedule.description),
63
+ scheduleKind: task.schedule.kind,
64
+ scheduledFor: new Date(run.scheduledForMs).toISOString(),
65
+ runningAt: new Date(args.nowMs).toISOString(),
66
+ taskId: task.id,
67
+ timezone: task.schedule.timezone,
68
+ ...(task.schedule.recurrence
69
+ ? {
70
+ recurrenceFrequency: task.schedule.recurrence.frequency,
71
+ recurrenceInterval: String(task.schedule.recurrence.interval),
72
+ recurrenceStartDate: task.schedule.recurrence.startDate,
73
+ }
74
+ : {}),
75
+ };
76
+ }
77
+
78
+ function scheduledTaskDispatchSource(task: ScheduledTask): Source {
79
+ return {
80
+ platform: "slack",
81
+ teamId: task.destination.teamId,
82
+ channelId: task.destination.channelId,
83
+ };
84
+ }
85
+
86
+ function schedulerStore(ctx: { db?: PluginDb }): SchedulerStore {
87
+ if (!ctx.db) {
88
+ throw new Error("Scheduler plugin requires ctx.db");
89
+ }
90
+ return createSchedulerSqlStore(ctx.db);
91
+ }
92
+
93
+ function schedulerOperationalStore(ctx: {
94
+ db?: PluginDb;
95
+ }): SchedulerOperationalStore {
96
+ if (!ctx.db) {
97
+ throw new Error("Scheduler plugin requires ctx.db");
98
+ }
99
+ return createSchedulerOperationalSqlStore(ctx.db);
100
+ }
101
+
34
102
  function shouldSkipRun(
35
103
  task: ScheduledTask,
36
104
  run: ScheduledRun,
@@ -64,7 +132,7 @@ function createSchedulerToolContext(
64
132
  }
65
133
  : undefined,
66
134
  requester: ctx.requester?.platform === "slack" ? ctx.requester : undefined,
67
- state: ctx.state,
135
+ store: schedulerStore(ctx),
68
136
  userText: ctx.userText,
69
137
  };
70
138
  }
@@ -73,7 +141,7 @@ async function applyDispatchResult(args: {
73
141
  dispatch: Dispatch;
74
142
  nowMs: number;
75
143
  run: ScheduledRun;
76
- store: ReturnType<typeof createSchedulerStore>;
144
+ store: SchedulerStore;
77
145
  }): Promise<boolean> {
78
146
  if (args.dispatch.status === "completed") {
79
147
  const completed = await args.store.markRunCompleted({
@@ -362,19 +430,20 @@ async function buildSchedulerOperationalReport(args: {
362
430
  /** Create Junior's built-in trusted scheduler plugin. */
363
431
  export function createSchedulerPlugin() {
364
432
  return defineJuniorPlugin({
433
+ database: {},
365
434
  manifest: {
366
435
  name: "scheduler",
367
436
  displayName: "Scheduler",
368
437
  description: "Scheduled Junior task management and heartbeat dispatch",
369
438
  },
370
- legacyStatePrefixes: ["junior:scheduler"],
439
+ packageName: "@sentry/junior-scheduler",
371
440
  hooks: {
372
441
  tools(ctx) {
373
442
  if (
374
443
  ctx.source.platform !== "slack" ||
375
444
  ctx.requester?.platform !== "slack"
376
445
  ) {
377
- return {} as Record<string, AgentPluginToolDefinition<any>>;
446
+ return {} as Record<string, PluginToolDefinition<any>>;
378
447
  }
379
448
  const context = createSchedulerToolContext(ctx);
380
449
  return {
@@ -383,10 +452,10 @@ export function createSchedulerPlugin() {
383
452
  slackScheduleUpdateTask: createSlackScheduleUpdateTaskTool(context),
384
453
  slackScheduleDeleteTask: createSlackScheduleDeleteTaskTool(context),
385
454
  slackScheduleRunTaskNow: createSlackScheduleRunTaskNowTool(context),
386
- } satisfies Record<string, AgentPluginToolDefinition<any>>;
455
+ } satisfies Record<string, PluginToolDefinition<any>>;
387
456
  },
388
457
  async heartbeat(ctx) {
389
- const store = createSchedulerStore(ctx.state);
458
+ const store = schedulerStore(ctx);
390
459
  let processedCount = 0;
391
460
  let dispatchCount = 0;
392
461
  for (const run of await store.listIncompleteRuns()) {
@@ -443,9 +512,9 @@ export function createSchedulerPlugin() {
443
512
  continue;
444
513
  }
445
514
 
446
- let prompt: string;
515
+ let dispatchMetadata: Record<string, string>;
447
516
  try {
448
- prompt = buildScheduledTaskRunPrompt({
517
+ dispatchMetadata = buildScheduledTaskDispatchMetadata({
449
518
  nowMs: ctx.nowMs,
450
519
  run,
451
520
  task,
@@ -453,8 +522,8 @@ export function createSchedulerPlugin() {
453
522
  } catch (error) {
454
523
  const errorMessage =
455
524
  error instanceof Error
456
- ? `Scheduled task prompt could not be built: ${error.message}`
457
- : "Scheduled task prompt could not be built.";
525
+ ? `Scheduled task dispatch metadata could not be built: ${error.message}`
526
+ : "Scheduled task dispatch metadata could not be built.";
458
527
  await blockClaimedRun({
459
528
  errorMessage,
460
529
  nowMs: ctx.nowMs,
@@ -471,11 +540,9 @@ export function createSchedulerPlugin() {
471
540
  ? { credentialSubject: task.credentialSubject }
472
541
  : {}),
473
542
  destination: task.destination,
474
- input: prompt,
475
- metadata: {
476
- runId: run.id,
477
- taskId: task.id,
478
- },
543
+ input: task.task.text,
544
+ metadata: dispatchMetadata,
545
+ source: scheduledTaskDispatchSource(task),
479
546
  });
480
547
  } catch (error) {
481
548
  const errorMessage =
@@ -504,7 +571,16 @@ export function createSchedulerPlugin() {
504
571
  async operationalReport(ctx) {
505
572
  return buildSchedulerOperationalReport({
506
573
  nowMs: ctx.nowMs,
507
- store: createSchedulerOperationalStore(ctx.state),
574
+ store: schedulerOperationalStore(ctx),
575
+ });
576
+ },
577
+ async migrateStorage(ctx) {
578
+ if (!ctx.db) {
579
+ throw new Error("Scheduler storage migration requires ctx.db");
580
+ }
581
+ return await migrateSchedulerStateToSql({
582
+ db: ctx.db,
583
+ state: ctx.state,
508
584
  });
509
585
  },
510
586
  },
@@ -1,19 +1,18 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { Type } from "@sinclair/typebox";
3
3
  import {
4
- AgentPluginToolInputError,
5
- agentPluginCredentialSubjectSchema,
4
+ PluginToolInputError,
5
+ pluginCredentialSubjectSchema,
6
6
  destinationSchema,
7
7
  isSlackDestination,
8
- type AgentPluginCredentialSubject,
9
- type AgentPluginState,
10
- type AgentPluginToolDefinition,
8
+ type PluginCredentialSubject,
9
+ type PluginToolDefinition,
11
10
  type SlackDestination,
12
11
  type SlackRequester,
13
12
  } from "@sentry/junior-plugin-api";
14
13
  import { buildCalendarRecurrence, parseScheduleTimestamp } from "./cadence";
15
14
  import { sanitizeScheduledTaskPrincipal } from "./identity";
16
- import { createSchedulerStore } from "./store";
15
+ import { type SchedulerStore } from "./store";
17
16
  import { SCHEDULED_TASK_SYSTEM_ACTOR } from "./types";
18
17
  import type {
19
18
  ScheduledCalendarFrequency,
@@ -25,10 +24,10 @@ import type {
25
24
  } from "./types";
26
25
 
27
26
  export interface SchedulerToolContext {
28
- credentialSubject?: AgentPluginCredentialSubject;
27
+ credentialSubject?: PluginCredentialSubject;
29
28
  requester?: SlackRequester;
30
29
  source?: SlackDestination;
31
- state: AgentPluginState;
30
+ store: SchedulerStore;
32
31
  userText?: string;
33
32
  }
34
33
 
@@ -42,7 +41,7 @@ type SchemaIssue = {
42
41
  };
43
42
 
44
43
  function throwToolInputError(error: string): never {
45
- throw new AgentPluginToolInputError(error);
44
+ throw new PluginToolInputError(error);
46
45
  }
47
46
 
48
47
  function requireActiveConversation(
@@ -99,8 +98,8 @@ function requireRequester(
99
98
  }
100
99
 
101
100
  function tool<TInput = any>(
102
- definition: AgentPluginToolDefinition<TInput>,
103
- ): AgentPluginToolDefinition<TInput> {
101
+ definition: PluginToolDefinition<TInput>,
102
+ ): PluginToolDefinition<TInput> {
104
103
  return definition;
105
104
  }
106
105
 
@@ -125,8 +124,8 @@ function getConversationAccess(
125
124
 
126
125
  function getCredentialSubject(args: {
127
126
  access: ScheduledTaskConversationAccess;
128
- subject: AgentPluginCredentialSubject | undefined;
129
- }): AgentPluginCredentialSubject | undefined {
127
+ subject: PluginCredentialSubject | undefined;
128
+ }): PluginCredentialSubject | undefined {
130
129
  if (
131
130
  args.access.audience !== "direct" ||
132
131
  args.access.visibility !== "private"
@@ -136,7 +135,7 @@ function getCredentialSubject(args: {
136
135
  if (!args.subject) {
137
136
  return undefined;
138
137
  }
139
- const subject = agentPluginCredentialSubjectSchema.safeParse(args.subject);
138
+ const subject = pluginCredentialSubjectSchema.safeParse(args.subject);
140
139
  if (!subject.success) {
141
140
  throwToolInputError("Active Slack credential subject is invalid.");
142
141
  }
@@ -165,9 +164,7 @@ async function getWritableTask(args: {
165
164
  }): Promise<ScheduledTask> {
166
165
  const destination = requireActiveConversation(args.context);
167
166
 
168
- const task = await createSchedulerStore(args.context.state).getTask(
169
- args.taskId,
170
- );
167
+ const task = await schedulerStore(args.context).getTask(args.taskId);
171
168
  if (!task || task.status === "deleted") {
172
169
  throwToolInputError(
173
170
  "Scheduled task was not found in the active Slack conversation.",
@@ -216,7 +213,6 @@ function compactTask(task: ScheduledTask): Record<string, unknown> {
216
213
  run_now_at: task.runNowAtMs
217
214
  ? new Date(task.runNowAtMs).toISOString()
218
215
  : null,
219
- version: task.version,
220
216
  };
221
217
  }
222
218
 
@@ -224,6 +220,10 @@ function buildTaskId(): string {
224
220
  return `${TASK_ID_PREFIX}_${randomUUID()}`;
225
221
  }
226
222
 
223
+ function schedulerStore(context: SchedulerToolContext): SchedulerStore {
224
+ return context.store;
225
+ }
226
+
227
227
  function normalizeStatus(
228
228
  value: string | undefined,
229
229
  ): ScheduledTaskStatus | undefined {
@@ -299,6 +299,30 @@ function validateRecurringFrequencyLimit(input: { recurrence?: unknown }) {
299
299
  }
300
300
  }
301
301
 
302
+ function validateCreateScheduleKind(input: {
303
+ recurrence?: unknown;
304
+ schedule_kind?: unknown;
305
+ }) {
306
+ if (input.schedule_kind === undefined) {
307
+ throwToolInputError("Provide schedule_kind as one_off or recurring.");
308
+ }
309
+ if (
310
+ input.schedule_kind !== "one_off" &&
311
+ input.schedule_kind !== "recurring"
312
+ ) {
313
+ throwToolInputError("schedule_kind must be one_off or recurring.");
314
+ }
315
+ if (input.schedule_kind === "one_off" && input.recurrence !== undefined) {
316
+ throwToolInputError("Omit recurrence when schedule_kind is one_off.");
317
+ }
318
+ if (
319
+ input.schedule_kind === "recurring" &&
320
+ (input.recurrence === undefined || input.recurrence === null)
321
+ ) {
322
+ throwToolInputError("Provide recurrence when schedule_kind is recurring.");
323
+ }
324
+ }
325
+
302
326
  function shouldRebuildRecurrence(input: {
303
327
  next_run_at?: string;
304
328
  recurrence?: unknown;
@@ -343,11 +367,18 @@ export function createSlackScheduleCreateTaskTool(
343
367
  ) {
344
368
  return tool({
345
369
  description:
346
- "Create a future or recurring Junior task in the active Slack conversation. Use only when the user explicitly asks Junior to do work later or on a recurring cadence. Only manage tasks for the active Slack DM or channel; never target threads, other channels, or another user's DM. When the task, schedule, and destination are clear, create it without asking for confirmation; ask only when one of those is ambiguous.",
370
+ "Create a one-time or recurring Junior task in the active Slack conversation. For one-time reminders or one-time scheduled work, omit recurrence entirely; never choose a default recurrence. Use only when the user explicitly asks Junior to do work later or on a recurring cadence. Only manage tasks for the active Slack DM or channel; never target threads, other channels, or another user's DM. When the task, schedule, and destination are clear, create it without asking for confirmation; ask only when one of those is ambiguous.",
347
371
  executionMode: "sequential",
348
372
  inputSchema: Type.Object({
349
373
  task: Type.String({ minLength: 1, maxLength: 4000 }),
350
374
  schedule: Type.String({ minLength: 1, maxLength: 300 }),
375
+ schedule_kind: Type.Union(
376
+ [Type.Literal("one_off"), Type.Literal("recurring")],
377
+ {
378
+ description:
379
+ "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.",
380
+ },
381
+ ),
351
382
  timezone: Type.Optional(
352
383
  Type.String({
353
384
  minLength: 1,
@@ -373,7 +404,7 @@ export function createSlackScheduleCreateTaskTool(
373
404
  ],
374
405
  {
375
406
  description:
376
- "Provide only for explicitly repeating schedules; omit for one-time requests like 'in 1 minute', 'tomorrow', or a specific date. Recurring tasks run at most once per day: use daily, weekly, monthly, or yearly only.",
407
+ "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.",
377
408
  },
378
409
  ),
379
410
  ),
@@ -384,6 +415,7 @@ export function createSlackScheduleCreateTaskTool(
384
415
 
385
416
  const nowMs = Date.now();
386
417
  const timezone = input.timezone ?? getDefaultScheduleTimezone();
418
+ validateCreateScheduleKind(input);
387
419
  validateRecurringFrequencyLimit(input);
388
420
  if (!isValidTimeZone(timezone)) {
389
421
  throwToolInputError("timezone must be a valid IANA time zone.");
@@ -424,10 +456,9 @@ export function createSlackScheduleCreateTaskTool(
424
456
  task: {
425
457
  text: input.task,
426
458
  },
427
- version: 1,
428
459
  };
429
460
 
430
- await createSchedulerStore(context.state).saveTask(task);
461
+ await schedulerStore(context).saveTask(task);
431
462
  return {
432
463
  ok: true,
433
464
  task: compactTask(task),
@@ -448,7 +479,7 @@ export function createSlackScheduleListTasksTool(
448
479
  execute: async () => {
449
480
  const destination = requireActiveConversation(context);
450
481
 
451
- const tasks = await createSchedulerStore(context.state).listTasksForTeam(
482
+ const tasks = await schedulerStore(context).listTasksForTeam(
452
483
  destination.teamId,
453
484
  );
454
485
  const matching = tasks.filter((task) =>
@@ -571,10 +602,9 @@ export function createSlackScheduleUpdateTaskTool(
571
602
  recurrence,
572
603
  },
573
604
  task: input.task ? { text: input.task } : lookup.task,
574
- version: lookup.version + 1,
575
605
  };
576
606
 
577
- await createSchedulerStore(context.state).saveTask(next);
607
+ await schedulerStore(context).saveTask(next);
578
608
  return {
579
609
  ok: true,
580
610
  task: compactTask(next),
@@ -607,10 +637,9 @@ export function createSlackScheduleDeleteTaskTool(
607
637
  status: "deleted",
608
638
  nextRunAtMs: undefined,
609
639
  runNowAtMs: undefined,
610
- version: lookup.version + 1,
611
640
  };
612
641
 
613
- await createSchedulerStore(context.state).saveTask(next);
642
+ await schedulerStore(context).saveTask(next);
614
643
  return {
615
644
  ok: true,
616
645
  task: compactTask(next),
@@ -647,10 +676,9 @@ export function createSlackScheduleRunTaskNowTool(
647
676
  ...lookup,
648
677
  updatedAtMs: nowMs,
649
678
  runNowAtMs: nowMs,
650
- version: lookup.version + 1,
651
679
  };
652
680
 
653
- await createSchedulerStore(context.state).saveTask(next);
681
+ await schedulerStore(context).saveTask(next);
654
682
  return {
655
683
  ok: true,
656
684
  task: compactTask(next),