@sentry/junior-scheduler 0.74.0 → 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/db/schema.d.ts +223 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.js +595 -444
- package/dist/plugin.d.ts +1 -1
- package/dist/schedule-tools.d.ts +9 -8
- package/dist/store.d.ts +17 -3
- package/dist/types.d.ts +2 -5
- package/migrations/0001_scheduler.sql +35 -0
- package/package.json +6 -2
- package/src/db/schema.ts +53 -0
- package/src/index.ts +5 -2
- package/src/plugin.ts +97 -21
- package/src/schedule-tools.ts +57 -29
- package/src/store.ts +822 -47
- package/src/types.ts +2 -5
- package/dist/prompt.d.ts +0 -7
- package/src/prompt.ts +0 -90
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").
|
|
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;
|
package/dist/schedule-tools.d.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import { type
|
|
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?:
|
|
4
|
+
credentialSubject?: PluginCredentialSubject;
|
|
4
5
|
requester?: SlackRequester;
|
|
5
6
|
source?: SlackDestination;
|
|
6
|
-
|
|
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):
|
|
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):
|
|
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):
|
|
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):
|
|
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):
|
|
19
|
+
export declare function createSlackScheduleRunTaskNowTool(context: SchedulerToolContext): PluginToolDefinition<any>;
|
package/dist/store.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
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:
|
|
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:
|
|
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 {
|
|
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?:
|
|
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.
|
|
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
|
-
"
|
|
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
|
}
|
package/src/db/schema.ts
ADDED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
12
|
-
|
|
14
|
+
createSchedulerOperationalSqlStore,
|
|
15
|
+
createSchedulerSqlStore,
|
|
16
|
+
migrateSchedulerStateToSql,
|
|
13
17
|
type SchedulerOperationalStore,
|
|
14
18
|
type SchedulerStore,
|
|
15
19
|
} from "./store";
|
|
16
|
-
import {
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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,
|
|
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,
|
|
455
|
+
} satisfies Record<string, PluginToolDefinition<any>>;
|
|
387
456
|
},
|
|
388
457
|
async heartbeat(ctx) {
|
|
389
|
-
const store =
|
|
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
|
|
515
|
+
let dispatchMetadata: Record<string, string>;
|
|
447
516
|
try {
|
|
448
|
-
|
|
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
|
|
457
|
-
: "Scheduled task
|
|
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:
|
|
475
|
-
metadata:
|
|
476
|
-
|
|
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:
|
|
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
|
},
|
package/src/schedule-tools.ts
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
PluginToolInputError,
|
|
5
|
+
pluginCredentialSubjectSchema,
|
|
6
6
|
destinationSchema,
|
|
7
7
|
isSlackDestination,
|
|
8
|
-
type
|
|
9
|
-
type
|
|
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 {
|
|
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?:
|
|
27
|
+
credentialSubject?: PluginCredentialSubject;
|
|
29
28
|
requester?: SlackRequester;
|
|
30
29
|
source?: SlackDestination;
|
|
31
|
-
|
|
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
|
|
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:
|
|
103
|
-
):
|
|
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:
|
|
129
|
-
}):
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
681
|
+
await schedulerStore(context).saveTask(next);
|
|
654
682
|
return {
|
|
655
683
|
ok: true,
|
|
656
684
|
task: compactTask(next),
|