@sentry/junior-scheduler 0.66.3 → 0.67.1
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/identity.d.ts +5 -0
- package/dist/index.js +254 -25
- package/dist/store.d.ts +8 -1
- package/package.json +2 -2
- package/src/identity.ts +64 -0
- package/src/plugin.ts +200 -2
- package/src/prompt.ts +2 -1
- package/src/schedule-tools.ts +5 -4
- package/src/store.ts +89 -25
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ScheduledTaskPrincipal } from "./types";
|
|
2
|
+
/** Keep scheduler creator metadata from promoting Slack IDs into display names. */
|
|
3
|
+
export declare function sanitizeScheduledTaskPrincipal(principal: ScheduledTaskPrincipal): ScheduledTaskPrincipal;
|
|
4
|
+
/** Render scheduler creator metadata without inventing human profile fields. */
|
|
5
|
+
export declare function scheduledTaskPrincipalLabel(principal: ScheduledTaskPrincipal): string;
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,50 @@ var SCHEDULED_TASK_SYSTEM_ACTOR = Object.freeze({
|
|
|
9
9
|
id: "scheduled-task"
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
+
// src/identity.ts
|
|
13
|
+
var SLACK_USER_ID_PATTERN = /^[UW][A-Z0-9]{5,}$/;
|
|
14
|
+
function clean(value) {
|
|
15
|
+
const normalized = value?.trim();
|
|
16
|
+
return normalized ? normalized : void 0;
|
|
17
|
+
}
|
|
18
|
+
function cleanSlackUserId(value) {
|
|
19
|
+
const normalized = clean(value);
|
|
20
|
+
return normalized && normalized.toLowerCase() !== "unknown" ? normalized : void 0;
|
|
21
|
+
}
|
|
22
|
+
function cleanDisplay(value, slackUserId) {
|
|
23
|
+
const normalized = clean(value);
|
|
24
|
+
if (!normalized || normalized.toLowerCase() === "unknown" || normalized === slackUserId) {
|
|
25
|
+
return void 0;
|
|
26
|
+
}
|
|
27
|
+
return SLACK_USER_ID_PATTERN.test(normalized) ? void 0 : normalized;
|
|
28
|
+
}
|
|
29
|
+
function sanitizeScheduledTaskPrincipal(principal) {
|
|
30
|
+
const slackUserId = cleanSlackUserId(principal.slackUserId);
|
|
31
|
+
if (!slackUserId) {
|
|
32
|
+
throw new Error("Scheduled task principal requires a Slack user id");
|
|
33
|
+
}
|
|
34
|
+
const fullName = cleanDisplay(principal.fullName, slackUserId);
|
|
35
|
+
const userName = cleanDisplay(principal.userName, slackUserId);
|
|
36
|
+
return {
|
|
37
|
+
slackUserId,
|
|
38
|
+
...fullName ? { fullName } : {},
|
|
39
|
+
...userName ? { userName } : {}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function scheduledTaskPrincipalLabel(principal) {
|
|
43
|
+
const author = sanitizeScheduledTaskPrincipal(principal);
|
|
44
|
+
if (author.fullName && author.userName) {
|
|
45
|
+
return `${author.fullName} (@${author.userName})`;
|
|
46
|
+
}
|
|
47
|
+
if (author.fullName) {
|
|
48
|
+
return author.fullName;
|
|
49
|
+
}
|
|
50
|
+
if (author.userName) {
|
|
51
|
+
return `@${author.userName}`;
|
|
52
|
+
}
|
|
53
|
+
return `Slack User ${author.slackUserId}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
12
56
|
// src/prompt.ts
|
|
13
57
|
function escapeXml(value) {
|
|
14
58
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
@@ -28,7 +72,7 @@ function renderOptionalLine(name, value) {
|
|
|
28
72
|
function buildScheduledTaskRunPrompt(args) {
|
|
29
73
|
const { run, task } = args;
|
|
30
74
|
const destination = task.destination;
|
|
31
|
-
const creator = task.createdBy;
|
|
75
|
+
const creator = sanitizeScheduledTaskPrincipal(task.createdBy);
|
|
32
76
|
const executionActor = task.executionActor ?? SCHEDULED_TASK_SYSTEM_ACTOR;
|
|
33
77
|
if (!task.task.text?.trim()) {
|
|
34
78
|
throw new Error("Scheduled task text is required");
|
|
@@ -606,6 +650,43 @@ function canFinishRun(run, startedAtMs) {
|
|
|
606
650
|
}
|
|
607
651
|
return run.status === "running" && run.startedAtMs === startedAtMs;
|
|
608
652
|
}
|
|
653
|
+
async function getTaskFromState(state, taskId) {
|
|
654
|
+
return await state.get(taskKey(taskId)) ?? void 0;
|
|
655
|
+
}
|
|
656
|
+
async function listTasksFromState(state, indexKey) {
|
|
657
|
+
const ids = await getIndex(state, indexKey);
|
|
658
|
+
const tasks = await Promise.all(ids.map((id) => getTaskFromState(state, id)));
|
|
659
|
+
return tasks.filter((task) => Boolean(task)).filter((task) => task.status !== "deleted").sort((a, b) => a.createdAtMs - b.createdAtMs);
|
|
660
|
+
}
|
|
661
|
+
async function getRunFromState(state, runId) {
|
|
662
|
+
return await state.get(runKey(runId)) ?? void 0;
|
|
663
|
+
}
|
|
664
|
+
async function listIncompleteRunsForTasksFromState(state, tasks) {
|
|
665
|
+
const runs = [];
|
|
666
|
+
for (const task of tasks) {
|
|
667
|
+
const active = await state.get(activeRunKey(task.id));
|
|
668
|
+
if (typeof active?.runId !== "string") {
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
const run = await getRunFromState(state, active.runId);
|
|
672
|
+
if (run && !isFinishedRun(run)) {
|
|
673
|
+
runs.push(run);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return runs;
|
|
677
|
+
}
|
|
678
|
+
var PluginStateSchedulerOperationalStore = class {
|
|
679
|
+
state;
|
|
680
|
+
constructor(state) {
|
|
681
|
+
this.state = state;
|
|
682
|
+
}
|
|
683
|
+
async listTasks() {
|
|
684
|
+
return await listTasksFromState(this.state, globalTaskIndexKey());
|
|
685
|
+
}
|
|
686
|
+
async listIncompleteRunsForTasks(tasks) {
|
|
687
|
+
return await listIncompleteRunsForTasksFromState(this.state, tasks);
|
|
688
|
+
}
|
|
689
|
+
};
|
|
609
690
|
var PluginStateSchedulerStore = class {
|
|
610
691
|
state;
|
|
611
692
|
constructor(state) {
|
|
@@ -653,12 +734,13 @@ var PluginStateSchedulerStore = class {
|
|
|
653
734
|
}
|
|
654
735
|
}
|
|
655
736
|
async getTask(taskId) {
|
|
656
|
-
return await this.state
|
|
737
|
+
return await getTaskFromState(this.state, taskId);
|
|
738
|
+
}
|
|
739
|
+
async listTasks() {
|
|
740
|
+
return await listTasksFromState(this.state, globalTaskIndexKey());
|
|
657
741
|
}
|
|
658
742
|
async listTasksForTeam(teamId) {
|
|
659
|
-
|
|
660
|
-
const tasks = await Promise.all(ids.map((id) => this.getTask(id)));
|
|
661
|
-
return tasks.filter((task) => Boolean(task)).filter((task) => task.status !== "deleted").sort((a, b) => a.createdAtMs - b.createdAtMs);
|
|
743
|
+
return await listTasksFromState(this.state, teamTaskIndexKey(teamId));
|
|
662
744
|
}
|
|
663
745
|
async claimDueRun(args) {
|
|
664
746
|
const ids = await getIndex(this.state, globalTaskIndexKey());
|
|
@@ -772,24 +854,11 @@ var PluginStateSchedulerStore = class {
|
|
|
772
854
|
).sort((a, b) => a.createdAtMs - b.createdAtMs || a.id.localeCompare(b.id)).at(0);
|
|
773
855
|
}
|
|
774
856
|
async getRun(runId) {
|
|
775
|
-
return await this.state
|
|
857
|
+
return await getRunFromState(this.state, runId);
|
|
776
858
|
}
|
|
777
859
|
async listIncompleteRuns() {
|
|
778
|
-
const
|
|
779
|
-
|
|
780
|
-
for (const taskId of ids) {
|
|
781
|
-
const active = await this.state.get(
|
|
782
|
-
activeRunKey(taskId)
|
|
783
|
-
);
|
|
784
|
-
if (typeof active?.runId !== "string") {
|
|
785
|
-
continue;
|
|
786
|
-
}
|
|
787
|
-
const run = await this.getRun(active.runId);
|
|
788
|
-
if (run && !isFinishedRun(run)) {
|
|
789
|
-
runs.push(run);
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
return runs;
|
|
860
|
+
const tasks = await this.listTasks();
|
|
861
|
+
return await listIncompleteRunsForTasksFromState(this.state, tasks);
|
|
793
862
|
}
|
|
794
863
|
async markRunDispatched(args) {
|
|
795
864
|
return await this.updateRun(
|
|
@@ -938,6 +1007,9 @@ var PluginStateSchedulerStore = class {
|
|
|
938
1007
|
function createSchedulerStore(state) {
|
|
939
1008
|
return new PluginStateSchedulerStore(state);
|
|
940
1009
|
}
|
|
1010
|
+
function createSchedulerOperationalStore(state) {
|
|
1011
|
+
return new PluginStateSchedulerOperationalStore(state);
|
|
1012
|
+
}
|
|
941
1013
|
|
|
942
1014
|
// src/schedule-tools.ts
|
|
943
1015
|
import { randomUUID } from "crypto";
|
|
@@ -969,15 +1041,15 @@ function requireActiveDestination(context) {
|
|
|
969
1041
|
};
|
|
970
1042
|
}
|
|
971
1043
|
function requireRequester(context) {
|
|
972
|
-
const userId = context.requester?.userId;
|
|
973
|
-
if (!userId) {
|
|
1044
|
+
const userId = context.requester?.userId?.trim();
|
|
1045
|
+
if (!userId || userId.toLowerCase() === "unknown") {
|
|
974
1046
|
throwToolInputError("No active Slack requester context is available.");
|
|
975
1047
|
}
|
|
976
|
-
return {
|
|
1048
|
+
return sanitizeScheduledTaskPrincipal({
|
|
977
1049
|
slackUserId: userId,
|
|
978
1050
|
...context.requester?.userName ? { userName: context.requester.userName } : {},
|
|
979
1051
|
...context.requester?.fullName ? { fullName: context.requester.fullName } : {}
|
|
980
|
-
};
|
|
1052
|
+
});
|
|
981
1053
|
}
|
|
982
1054
|
function tool(definition) {
|
|
983
1055
|
return definition;
|
|
@@ -1413,6 +1485,7 @@ function createSlackScheduleRunTaskNowTool(context) {
|
|
|
1413
1485
|
|
|
1414
1486
|
// src/plugin.ts
|
|
1415
1487
|
var SCHEDULER_HEARTBEAT_LIMIT = 10;
|
|
1488
|
+
var DASHBOARD_TABLE_LIMIT = 5;
|
|
1416
1489
|
function shouldSkipRun(task, run) {
|
|
1417
1490
|
if (task.status === "deleted") {
|
|
1418
1491
|
return `Scheduled task ${task.id} was deleted before the run started.`;
|
|
@@ -1531,6 +1604,156 @@ async function failClaimedRun(args) {
|
|
|
1531
1604
|
status: "failed"
|
|
1532
1605
|
});
|
|
1533
1606
|
}
|
|
1607
|
+
function formatCount(value) {
|
|
1608
|
+
return String(value);
|
|
1609
|
+
}
|
|
1610
|
+
function formatTimestamp(timestampMs) {
|
|
1611
|
+
return typeof timestampMs === "number" && Number.isFinite(timestampMs) ? new Date(timestampMs).toISOString() : "none";
|
|
1612
|
+
}
|
|
1613
|
+
function destinationLabel(destination) {
|
|
1614
|
+
if (destination.channelId.startsWith("D")) {
|
|
1615
|
+
return "Direct Message";
|
|
1616
|
+
}
|
|
1617
|
+
if (destination.channelId.startsWith("C")) {
|
|
1618
|
+
return `Public Channel ${destination.channelId}`;
|
|
1619
|
+
}
|
|
1620
|
+
if (destination.channelId.startsWith("G")) {
|
|
1621
|
+
return `Private Destination ${destination.channelId}`;
|
|
1622
|
+
}
|
|
1623
|
+
return destination.channelId;
|
|
1624
|
+
}
|
|
1625
|
+
function operationalAuthorLabel(author) {
|
|
1626
|
+
try {
|
|
1627
|
+
return scheduledTaskPrincipalLabel(author);
|
|
1628
|
+
} catch {
|
|
1629
|
+
return "Invalid Slack creator metadata";
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
function cadenceLabel(task) {
|
|
1633
|
+
if (task.schedule.kind === "one_off") {
|
|
1634
|
+
return "one-off";
|
|
1635
|
+
}
|
|
1636
|
+
return task.schedule.recurrence ? task.schedule.recurrence.frequency : "recurring";
|
|
1637
|
+
}
|
|
1638
|
+
function taskStatusCounts(tasks) {
|
|
1639
|
+
return tasks.reduce(
|
|
1640
|
+
(counts, task) => ({
|
|
1641
|
+
active: counts.active + (task.status === "active" ? 1 : 0),
|
|
1642
|
+
blocked: counts.blocked + (task.status === "blocked" ? 1 : 0),
|
|
1643
|
+
paused: counts.paused + (task.status === "paused" ? 1 : 0)
|
|
1644
|
+
}),
|
|
1645
|
+
{ active: 0, blocked: 0, paused: 0 }
|
|
1646
|
+
);
|
|
1647
|
+
}
|
|
1648
|
+
function isDue(task, nowMs) {
|
|
1649
|
+
return task.status === "active" && (typeof task.runNowAtMs === "number" && task.runNowAtMs <= nowMs || typeof task.nextRunAtMs === "number" && task.nextRunAtMs <= nowMs);
|
|
1650
|
+
}
|
|
1651
|
+
async function buildSchedulerOperationalReport(args) {
|
|
1652
|
+
const tasks = await args.store.listTasks();
|
|
1653
|
+
const incompleteRuns = await args.store.listIncompleteRunsForTasks(tasks);
|
|
1654
|
+
const taskById = new Map(tasks.map((task) => [task.id, task]));
|
|
1655
|
+
const counts = taskStatusCounts(tasks);
|
|
1656
|
+
const dueCount = tasks.filter((task) => isDue(task, args.nowMs)).length;
|
|
1657
|
+
const upcomingTasks = tasks.filter((task) => task.status === "active" && task.nextRunAtMs).sort((left, right) => left.nextRunAtMs - right.nextRunAtMs).slice(0, DASHBOARD_TABLE_LIMIT);
|
|
1658
|
+
const blockedTasks = tasks.filter((task) => task.status === "blocked").sort((left, right) => right.updatedAtMs - left.updatedAtMs).slice(0, DASHBOARD_TABLE_LIMIT);
|
|
1659
|
+
const runningCount = incompleteRuns.length;
|
|
1660
|
+
const runningRuns = incompleteRuns.sort((left, right) => right.claimedAtMs - left.claimedAtMs).slice(0, DASHBOARD_TABLE_LIMIT);
|
|
1661
|
+
return {
|
|
1662
|
+
title: "Scheduler",
|
|
1663
|
+
generatedAt: new Date(args.nowMs).toISOString(),
|
|
1664
|
+
metrics: [
|
|
1665
|
+
{
|
|
1666
|
+
label: "active",
|
|
1667
|
+
tone: counts.active > 0 ? "good" : "neutral",
|
|
1668
|
+
value: formatCount(counts.active)
|
|
1669
|
+
},
|
|
1670
|
+
{
|
|
1671
|
+
label: "blocked",
|
|
1672
|
+
tone: counts.blocked > 0 ? "danger" : "neutral",
|
|
1673
|
+
value: formatCount(counts.blocked)
|
|
1674
|
+
},
|
|
1675
|
+
{ label: "paused", value: formatCount(counts.paused) },
|
|
1676
|
+
{
|
|
1677
|
+
label: "due now",
|
|
1678
|
+
tone: dueCount > 0 ? "warning" : "neutral",
|
|
1679
|
+
value: formatCount(dueCount)
|
|
1680
|
+
},
|
|
1681
|
+
{
|
|
1682
|
+
label: "running",
|
|
1683
|
+
tone: runningCount > 0 ? "warning" : "neutral",
|
|
1684
|
+
value: formatCount(runningCount)
|
|
1685
|
+
}
|
|
1686
|
+
],
|
|
1687
|
+
recordSets: [
|
|
1688
|
+
{
|
|
1689
|
+
title: "Upcoming",
|
|
1690
|
+
emptyText: "No active scheduled tasks.",
|
|
1691
|
+
fields: [
|
|
1692
|
+
{ key: "task", label: "Task" },
|
|
1693
|
+
{ key: "author", label: "Author" },
|
|
1694
|
+
{ key: "destination", label: "Destination" },
|
|
1695
|
+
{ key: "nextRun", label: "Next Run" },
|
|
1696
|
+
{ key: "cadence", label: "Cadence" }
|
|
1697
|
+
],
|
|
1698
|
+
records: upcomingTasks.map((task) => ({
|
|
1699
|
+
id: task.id,
|
|
1700
|
+
values: {
|
|
1701
|
+
task: task.id,
|
|
1702
|
+
author: operationalAuthorLabel(task.createdBy),
|
|
1703
|
+
destination: destinationLabel(task.destination),
|
|
1704
|
+
nextRun: formatTimestamp(task.nextRunAtMs),
|
|
1705
|
+
cadence: cadenceLabel(task)
|
|
1706
|
+
}
|
|
1707
|
+
}))
|
|
1708
|
+
},
|
|
1709
|
+
{
|
|
1710
|
+
title: "Blocked",
|
|
1711
|
+
emptyText: "No blocked scheduled tasks.",
|
|
1712
|
+
fields: [
|
|
1713
|
+
{ key: "task", label: "Task" },
|
|
1714
|
+
{ key: "author", label: "Author" },
|
|
1715
|
+
{ key: "destination", label: "Destination" },
|
|
1716
|
+
{ key: "updated", label: "Updated" }
|
|
1717
|
+
],
|
|
1718
|
+
records: blockedTasks.map((task) => ({
|
|
1719
|
+
id: task.id,
|
|
1720
|
+
tone: "danger",
|
|
1721
|
+
values: {
|
|
1722
|
+
task: task.id,
|
|
1723
|
+
author: operationalAuthorLabel(task.createdBy),
|
|
1724
|
+
destination: destinationLabel(task.destination),
|
|
1725
|
+
updated: formatTimestamp(task.updatedAtMs)
|
|
1726
|
+
}
|
|
1727
|
+
}))
|
|
1728
|
+
},
|
|
1729
|
+
{
|
|
1730
|
+
title: "Running",
|
|
1731
|
+
emptyText: "No scheduler runs in flight.",
|
|
1732
|
+
fields: [
|
|
1733
|
+
{ key: "run", label: "Run" },
|
|
1734
|
+
{ key: "task", label: "Task" },
|
|
1735
|
+
{ key: "author", label: "Author" },
|
|
1736
|
+
{ key: "scheduledFor", label: "Scheduled For" },
|
|
1737
|
+
{ key: "status", label: "Status" }
|
|
1738
|
+
],
|
|
1739
|
+
records: runningRuns.map((run) => {
|
|
1740
|
+
const task = taskById.get(run.taskId);
|
|
1741
|
+
return {
|
|
1742
|
+
id: run.id,
|
|
1743
|
+
tone: run.status === "pending" ? "warning" : "neutral",
|
|
1744
|
+
values: {
|
|
1745
|
+
run: run.id,
|
|
1746
|
+
task: run.taskId,
|
|
1747
|
+
author: task ? operationalAuthorLabel(task.createdBy) : "Missing scheduled task",
|
|
1748
|
+
scheduledFor: formatTimestamp(run.scheduledForMs),
|
|
1749
|
+
status: run.status
|
|
1750
|
+
}
|
|
1751
|
+
};
|
|
1752
|
+
})
|
|
1753
|
+
}
|
|
1754
|
+
]
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1534
1757
|
function createSchedulerPlugin() {
|
|
1535
1758
|
return defineJuniorPlugin({
|
|
1536
1759
|
manifest: {
|
|
@@ -1650,6 +1873,12 @@ function createSchedulerPlugin() {
|
|
|
1650
1873
|
dispatchCount += 1;
|
|
1651
1874
|
}
|
|
1652
1875
|
return { dispatchCount };
|
|
1876
|
+
},
|
|
1877
|
+
async operationalReport(ctx) {
|
|
1878
|
+
return buildSchedulerOperationalReport({
|
|
1879
|
+
nowMs: ctx.nowMs,
|
|
1880
|
+
store: createSchedulerOperationalStore(ctx.state)
|
|
1881
|
+
});
|
|
1653
1882
|
}
|
|
1654
1883
|
}
|
|
1655
1884
|
});
|
package/dist/store.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentPluginState } from "@sentry/junior-plugin-api";
|
|
1
|
+
import type { AgentPluginReadState, AgentPluginState } from "@sentry/junior-plugin-api";
|
|
2
2
|
import type { ScheduledRun, ScheduledTask } from "./types";
|
|
3
3
|
export interface SchedulerStore {
|
|
4
4
|
claimDueRun(args: {
|
|
@@ -7,6 +7,7 @@ export interface SchedulerStore {
|
|
|
7
7
|
getRun(runId: string): Promise<ScheduledRun | undefined>;
|
|
8
8
|
getTask(taskId: string): Promise<ScheduledTask | undefined>;
|
|
9
9
|
listIncompleteRuns(): Promise<ScheduledRun[]>;
|
|
10
|
+
listTasks(): Promise<ScheduledTask[]>;
|
|
10
11
|
listTasksForTeam(teamId: string): Promise<ScheduledTask[]>;
|
|
11
12
|
markRunBlocked(args: {
|
|
12
13
|
completedAtMs: number;
|
|
@@ -45,5 +46,11 @@ export interface SchedulerStore {
|
|
|
45
46
|
status: "blocked" | "completed" | "failed";
|
|
46
47
|
}): Promise<void>;
|
|
47
48
|
}
|
|
49
|
+
export interface SchedulerOperationalStore {
|
|
50
|
+
listIncompleteRunsForTasks(tasks: ScheduledTask[]): Promise<ScheduledRun[]>;
|
|
51
|
+
listTasks(): Promise<ScheduledTask[]>;
|
|
52
|
+
}
|
|
48
53
|
/** Create a scheduler store backed by this plugin's durable state namespace. */
|
|
49
54
|
export declare function createSchedulerStore(state: AgentPluginState): SchedulerStore;
|
|
55
|
+
/** Create a read-only scheduler store for operational reporting. */
|
|
56
|
+
export declare function createSchedulerOperationalStore(state: AgentPluginReadState): SchedulerOperationalStore;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentry/junior-scheduler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.67.1",
|
|
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.
|
|
26
|
+
"@sentry/junior-plugin-api": "0.67.1"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^25.9.1",
|
package/src/identity.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ScheduledTaskPrincipal } from "./types";
|
|
2
|
+
|
|
3
|
+
const SLACK_USER_ID_PATTERN = /^[UW][A-Z0-9]{5,}$/;
|
|
4
|
+
|
|
5
|
+
function clean(value: string | undefined): string | undefined {
|
|
6
|
+
const normalized = value?.trim();
|
|
7
|
+
return normalized ? normalized : undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function cleanSlackUserId(value: string | undefined): string | undefined {
|
|
11
|
+
const normalized = clean(value);
|
|
12
|
+
return normalized && normalized.toLowerCase() !== "unknown"
|
|
13
|
+
? normalized
|
|
14
|
+
: undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function cleanDisplay(
|
|
18
|
+
value: string | undefined,
|
|
19
|
+
slackUserId: string,
|
|
20
|
+
): string | undefined {
|
|
21
|
+
const normalized = clean(value);
|
|
22
|
+
if (
|
|
23
|
+
!normalized ||
|
|
24
|
+
normalized.toLowerCase() === "unknown" ||
|
|
25
|
+
normalized === slackUserId
|
|
26
|
+
) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
return SLACK_USER_ID_PATTERN.test(normalized) ? undefined : normalized;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Keep scheduler creator metadata from promoting Slack IDs into display names. */
|
|
33
|
+
export function sanitizeScheduledTaskPrincipal(
|
|
34
|
+
principal: ScheduledTaskPrincipal,
|
|
35
|
+
): ScheduledTaskPrincipal {
|
|
36
|
+
const slackUserId = cleanSlackUserId(principal.slackUserId);
|
|
37
|
+
if (!slackUserId) {
|
|
38
|
+
throw new Error("Scheduled task principal requires a Slack user id");
|
|
39
|
+
}
|
|
40
|
+
const fullName = cleanDisplay(principal.fullName, slackUserId);
|
|
41
|
+
const userName = cleanDisplay(principal.userName, slackUserId);
|
|
42
|
+
return {
|
|
43
|
+
slackUserId,
|
|
44
|
+
...(fullName ? { fullName } : {}),
|
|
45
|
+
...(userName ? { userName } : {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Render scheduler creator metadata without inventing human profile fields. */
|
|
50
|
+
export function scheduledTaskPrincipalLabel(
|
|
51
|
+
principal: ScheduledTaskPrincipal,
|
|
52
|
+
): string {
|
|
53
|
+
const author = sanitizeScheduledTaskPrincipal(principal);
|
|
54
|
+
if (author.fullName && author.userName) {
|
|
55
|
+
return `${author.fullName} (@${author.userName})`;
|
|
56
|
+
}
|
|
57
|
+
if (author.fullName) {
|
|
58
|
+
return author.fullName;
|
|
59
|
+
}
|
|
60
|
+
if (author.userName) {
|
|
61
|
+
return `@${author.userName}`;
|
|
62
|
+
}
|
|
63
|
+
return `Slack User ${author.slackUserId}`;
|
|
64
|
+
}
|
package/src/plugin.ts
CHANGED
|
@@ -2,11 +2,22 @@ import {
|
|
|
2
2
|
defineJuniorPlugin,
|
|
3
3
|
type Dispatch,
|
|
4
4
|
type AgentPluginToolDefinition,
|
|
5
|
+
type PluginOperationalReportContent,
|
|
5
6
|
type ToolRegistrationHookContext,
|
|
6
7
|
} from "@sentry/junior-plugin-api";
|
|
7
8
|
import { buildScheduledTaskRunPrompt } from "./prompt";
|
|
8
|
-
import {
|
|
9
|
-
|
|
9
|
+
import {
|
|
10
|
+
createSchedulerOperationalStore,
|
|
11
|
+
createSchedulerStore,
|
|
12
|
+
type SchedulerOperationalStore,
|
|
13
|
+
type SchedulerStore,
|
|
14
|
+
} from "./store";
|
|
15
|
+
import { scheduledTaskPrincipalLabel } from "./identity";
|
|
16
|
+
import type {
|
|
17
|
+
ScheduledRun,
|
|
18
|
+
ScheduledTask,
|
|
19
|
+
ScheduledTaskPrincipal,
|
|
20
|
+
} from "./types";
|
|
10
21
|
import {
|
|
11
22
|
createSlackScheduleCreateTaskTool,
|
|
12
23
|
createSlackScheduleDeleteTaskTool,
|
|
@@ -17,6 +28,7 @@ import {
|
|
|
17
28
|
} from "./schedule-tools";
|
|
18
29
|
|
|
19
30
|
const SCHEDULER_HEARTBEAT_LIMIT = 10;
|
|
31
|
+
const DASHBOARD_TABLE_LIMIT = 5;
|
|
20
32
|
|
|
21
33
|
function shouldSkipRun(
|
|
22
34
|
task: ScheduledTask,
|
|
@@ -167,6 +179,186 @@ async function failClaimedRun(args: {
|
|
|
167
179
|
});
|
|
168
180
|
}
|
|
169
181
|
|
|
182
|
+
function formatCount(value: number): string {
|
|
183
|
+
return String(value);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function formatTimestamp(timestampMs: number | undefined): string {
|
|
187
|
+
return typeof timestampMs === "number" && Number.isFinite(timestampMs)
|
|
188
|
+
? new Date(timestampMs).toISOString()
|
|
189
|
+
: "none";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function destinationLabel(destination: ScheduledTask["destination"]): string {
|
|
193
|
+
if (destination.channelId.startsWith("D")) {
|
|
194
|
+
return "Direct Message";
|
|
195
|
+
}
|
|
196
|
+
if (destination.channelId.startsWith("C")) {
|
|
197
|
+
return `Public Channel ${destination.channelId}`;
|
|
198
|
+
}
|
|
199
|
+
if (destination.channelId.startsWith("G")) {
|
|
200
|
+
return `Private Destination ${destination.channelId}`;
|
|
201
|
+
}
|
|
202
|
+
return destination.channelId;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function operationalAuthorLabel(author: ScheduledTaskPrincipal): string {
|
|
206
|
+
try {
|
|
207
|
+
return scheduledTaskPrincipalLabel(author);
|
|
208
|
+
} catch {
|
|
209
|
+
return "Invalid Slack creator metadata";
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function cadenceLabel(task: ScheduledTask): string {
|
|
214
|
+
if (task.schedule.kind === "one_off") {
|
|
215
|
+
return "one-off";
|
|
216
|
+
}
|
|
217
|
+
return task.schedule.recurrence
|
|
218
|
+
? task.schedule.recurrence.frequency
|
|
219
|
+
: "recurring";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function taskStatusCounts(tasks: ScheduledTask[]) {
|
|
223
|
+
return tasks.reduce(
|
|
224
|
+
(counts, task) => ({
|
|
225
|
+
active: counts.active + (task.status === "active" ? 1 : 0),
|
|
226
|
+
blocked: counts.blocked + (task.status === "blocked" ? 1 : 0),
|
|
227
|
+
paused: counts.paused + (task.status === "paused" ? 1 : 0),
|
|
228
|
+
}),
|
|
229
|
+
{ active: 0, blocked: 0, paused: 0 },
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isDue(task: ScheduledTask, nowMs: number): boolean {
|
|
234
|
+
return (
|
|
235
|
+
task.status === "active" &&
|
|
236
|
+
((typeof task.runNowAtMs === "number" && task.runNowAtMs <= nowMs) ||
|
|
237
|
+
(typeof task.nextRunAtMs === "number" && task.nextRunAtMs <= nowMs))
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function buildSchedulerOperationalReport(args: {
|
|
242
|
+
nowMs: number;
|
|
243
|
+
store: SchedulerOperationalStore;
|
|
244
|
+
}): Promise<PluginOperationalReportContent> {
|
|
245
|
+
const tasks = await args.store.listTasks();
|
|
246
|
+
const incompleteRuns = await args.store.listIncompleteRunsForTasks(tasks);
|
|
247
|
+
const taskById = new Map(tasks.map((task) => [task.id, task]));
|
|
248
|
+
const counts = taskStatusCounts(tasks);
|
|
249
|
+
const dueCount = tasks.filter((task) => isDue(task, args.nowMs)).length;
|
|
250
|
+
const upcomingTasks = tasks
|
|
251
|
+
.filter((task) => task.status === "active" && task.nextRunAtMs)
|
|
252
|
+
.sort((left, right) => left.nextRunAtMs! - right.nextRunAtMs!)
|
|
253
|
+
.slice(0, DASHBOARD_TABLE_LIMIT);
|
|
254
|
+
const blockedTasks = tasks
|
|
255
|
+
.filter((task) => task.status === "blocked")
|
|
256
|
+
.sort((left, right) => right.updatedAtMs - left.updatedAtMs)
|
|
257
|
+
.slice(0, DASHBOARD_TABLE_LIMIT);
|
|
258
|
+
const runningCount = incompleteRuns.length;
|
|
259
|
+
const runningRuns = incompleteRuns
|
|
260
|
+
.sort((left, right) => right.claimedAtMs - left.claimedAtMs)
|
|
261
|
+
.slice(0, DASHBOARD_TABLE_LIMIT);
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
title: "Scheduler",
|
|
265
|
+
generatedAt: new Date(args.nowMs).toISOString(),
|
|
266
|
+
metrics: [
|
|
267
|
+
{
|
|
268
|
+
label: "active",
|
|
269
|
+
tone: counts.active > 0 ? "good" : "neutral",
|
|
270
|
+
value: formatCount(counts.active),
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
label: "blocked",
|
|
274
|
+
tone: counts.blocked > 0 ? "danger" : "neutral",
|
|
275
|
+
value: formatCount(counts.blocked),
|
|
276
|
+
},
|
|
277
|
+
{ label: "paused", value: formatCount(counts.paused) },
|
|
278
|
+
{
|
|
279
|
+
label: "due now",
|
|
280
|
+
tone: dueCount > 0 ? "warning" : "neutral",
|
|
281
|
+
value: formatCount(dueCount),
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
label: "running",
|
|
285
|
+
tone: runningCount > 0 ? "warning" : "neutral",
|
|
286
|
+
value: formatCount(runningCount),
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
recordSets: [
|
|
290
|
+
{
|
|
291
|
+
title: "Upcoming",
|
|
292
|
+
emptyText: "No active scheduled tasks.",
|
|
293
|
+
fields: [
|
|
294
|
+
{ key: "task", label: "Task" },
|
|
295
|
+
{ key: "author", label: "Author" },
|
|
296
|
+
{ key: "destination", label: "Destination" },
|
|
297
|
+
{ key: "nextRun", label: "Next Run" },
|
|
298
|
+
{ key: "cadence", label: "Cadence" },
|
|
299
|
+
],
|
|
300
|
+
records: upcomingTasks.map((task) => ({
|
|
301
|
+
id: task.id,
|
|
302
|
+
values: {
|
|
303
|
+
task: task.id,
|
|
304
|
+
author: operationalAuthorLabel(task.createdBy),
|
|
305
|
+
destination: destinationLabel(task.destination),
|
|
306
|
+
nextRun: formatTimestamp(task.nextRunAtMs),
|
|
307
|
+
cadence: cadenceLabel(task),
|
|
308
|
+
},
|
|
309
|
+
})),
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
title: "Blocked",
|
|
313
|
+
emptyText: "No blocked scheduled tasks.",
|
|
314
|
+
fields: [
|
|
315
|
+
{ key: "task", label: "Task" },
|
|
316
|
+
{ key: "author", label: "Author" },
|
|
317
|
+
{ key: "destination", label: "Destination" },
|
|
318
|
+
{ key: "updated", label: "Updated" },
|
|
319
|
+
],
|
|
320
|
+
records: blockedTasks.map((task) => ({
|
|
321
|
+
id: task.id,
|
|
322
|
+
tone: "danger",
|
|
323
|
+
values: {
|
|
324
|
+
task: task.id,
|
|
325
|
+
author: operationalAuthorLabel(task.createdBy),
|
|
326
|
+
destination: destinationLabel(task.destination),
|
|
327
|
+
updated: formatTimestamp(task.updatedAtMs),
|
|
328
|
+
},
|
|
329
|
+
})),
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
title: "Running",
|
|
333
|
+
emptyText: "No scheduler runs in flight.",
|
|
334
|
+
fields: [
|
|
335
|
+
{ key: "run", label: "Run" },
|
|
336
|
+
{ key: "task", label: "Task" },
|
|
337
|
+
{ key: "author", label: "Author" },
|
|
338
|
+
{ key: "scheduledFor", label: "Scheduled For" },
|
|
339
|
+
{ key: "status", label: "Status" },
|
|
340
|
+
],
|
|
341
|
+
records: runningRuns.map((run) => {
|
|
342
|
+
const task = taskById.get(run.taskId);
|
|
343
|
+
return {
|
|
344
|
+
id: run.id,
|
|
345
|
+
tone: run.status === "pending" ? "warning" : "neutral",
|
|
346
|
+
values: {
|
|
347
|
+
run: run.id,
|
|
348
|
+
task: run.taskId,
|
|
349
|
+
author: task
|
|
350
|
+
? operationalAuthorLabel(task.createdBy)
|
|
351
|
+
: "Missing scheduled task",
|
|
352
|
+
scheduledFor: formatTimestamp(run.scheduledForMs),
|
|
353
|
+
status: run.status,
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
}),
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
170
362
|
/** Create Junior's built-in trusted scheduler plugin. */
|
|
171
363
|
export function createSchedulerPlugin() {
|
|
172
364
|
return defineJuniorPlugin({
|
|
@@ -305,6 +497,12 @@ export function createSchedulerPlugin() {
|
|
|
305
497
|
|
|
306
498
|
return { dispatchCount };
|
|
307
499
|
},
|
|
500
|
+
async operationalReport(ctx) {
|
|
501
|
+
return buildSchedulerOperationalReport({
|
|
502
|
+
nowMs: ctx.nowMs,
|
|
503
|
+
store: createSchedulerOperationalStore(ctx.state),
|
|
504
|
+
});
|
|
505
|
+
},
|
|
308
506
|
},
|
|
309
507
|
});
|
|
310
508
|
}
|
package/src/prompt.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type ScheduledRun,
|
|
4
4
|
type ScheduledTask,
|
|
5
5
|
} from "./types";
|
|
6
|
+
import { sanitizeScheduledTaskPrincipal } from "./identity";
|
|
6
7
|
|
|
7
8
|
function escapeXml(value: string): string {
|
|
8
9
|
return value
|
|
@@ -35,7 +36,7 @@ export function buildScheduledTaskRunPrompt(args: {
|
|
|
35
36
|
}): string {
|
|
36
37
|
const { run, task } = args;
|
|
37
38
|
const destination = task.destination;
|
|
38
|
-
const creator = task.createdBy;
|
|
39
|
+
const creator = sanitizeScheduledTaskPrincipal(task.createdBy);
|
|
39
40
|
const executionActor = task.executionActor ?? SCHEDULED_TASK_SYSTEM_ACTOR;
|
|
40
41
|
if (!task.task.text?.trim()) {
|
|
41
42
|
throw new Error("Scheduled task text is required");
|
package/src/schedule-tools.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type AgentPluginToolDefinition,
|
|
8
8
|
} from "@sentry/junior-plugin-api";
|
|
9
9
|
import { buildCalendarRecurrence, parseScheduleTimestamp } from "./cadence";
|
|
10
|
+
import { sanitizeScheduledTaskPrincipal } from "./identity";
|
|
10
11
|
import { createSchedulerStore } from "./store";
|
|
11
12
|
import { SCHEDULED_TASK_SYSTEM_ACTOR } from "./types";
|
|
12
13
|
import type {
|
|
@@ -68,12 +69,12 @@ function requireActiveDestination(
|
|
|
68
69
|
function requireRequester(
|
|
69
70
|
context: SchedulerToolContext,
|
|
70
71
|
): ScheduledTaskPrincipal {
|
|
71
|
-
const userId = context.requester?.userId;
|
|
72
|
-
if (!userId) {
|
|
72
|
+
const userId = context.requester?.userId?.trim();
|
|
73
|
+
if (!userId || userId.toLowerCase() === "unknown") {
|
|
73
74
|
throwToolInputError("No active Slack requester context is available.");
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
return {
|
|
77
|
+
return sanitizeScheduledTaskPrincipal({
|
|
77
78
|
slackUserId: userId,
|
|
78
79
|
...(context.requester?.userName
|
|
79
80
|
? { userName: context.requester.userName }
|
|
@@ -81,7 +82,7 @@ function requireRequester(
|
|
|
81
82
|
...(context.requester?.fullName
|
|
82
83
|
? { fullName: context.requester.fullName }
|
|
83
84
|
: {}),
|
|
84
|
-
};
|
|
85
|
+
});
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
function tool<TInput = any>(
|
package/src/store.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AgentPluginReadState,
|
|
3
|
+
AgentPluginState,
|
|
4
|
+
} from "@sentry/junior-plugin-api";
|
|
2
5
|
import { getNextRunAtMs } from "./cadence";
|
|
3
6
|
import type { ScheduledRun, ScheduledTask } from "./types";
|
|
4
7
|
|
|
@@ -15,6 +18,7 @@ export interface SchedulerStore {
|
|
|
15
18
|
getRun(runId: string): Promise<ScheduledRun | undefined>;
|
|
16
19
|
getTask(taskId: string): Promise<ScheduledTask | undefined>;
|
|
17
20
|
listIncompleteRuns(): Promise<ScheduledRun[]>;
|
|
21
|
+
listTasks(): Promise<ScheduledTask[]>;
|
|
18
22
|
listTasksForTeam(teamId: string): Promise<ScheduledTask[]>;
|
|
19
23
|
markRunBlocked(args: {
|
|
20
24
|
completedAtMs: number;
|
|
@@ -54,6 +58,11 @@ export interface SchedulerStore {
|
|
|
54
58
|
}): Promise<void>;
|
|
55
59
|
}
|
|
56
60
|
|
|
61
|
+
export interface SchedulerOperationalStore {
|
|
62
|
+
listIncompleteRunsForTasks(tasks: ScheduledTask[]): Promise<ScheduledRun[]>;
|
|
63
|
+
listTasks(): Promise<ScheduledTask[]>;
|
|
64
|
+
}
|
|
65
|
+
|
|
57
66
|
function taskKey(taskId: string): string {
|
|
58
67
|
return `${SCHEDULER_KEY_PREFIX}:task:${taskId}`;
|
|
59
68
|
}
|
|
@@ -139,7 +148,7 @@ async function removeFromIndex(
|
|
|
139
148
|
}
|
|
140
149
|
|
|
141
150
|
async function getIndex(
|
|
142
|
-
state:
|
|
151
|
+
state: AgentPluginReadState,
|
|
143
152
|
key: string,
|
|
144
153
|
): Promise<string[]> {
|
|
145
154
|
const values = (await state.get<string[]>(key)) ?? [];
|
|
@@ -346,6 +355,68 @@ function canFinishRun(
|
|
|
346
355
|
return run.status === "running" && run.startedAtMs === startedAtMs;
|
|
347
356
|
}
|
|
348
357
|
|
|
358
|
+
async function getTaskFromState(
|
|
359
|
+
state: AgentPluginReadState,
|
|
360
|
+
taskId: string,
|
|
361
|
+
): Promise<ScheduledTask | undefined> {
|
|
362
|
+
return (await state.get<ScheduledTask>(taskKey(taskId))) ?? undefined;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function listTasksFromState(
|
|
366
|
+
state: AgentPluginReadState,
|
|
367
|
+
indexKey: string,
|
|
368
|
+
): Promise<ScheduledTask[]> {
|
|
369
|
+
const ids = await getIndex(state, indexKey);
|
|
370
|
+
const tasks = await Promise.all(ids.map((id) => getTaskFromState(state, id)));
|
|
371
|
+
return tasks
|
|
372
|
+
.filter((task): task is ScheduledTask => Boolean(task))
|
|
373
|
+
.filter((task) => task.status !== "deleted")
|
|
374
|
+
.sort((a, b) => a.createdAtMs - b.createdAtMs);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function getRunFromState(
|
|
378
|
+
state: AgentPluginReadState,
|
|
379
|
+
runId: string,
|
|
380
|
+
): Promise<ScheduledRun | undefined> {
|
|
381
|
+
return (await state.get<ScheduledRun>(runKey(runId))) ?? undefined;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function listIncompleteRunsForTasksFromState(
|
|
385
|
+
state: AgentPluginReadState,
|
|
386
|
+
tasks: ScheduledTask[],
|
|
387
|
+
): Promise<ScheduledRun[]> {
|
|
388
|
+
const runs: ScheduledRun[] = [];
|
|
389
|
+
for (const task of tasks) {
|
|
390
|
+
const active = await state.get<{ runId?: unknown }>(activeRunKey(task.id));
|
|
391
|
+
if (typeof active?.runId !== "string") {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
const run = await getRunFromState(state, active.runId);
|
|
395
|
+
if (run && !isFinishedRun(run)) {
|
|
396
|
+
runs.push(run);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return runs;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
class PluginStateSchedulerOperationalStore implements SchedulerOperationalStore {
|
|
403
|
+
private readonly state: AgentPluginReadState;
|
|
404
|
+
|
|
405
|
+
constructor(state: AgentPluginReadState) {
|
|
406
|
+
this.state = state;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async listTasks(): Promise<ScheduledTask[]> {
|
|
410
|
+
return await listTasksFromState(this.state, globalTaskIndexKey());
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async listIncompleteRunsForTasks(
|
|
414
|
+
tasks: ScheduledTask[],
|
|
415
|
+
): Promise<ScheduledRun[]> {
|
|
416
|
+
return await listIncompleteRunsForTasksFromState(this.state, tasks);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
349
420
|
class PluginStateSchedulerStore implements SchedulerStore {
|
|
350
421
|
private readonly state: AgentPluginState;
|
|
351
422
|
|
|
@@ -408,16 +479,15 @@ class PluginStateSchedulerStore implements SchedulerStore {
|
|
|
408
479
|
}
|
|
409
480
|
|
|
410
481
|
async getTask(taskId: string): Promise<ScheduledTask | undefined> {
|
|
411
|
-
return
|
|
482
|
+
return await getTaskFromState(this.state, taskId);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async listTasks(): Promise<ScheduledTask[]> {
|
|
486
|
+
return await listTasksFromState(this.state, globalTaskIndexKey());
|
|
412
487
|
}
|
|
413
488
|
|
|
414
489
|
async listTasksForTeam(teamId: string): Promise<ScheduledTask[]> {
|
|
415
|
-
|
|
416
|
-
const tasks = await Promise.all(ids.map((id) => this.getTask(id)));
|
|
417
|
-
return tasks
|
|
418
|
-
.filter((task): task is ScheduledTask => Boolean(task))
|
|
419
|
-
.filter((task) => task.status !== "deleted")
|
|
420
|
-
.sort((a, b) => a.createdAtMs - b.createdAtMs);
|
|
490
|
+
return await listTasksFromState(this.state, teamTaskIndexKey(teamId));
|
|
421
491
|
}
|
|
422
492
|
|
|
423
493
|
async claimDueRun(args: {
|
|
@@ -576,25 +646,12 @@ class PluginStateSchedulerStore implements SchedulerStore {
|
|
|
576
646
|
}
|
|
577
647
|
|
|
578
648
|
async getRun(runId: string): Promise<ScheduledRun | undefined> {
|
|
579
|
-
return
|
|
649
|
+
return await getRunFromState(this.state, runId);
|
|
580
650
|
}
|
|
581
651
|
|
|
582
652
|
async listIncompleteRuns(): Promise<ScheduledRun[]> {
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
for (const taskId of ids) {
|
|
586
|
-
const active = await this.state.get<{ runId?: unknown }>(
|
|
587
|
-
activeRunKey(taskId),
|
|
588
|
-
);
|
|
589
|
-
if (typeof active?.runId !== "string") {
|
|
590
|
-
continue;
|
|
591
|
-
}
|
|
592
|
-
const run = await this.getRun(active.runId);
|
|
593
|
-
if (run && !isFinishedRun(run)) {
|
|
594
|
-
runs.push(run);
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
return runs;
|
|
653
|
+
const tasks = await this.listTasks();
|
|
654
|
+
return await listIncompleteRunsForTasksFromState(this.state, tasks);
|
|
598
655
|
}
|
|
599
656
|
|
|
600
657
|
async markRunDispatched(args: {
|
|
@@ -817,3 +874,10 @@ class PluginStateSchedulerStore implements SchedulerStore {
|
|
|
817
874
|
export function createSchedulerStore(state: AgentPluginState): SchedulerStore {
|
|
818
875
|
return new PluginStateSchedulerStore(state);
|
|
819
876
|
}
|
|
877
|
+
|
|
878
|
+
/** Create a read-only scheduler store for operational reporting. */
|
|
879
|
+
export function createSchedulerOperationalStore(
|
|
880
|
+
state: AgentPluginReadState,
|
|
881
|
+
): SchedulerOperationalStore {
|
|
882
|
+
return new PluginStateSchedulerOperationalStore(state);
|
|
883
|
+
}
|