teamcopilot 0.3.6 → 0.4.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/chat/index.js +104 -81
- package/dist/cronjob/index.js +2 -0
- package/dist/cronjobs/index.js +822 -0
- package/dist/cronjobs/scheduler.js +936 -0
- package/dist/frontend/assets/{cssMode-BRVRAYCz.js → cssMode-Cqdl5sUM.js} +1 -1
- package/dist/frontend/assets/{freemarker2-B5FvHwsO.js → freemarker2-ykAhuplU.js} +1 -1
- package/dist/frontend/assets/{handlebars-DWX2asql.js → handlebars-DX_JwRM8.js} +1 -1
- package/dist/frontend/assets/{html-BEBxxD9G.js → html-Bi_zOcbU.js} +1 -1
- package/dist/frontend/assets/{htmlMode-B2LbPTwC.js → htmlMode-CkAUoAah.js} +1 -1
- package/dist/frontend/assets/index-Ba9bElZm.css +1 -0
- package/dist/frontend/assets/{index-D3TE04C5.js → index-Cgozj4fx.js} +245 -242
- package/dist/frontend/assets/{javascript-Bh4JwoPV.js → javascript-D3Rjwp97.js} +1 -1
- package/dist/frontend/assets/{jsonMode-7j-aplXT.js → jsonMode-K4i6LjP2.js} +1 -1
- package/dist/frontend/assets/{liquid-BP4OxkO7.js → liquid-D8F4-sAz.js} +1 -1
- package/dist/frontend/assets/{mdx-C1OIcGbY.js → mdx-C2xw8PNz.js} +1 -1
- package/dist/frontend/assets/{python-BO8Wy5jz.js → python-CqTGfu2v.js} +1 -1
- package/dist/frontend/assets/{razor-BDtqXvAH.js → razor-DFSsPzdZ.js} +1 -1
- package/dist/frontend/assets/{tsMode-D22HcCuX.js → tsMode-BkLQEtPb.js} +1 -1
- package/dist/frontend/assets/{typescript-CagwEzRw.js → typescript-CE_GQ-M1.js} +1 -1
- package/dist/frontend/assets/{xml-fE5sGZ5z.js → xml-CGjMtNcA.js} +1 -1
- package/dist/frontend/assets/{yaml-CZMoG4WG.js → yaml-Zju9kuFB.js} +1 -1
- package/dist/frontend/index.html +2 -2
- package/dist/index.js +3 -0
- package/dist/types/cronjob.js +2 -0
- package/dist/utils/chat-prompt-context.js +65 -0
- package/dist/utils/chat-session.js +12 -0
- package/dist/utils/index.js +27 -0
- package/dist/utils/workflow-interruption.js +5 -2
- package/dist/utils/workflow-run-validation.js +25 -0
- package/dist/utils/workspace-sync.js +17 -0
- package/dist/workflows/index.js +24 -25
- package/dist/workspace_files/.opencode/plugins/apply-patch-session-diff.ts +2 -2
- package/dist/workspace_files/.opencode/plugins/askCronjobUser.ts +106 -0
- package/dist/workspace_files/.opencode/plugins/manageCronjobTodos.ts +190 -0
- package/dist/workspace_files/.opencode/plugins/manageCronjobs.ts +376 -0
- package/dist/workspace_files/.opencode/plugins/markCronjobCompleted.ts +107 -0
- package/dist/workspace_files/.opencode/plugins/markCronjobFailed.ts +107 -0
- package/dist/workspace_files/AGENTS.md +51 -1
- package/package.json +1 -1
- package/prisma/generated/client/edge.js +50 -3
- package/prisma/generated/client/index-browser.js +47 -0
- package/prisma/generated/client/index.d.ts +13918 -7530
- package/prisma/generated/client/index.js +50 -3
- package/prisma/generated/client/package.json +1 -1
- package/prisma/generated/client/schema.prisma +72 -1
- package/prisma/generated/client/wasm.js +50 -3
- package/prisma/migrations/20260508050030_add_cronjobs/migration.sql +78 -0
- package/prisma/migrations/20260508093158_add_structured_cronjob_schedules/migration.sql +23 -0
- package/prisma/migrations/20260508105129_add_cronjob_targets/migration.sql +50 -0
- package/prisma/migrations/20260509044545_flatten_cronjob_schema/migration.sql +88 -0
- package/prisma/migrations/20260509052232_simplify_cronjob_schedule_storage/migration.sql +42 -0
- package/prisma/migrations/20260509054000_remove_chat_session_source_add_cronjob_run_indexes/migration.sql +28 -0
- package/prisma/migrations/20260509061000_cascade_cronjob_run_links/migration.sql +29 -0
- package/prisma/migrations/20260513073541_add_cronjob_run_todos/migration.sql +18 -0
- package/prisma/migrations/20260513133021_add_cronjob_user_response_wait/migration.sql +29 -0
- package/prisma/migrations/20260513135733_add_cronjob_user_handoff_state/migration.sql +30 -0
- package/prisma/migrations/20260513142511_drop_awaiting_user_response/migration.sql +35 -0
- package/prisma/migrations/20260514032204_simplify_cronjob_run_lifecycle/migration.sql +34 -0
- package/prisma/migrations/20260514043000_clear_cronjob_run_history/migration.sql +6 -0
- package/prisma/migrations/20260515094618_add_todo_list_version/migration.sql +32 -0
- package/prisma/migrations/20260516082714_add_cronjob_monitor_timeout/migration.sql +38 -0
- package/prisma/migrations/20260516083452_allow_decimal_cronjob_timeout/migration.sql +37 -0
- package/prisma/migrations/20260516084455_add_cronjob_timeout_defaults/migration.sql +31 -0
- package/prisma/schema.prisma +71 -1
- package/dist/frontend/assets/index-D1Hcz_bo.css +0 -1
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const express_1 = __importDefault(require("express"));
|
|
7
|
+
const client_1 = __importDefault(require("../prisma/client"));
|
|
8
|
+
const client_2 = require("../../prisma/generated/client");
|
|
9
|
+
const utils_1 = require("../utils");
|
|
10
|
+
const scheduler_1 = require("./scheduler");
|
|
11
|
+
const router = express_1.default.Router({ mergeParams: true });
|
|
12
|
+
function nowMs() {
|
|
13
|
+
return BigInt(Date.now());
|
|
14
|
+
}
|
|
15
|
+
function assertNonEmptyString(value, label) {
|
|
16
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
17
|
+
throw {
|
|
18
|
+
status: 400,
|
|
19
|
+
message: `${label} is required`
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return value.trim();
|
|
23
|
+
}
|
|
24
|
+
function assertBoolean(value, label) {
|
|
25
|
+
if (typeof value !== "boolean") {
|
|
26
|
+
throw {
|
|
27
|
+
status: 400,
|
|
28
|
+
message: `${label} must be a boolean`
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
function assertStringArray(value, label) {
|
|
34
|
+
if (!Array.isArray(value)) {
|
|
35
|
+
throw {
|
|
36
|
+
status: 400,
|
|
37
|
+
message: `${label} must be an array of non-empty strings`
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const items = value.map((item) => assertNonEmptyString(item, label));
|
|
41
|
+
if (items.length === 0) {
|
|
42
|
+
throw {
|
|
43
|
+
status: 400,
|
|
44
|
+
message: `${label} must contain at least one item`
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return items;
|
|
48
|
+
}
|
|
49
|
+
function assertInsertIndex(value) {
|
|
50
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
51
|
+
throw {
|
|
52
|
+
status: 400,
|
|
53
|
+
message: "index is required and must be a non-negative integer"
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
function assertTodoListVersion(value) {
|
|
59
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
60
|
+
throw {
|
|
61
|
+
status: 400,
|
|
62
|
+
message: "todo_list_version is required and must be a non-negative integer"
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
function hasRequestField(body, field) {
|
|
68
|
+
return typeof body === "object" && body !== null && Object.prototype.hasOwnProperty.call(body, field);
|
|
69
|
+
}
|
|
70
|
+
function throwCronjobNameConflictIfNeeded(err) {
|
|
71
|
+
if (err instanceof client_2.Prisma.PrismaClientKnownRequestError && err.code === "P2002") {
|
|
72
|
+
throw {
|
|
73
|
+
status: 409,
|
|
74
|
+
message: "A cronjob with this name already exists."
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
function serializeCronjob(cronjob) {
|
|
80
|
+
const schedule = {
|
|
81
|
+
cron_expression: cronjob.cron_expression,
|
|
82
|
+
timezone: cronjob.timezone,
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
id: cronjob.id,
|
|
86
|
+
name: cronjob.name,
|
|
87
|
+
prompt: cronjob.prompt ?? "",
|
|
88
|
+
enabled: cronjob.enabled,
|
|
89
|
+
allow_workflow_runs_without_permission: cronjob.prompt_allow_workflow_runs_without_permission ?? true,
|
|
90
|
+
target: {
|
|
91
|
+
target_type: cronjob.target_type,
|
|
92
|
+
prompt: cronjob.prompt,
|
|
93
|
+
prompt_allow_workflow_runs_without_permission: cronjob.prompt_allow_workflow_runs_without_permission,
|
|
94
|
+
workflow_slug: cronjob.workflow_slug,
|
|
95
|
+
workflow_inputs: cronjob.workflow_input_json ? JSON.parse(cronjob.workflow_input_json) : null,
|
|
96
|
+
},
|
|
97
|
+
created_at: cronjob.created_at,
|
|
98
|
+
updated_at: cronjob.updated_at,
|
|
99
|
+
schedule: {
|
|
100
|
+
cron_expression: schedule.cron_expression,
|
|
101
|
+
timezone: schedule.timezone,
|
|
102
|
+
effective_cron_expression: schedule.cron_expression,
|
|
103
|
+
},
|
|
104
|
+
monitor_timeout_value: cronjob.monitor_timeout_value,
|
|
105
|
+
monitor_timeout_unit: cronjob.monitor_timeout_unit,
|
|
106
|
+
next_run_at: cronjob.enabled ? (0, scheduler_1.getNextRunAt)(schedule) : null,
|
|
107
|
+
is_running: cronjob.is_running === true,
|
|
108
|
+
current_run_id: cronjob.current_run_id ?? null,
|
|
109
|
+
current_workflow_run_id: cronjob.current_workflow_run_id ?? null,
|
|
110
|
+
latest_run: cronjob.runs?.[0]
|
|
111
|
+
? {
|
|
112
|
+
...cronjob.runs[0],
|
|
113
|
+
target_type_snapshot: cronjob.runs[0].workflow_run_id ? "workflow" : cronjob.target_type,
|
|
114
|
+
}
|
|
115
|
+
: null,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function serializeRun(run) {
|
|
119
|
+
const targetType = run.workflow_run_id ? "workflow" : run.cronjob?.target_type ?? "prompt";
|
|
120
|
+
const workflowInputJson = run.workflowRun?.args ?? run.cronjob?.workflow_input_json ?? null;
|
|
121
|
+
return {
|
|
122
|
+
id: run.id,
|
|
123
|
+
cronjob_id: run.cronjob_id,
|
|
124
|
+
status: run.status,
|
|
125
|
+
started_at: run.started_at,
|
|
126
|
+
completed_at: run.completed_at,
|
|
127
|
+
target_type_snapshot: targetType,
|
|
128
|
+
prompt_snapshot: targetType === "prompt" ? run.cronjob?.prompt ?? null : null,
|
|
129
|
+
workflow_slug_snapshot: run.workflowRun?.workflow_slug ?? run.cronjob?.workflow_slug ?? null,
|
|
130
|
+
workflow_input_snapshot: workflowInputJson ? JSON.parse(workflowInputJson) : null,
|
|
131
|
+
workflow_run_id: run.workflow_run_id,
|
|
132
|
+
summary: run.summary,
|
|
133
|
+
session_id: run.session_id,
|
|
134
|
+
opencode_session_id: run.opencode_session_id,
|
|
135
|
+
error_message: run.error_message,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async function getPromptCronjobRun(client, opencodeSessionId) {
|
|
139
|
+
const runs = await client.cronjob_runs.findMany({
|
|
140
|
+
where: {
|
|
141
|
+
opencode_session_id: opencodeSessionId,
|
|
142
|
+
cronjob: { target_type: "prompt" },
|
|
143
|
+
},
|
|
144
|
+
select: { id: true, status: true, session_id: true, todo_list_version: true },
|
|
145
|
+
});
|
|
146
|
+
if (runs.length === 0) {
|
|
147
|
+
throw {
|
|
148
|
+
status: 404,
|
|
149
|
+
message: "This is not a prompt cronjob session."
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (runs.length > 1) {
|
|
153
|
+
throw {
|
|
154
|
+
status: 500,
|
|
155
|
+
message: "Invariant violation: multiple prompt cronjob runs share one OpenCode session."
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return runs[0];
|
|
159
|
+
}
|
|
160
|
+
function assertActivePromptCronjobRun(run) {
|
|
161
|
+
if (run.status === "running" || run.status === "paused")
|
|
162
|
+
return;
|
|
163
|
+
throw {
|
|
164
|
+
status: 400,
|
|
165
|
+
message: `Cronjob session is already finished. Current state is: ${run.status}`
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
async function requireActivePromptCronjobRun(opencodeSessionId) {
|
|
169
|
+
const run = await getPromptCronjobRun(client_1.default, opencodeSessionId);
|
|
170
|
+
assertActivePromptCronjobRun(run);
|
|
171
|
+
return run;
|
|
172
|
+
}
|
|
173
|
+
function serializeCronjobTodo(todo) {
|
|
174
|
+
return {
|
|
175
|
+
id: todo.id,
|
|
176
|
+
run_id: todo.run_id,
|
|
177
|
+
content: todo.content,
|
|
178
|
+
status: todo.status,
|
|
179
|
+
position: todo.position,
|
|
180
|
+
completionSummary: todo.summary,
|
|
181
|
+
created_at: todo.created_at,
|
|
182
|
+
completed_at: todo.completed_at,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
async function getActiveCronjobTodos(runId) {
|
|
186
|
+
return client_1.default.cronjob_run_todos.findMany({
|
|
187
|
+
where: {
|
|
188
|
+
run_id: runId,
|
|
189
|
+
status: { not: "completed" },
|
|
190
|
+
},
|
|
191
|
+
orderBy: [
|
|
192
|
+
{ position: "asc" },
|
|
193
|
+
{ created_at: "asc" },
|
|
194
|
+
{ id: "asc" },
|
|
195
|
+
],
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
async function getCurrentCronjobTodoRecord(runId) {
|
|
199
|
+
return client_1.default.cronjob_run_todos.findFirst({
|
|
200
|
+
where: {
|
|
201
|
+
run_id: runId,
|
|
202
|
+
status: "in_progress",
|
|
203
|
+
},
|
|
204
|
+
orderBy: [
|
|
205
|
+
{ position: "asc" },
|
|
206
|
+
{ created_at: "asc" },
|
|
207
|
+
{ id: "asc" },
|
|
208
|
+
],
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
async function normalizeActiveCronjobTodoPositions(tx, runId) {
|
|
212
|
+
const activeTodos = await tx.cronjob_run_todos.findMany({
|
|
213
|
+
where: {
|
|
214
|
+
run_id: runId,
|
|
215
|
+
status: { not: "completed" },
|
|
216
|
+
},
|
|
217
|
+
orderBy: [
|
|
218
|
+
{ position: "asc" },
|
|
219
|
+
{ created_at: "asc" },
|
|
220
|
+
{ id: "asc" },
|
|
221
|
+
],
|
|
222
|
+
});
|
|
223
|
+
await Promise.all(activeTodos.map((todo, position) => (tx.cronjob_run_todos.update({
|
|
224
|
+
where: { id: todo.id },
|
|
225
|
+
data: { position },
|
|
226
|
+
}))));
|
|
227
|
+
return activeTodos.map((todo, position) => ({
|
|
228
|
+
...todo,
|
|
229
|
+
position,
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
router.post("/runs/ask-user-current", (0, utils_1.apiHandler)(async (req, res) => {
|
|
233
|
+
if (!req.opencode_session_id) {
|
|
234
|
+
throw {
|
|
235
|
+
status: 400,
|
|
236
|
+
message: "This endpoint requires an opencode session token"
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const message = assertNonEmptyString(req.body?.message, "message");
|
|
240
|
+
const run = await requireActivePromptCronjobRun(req.opencode_session_id);
|
|
241
|
+
if (!run.session_id) {
|
|
242
|
+
throw {
|
|
243
|
+
status: 400,
|
|
244
|
+
message: "Cronjob run does not have an AI chat session."
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const now = Number(nowMs());
|
|
248
|
+
if (run.status === "running") {
|
|
249
|
+
await client_1.default.$transaction([
|
|
250
|
+
client_1.default.chat_sessions.update({
|
|
251
|
+
where: { id: run.session_id },
|
|
252
|
+
data: {
|
|
253
|
+
visible_to_user: true,
|
|
254
|
+
updated_at: now,
|
|
255
|
+
},
|
|
256
|
+
}),
|
|
257
|
+
client_1.default.cronjob_runs.update({
|
|
258
|
+
where: { id: run.id },
|
|
259
|
+
data: { status: "paused" },
|
|
260
|
+
}),
|
|
261
|
+
]);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
await client_1.default.chat_sessions.update({
|
|
265
|
+
where: { id: run.session_id },
|
|
266
|
+
data: {
|
|
267
|
+
visible_to_user: true,
|
|
268
|
+
updated_at: now,
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
res.json({ success: true, message });
|
|
273
|
+
}, true));
|
|
274
|
+
router.get("/runs/todos/not-completed", (0, utils_1.apiHandler)(async (req, res) => {
|
|
275
|
+
if (!req.opencode_session_id) {
|
|
276
|
+
throw {
|
|
277
|
+
status: 400,
|
|
278
|
+
message: "This endpoint requires an opencode session token"
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
const run = await requireActivePromptCronjobRun(req.opencode_session_id);
|
|
282
|
+
const todos = await getActiveCronjobTodos(run.id);
|
|
283
|
+
res.json({ todo_list_version: run.todo_list_version, todos: todos.map(serializeCronjobTodo) });
|
|
284
|
+
}, true));
|
|
285
|
+
router.get("/", (0, utils_1.apiHandler)(async (req, res) => {
|
|
286
|
+
const cronjobs = await client_1.default.cronjobs.findMany({
|
|
287
|
+
where: { user_id: req.userId },
|
|
288
|
+
orderBy: { updated_at: "desc" },
|
|
289
|
+
include: {
|
|
290
|
+
runs: {
|
|
291
|
+
orderBy: { started_at: "desc" },
|
|
292
|
+
take: 1,
|
|
293
|
+
select: {
|
|
294
|
+
id: true,
|
|
295
|
+
status: true,
|
|
296
|
+
started_at: true,
|
|
297
|
+
completed_at: true,
|
|
298
|
+
workflow_run_id: true,
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
const activeRuns = await client_1.default.cronjob_runs.findMany({
|
|
304
|
+
where: {
|
|
305
|
+
cronjob_id: { in: cronjobs.map((cronjob) => cronjob.id) },
|
|
306
|
+
status: { in: ["running", "paused"] },
|
|
307
|
+
},
|
|
308
|
+
select: { id: true, cronjob_id: true, workflow_run_id: true },
|
|
309
|
+
});
|
|
310
|
+
const activeCronjobIds = new Set(activeRuns.map((run) => run.cronjob_id));
|
|
311
|
+
const activeRunIdByCronjobId = new Map(activeRuns.map((run) => [run.cronjob_id, run.id]));
|
|
312
|
+
const activeWorkflowRunIdByCronjobId = new Map(activeRuns.map((run) => [run.cronjob_id, run.workflow_run_id]));
|
|
313
|
+
res.json({
|
|
314
|
+
cronjobs: cronjobs.map((cronjob) => serializeCronjob({
|
|
315
|
+
...cronjob,
|
|
316
|
+
is_running: activeCronjobIds.has(cronjob.id),
|
|
317
|
+
current_run_id: activeRunIdByCronjobId.get(cronjob.id) ?? null,
|
|
318
|
+
current_workflow_run_id: activeWorkflowRunIdByCronjobId.get(cronjob.id) ?? null,
|
|
319
|
+
}))
|
|
320
|
+
});
|
|
321
|
+
}, true));
|
|
322
|
+
router.post("/", (0, utils_1.apiHandler)(async (req, res) => {
|
|
323
|
+
const name = assertNonEmptyString(req.body?.name, "name");
|
|
324
|
+
const target = await (0, scheduler_1.validateCronjobTarget)({
|
|
325
|
+
target_type: req.body?.target_type,
|
|
326
|
+
prompt: req.body?.prompt,
|
|
327
|
+
allow_workflow_runs_without_permission: req.body?.allow_workflow_runs_without_permission,
|
|
328
|
+
workflow_slug: req.body?.workflow_slug,
|
|
329
|
+
workflow_inputs: req.body?.workflow_inputs,
|
|
330
|
+
}, req.userId);
|
|
331
|
+
const schedule = (0, scheduler_1.validateCronjobSchedule)({
|
|
332
|
+
cron_expression: req.body?.cron_expression,
|
|
333
|
+
timezone: req.body?.timezone,
|
|
334
|
+
});
|
|
335
|
+
const monitorTimeout = (0, scheduler_1.validateCronjobMonitorTimeout)({
|
|
336
|
+
monitor_timeout_value: req.body?.monitor_timeout_value,
|
|
337
|
+
monitor_timeout_unit: req.body?.monitor_timeout_unit,
|
|
338
|
+
});
|
|
339
|
+
const now = nowMs();
|
|
340
|
+
const enabled = assertBoolean(req.body?.enabled, "enabled");
|
|
341
|
+
const cronjob = await client_1.default.cronjobs.create({
|
|
342
|
+
data: {
|
|
343
|
+
user_id: req.userId,
|
|
344
|
+
name,
|
|
345
|
+
enabled,
|
|
346
|
+
target_type: target.targetType,
|
|
347
|
+
prompt: target.prompt,
|
|
348
|
+
prompt_allow_workflow_runs_without_permission: target.promptAllowWorkflowRunsWithoutPermission,
|
|
349
|
+
workflow_slug: target.workflowSlug,
|
|
350
|
+
workflow_input_json: target.workflowInputJson,
|
|
351
|
+
cron_expression: schedule.cronExpression,
|
|
352
|
+
timezone: schedule.timezone,
|
|
353
|
+
monitor_timeout_value: monitorTimeout.monitorTimeoutValue,
|
|
354
|
+
monitor_timeout_unit: monitorTimeout.monitorTimeoutUnit,
|
|
355
|
+
created_at: now,
|
|
356
|
+
updated_at: now,
|
|
357
|
+
},
|
|
358
|
+
}).catch(throwCronjobNameConflictIfNeeded);
|
|
359
|
+
(0, scheduler_1.scheduleOneCronjob)(cronjob);
|
|
360
|
+
const activeRun = await client_1.default.cronjob_runs.findFirst({
|
|
361
|
+
where: { cronjob_id: cronjob.id, status: { in: ["running", "paused"] } },
|
|
362
|
+
select: { id: true },
|
|
363
|
+
});
|
|
364
|
+
res.json({ cronjob: serializeCronjob({ ...cronjob, is_running: activeRun !== null }) });
|
|
365
|
+
}, true));
|
|
366
|
+
router.get("/:id", (0, utils_1.apiHandler)(async (req, res) => {
|
|
367
|
+
const id = req.params.id;
|
|
368
|
+
const cronjob = await client_1.default.cronjobs.findFirst({
|
|
369
|
+
where: { id, user_id: req.userId },
|
|
370
|
+
include: {
|
|
371
|
+
runs: {
|
|
372
|
+
orderBy: { started_at: "desc" },
|
|
373
|
+
take: 1,
|
|
374
|
+
select: {
|
|
375
|
+
id: true,
|
|
376
|
+
status: true,
|
|
377
|
+
started_at: true,
|
|
378
|
+
completed_at: true,
|
|
379
|
+
workflow_run_id: true,
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
if (!cronjob) {
|
|
385
|
+
throw { status: 404, message: "Cronjob not found" };
|
|
386
|
+
}
|
|
387
|
+
const activeRun = await client_1.default.cronjob_runs.findFirst({
|
|
388
|
+
where: { cronjob_id: cronjob.id, status: { in: ["running", "paused"] } },
|
|
389
|
+
select: { id: true },
|
|
390
|
+
});
|
|
391
|
+
res.json({ cronjob: serializeCronjob({ ...cronjob, is_running: activeRun !== null }) });
|
|
392
|
+
}, true));
|
|
393
|
+
router.patch("/:id", (0, utils_1.apiHandler)(async (req, res) => {
|
|
394
|
+
const id = req.params.id;
|
|
395
|
+
const existing = await client_1.default.cronjobs.findFirst({
|
|
396
|
+
where: { id, user_id: req.userId },
|
|
397
|
+
});
|
|
398
|
+
if (!existing) {
|
|
399
|
+
throw { status: 404, message: "Cronjob not found" };
|
|
400
|
+
}
|
|
401
|
+
const name = req.body?.name === undefined ? existing.name : assertNonEmptyString(req.body.name, "name");
|
|
402
|
+
const enabled = req.body?.enabled === undefined ? existing.enabled : assertBoolean(req.body.enabled, "enabled");
|
|
403
|
+
const existingTarget = {
|
|
404
|
+
target_type: existing.target_type,
|
|
405
|
+
prompt: existing.prompt,
|
|
406
|
+
prompt_allow_workflow_runs_without_permission: existing.prompt_allow_workflow_runs_without_permission,
|
|
407
|
+
workflow_slug: existing.workflow_slug,
|
|
408
|
+
workflow_input_json: existing.workflow_input_json,
|
|
409
|
+
};
|
|
410
|
+
const target = await (0, scheduler_1.validateCronjobTarget)({
|
|
411
|
+
target_type: hasRequestField(req.body, "target_type") ? req.body.target_type : existingTarget.target_type,
|
|
412
|
+
prompt: hasRequestField(req.body, "prompt") ? req.body.prompt : existingTarget.prompt,
|
|
413
|
+
allow_workflow_runs_without_permission: hasRequestField(req.body, "allow_workflow_runs_without_permission")
|
|
414
|
+
? req.body.allow_workflow_runs_without_permission
|
|
415
|
+
: existingTarget.prompt_allow_workflow_runs_without_permission ?? undefined,
|
|
416
|
+
workflow_slug: hasRequestField(req.body, "workflow_slug") ? req.body.workflow_slug : existingTarget.workflow_slug ?? undefined,
|
|
417
|
+
workflow_inputs: hasRequestField(req.body, "workflow_inputs")
|
|
418
|
+
? req.body.workflow_inputs
|
|
419
|
+
: existingTarget.workflow_input_json
|
|
420
|
+
? JSON.parse(existingTarget.workflow_input_json)
|
|
421
|
+
: undefined,
|
|
422
|
+
}, req.userId);
|
|
423
|
+
const schedule = (0, scheduler_1.validateCronjobSchedule)({
|
|
424
|
+
cron_expression: hasRequestField(req.body, "cron_expression") ? req.body.cron_expression : existing.cron_expression,
|
|
425
|
+
timezone: hasRequestField(req.body, "timezone") ? req.body.timezone : existing.timezone,
|
|
426
|
+
});
|
|
427
|
+
const monitorTimeout = (0, scheduler_1.validateCronjobMonitorTimeout)({
|
|
428
|
+
monitor_timeout_value: hasRequestField(req.body, "monitor_timeout_value")
|
|
429
|
+
? req.body.monitor_timeout_value
|
|
430
|
+
: existing.monitor_timeout_value,
|
|
431
|
+
monitor_timeout_unit: hasRequestField(req.body, "monitor_timeout_unit")
|
|
432
|
+
? req.body.monitor_timeout_unit
|
|
433
|
+
: existing.monitor_timeout_unit,
|
|
434
|
+
});
|
|
435
|
+
const now = nowMs();
|
|
436
|
+
const cronjob = await client_1.default.cronjobs.update({
|
|
437
|
+
where: { id },
|
|
438
|
+
data: {
|
|
439
|
+
name,
|
|
440
|
+
enabled,
|
|
441
|
+
target_type: target.targetType,
|
|
442
|
+
prompt: target.prompt,
|
|
443
|
+
prompt_allow_workflow_runs_without_permission: target.promptAllowWorkflowRunsWithoutPermission,
|
|
444
|
+
workflow_slug: target.workflowSlug,
|
|
445
|
+
workflow_input_json: target.workflowInputJson,
|
|
446
|
+
cron_expression: schedule.cronExpression,
|
|
447
|
+
timezone: schedule.timezone,
|
|
448
|
+
monitor_timeout_value: monitorTimeout.monitorTimeoutValue,
|
|
449
|
+
monitor_timeout_unit: monitorTimeout.monitorTimeoutUnit,
|
|
450
|
+
updated_at: now,
|
|
451
|
+
},
|
|
452
|
+
}).catch(throwCronjobNameConflictIfNeeded);
|
|
453
|
+
(0, scheduler_1.scheduleOneCronjob)(cronjob);
|
|
454
|
+
res.json({ cronjob: serializeCronjob(cronjob) });
|
|
455
|
+
}, true));
|
|
456
|
+
router.delete("/:id", (0, utils_1.apiHandler)(async (req, res) => {
|
|
457
|
+
const id = req.params.id;
|
|
458
|
+
const cronjob = await client_1.default.cronjobs.findFirst({
|
|
459
|
+
where: { id, user_id: req.userId },
|
|
460
|
+
select: { id: true, cron_expression: true, timezone: true, enabled: true }
|
|
461
|
+
});
|
|
462
|
+
if (!cronjob) {
|
|
463
|
+
throw { status: 404, message: "Cronjob not found" };
|
|
464
|
+
}
|
|
465
|
+
const activeRun = await client_1.default.cronjob_runs.findFirst({
|
|
466
|
+
where: {
|
|
467
|
+
cronjob_id: id,
|
|
468
|
+
status: { in: ["running", "paused"] },
|
|
469
|
+
},
|
|
470
|
+
select: { id: true },
|
|
471
|
+
});
|
|
472
|
+
if (activeRun) {
|
|
473
|
+
throw {
|
|
474
|
+
status: 409,
|
|
475
|
+
message: "Cronjob currently has an active run. Terminate the active run before deleting it."
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
(0, scheduler_1.scheduleOneCronjob)({ ...cronjob, enabled: false });
|
|
479
|
+
await client_1.default.cronjobs.delete({ where: { id } });
|
|
480
|
+
res.json({ success: true });
|
|
481
|
+
}, true));
|
|
482
|
+
router.post("/:id/enable", (0, utils_1.apiHandler)(async (req, res) => {
|
|
483
|
+
const id = req.params.id;
|
|
484
|
+
const existing = await client_1.default.cronjobs.findFirst({
|
|
485
|
+
where: { id, user_id: req.userId },
|
|
486
|
+
select: { id: true },
|
|
487
|
+
});
|
|
488
|
+
if (!existing) {
|
|
489
|
+
throw { status: 404, message: "Cronjob not found" };
|
|
490
|
+
}
|
|
491
|
+
const cronjob = await client_1.default.cronjobs.update({
|
|
492
|
+
where: { id },
|
|
493
|
+
data: { enabled: true, updated_at: nowMs() },
|
|
494
|
+
});
|
|
495
|
+
(0, scheduler_1.scheduleOneCronjob)(cronjob);
|
|
496
|
+
res.json({ cronjob: serializeCronjob(cronjob) });
|
|
497
|
+
}, true));
|
|
498
|
+
router.post("/:id/disable", (0, utils_1.apiHandler)(async (req, res) => {
|
|
499
|
+
const id = req.params.id;
|
|
500
|
+
const existing = await client_1.default.cronjobs.findFirst({
|
|
501
|
+
where: { id, user_id: req.userId },
|
|
502
|
+
select: { id: true },
|
|
503
|
+
});
|
|
504
|
+
if (!existing) {
|
|
505
|
+
throw { status: 404, message: "Cronjob not found" };
|
|
506
|
+
}
|
|
507
|
+
const cronjob = await client_1.default.cronjobs.update({
|
|
508
|
+
where: { id },
|
|
509
|
+
data: { enabled: false, updated_at: nowMs() },
|
|
510
|
+
});
|
|
511
|
+
(0, scheduler_1.scheduleOneCronjob)(cronjob);
|
|
512
|
+
res.json({ cronjob: serializeCronjob(cronjob) });
|
|
513
|
+
}, true));
|
|
514
|
+
router.get("/:id/runs", (0, utils_1.apiHandler)(async (req, res) => {
|
|
515
|
+
const id = req.params.id;
|
|
516
|
+
const cronjob = await client_1.default.cronjobs.findFirst({
|
|
517
|
+
where: { id, user_id: req.userId },
|
|
518
|
+
select: { id: true },
|
|
519
|
+
});
|
|
520
|
+
if (!cronjob) {
|
|
521
|
+
throw { status: 404, message: "Cronjob not found" };
|
|
522
|
+
}
|
|
523
|
+
const runs = await client_1.default.cronjob_runs.findMany({
|
|
524
|
+
where: { cronjob_id: id },
|
|
525
|
+
orderBy: { started_at: "desc" },
|
|
526
|
+
take: 50,
|
|
527
|
+
include: {
|
|
528
|
+
session: { select: { visible_to_user: true } },
|
|
529
|
+
workflowRun: { select: { workflow_slug: true, args: true } },
|
|
530
|
+
cronjob: { select: { target_type: true, prompt: true, workflow_slug: true, workflow_input_json: true } },
|
|
531
|
+
},
|
|
532
|
+
});
|
|
533
|
+
res.json({ runs: runs.map(serializeRun) });
|
|
534
|
+
}, true));
|
|
535
|
+
router.post("/:id/run-now", (0, utils_1.apiHandler)(async (req, res) => {
|
|
536
|
+
const id = req.params.id;
|
|
537
|
+
const cronjob = await client_1.default.cronjobs.findFirst({
|
|
538
|
+
where: { id, user_id: req.userId },
|
|
539
|
+
select: { id: true },
|
|
540
|
+
});
|
|
541
|
+
if (!cronjob) {
|
|
542
|
+
throw { status: 404, message: "Cronjob not found" };
|
|
543
|
+
}
|
|
544
|
+
const runId = await (0, scheduler_1.dispatchCronjobRun)(id, "manual");
|
|
545
|
+
const run = await client_1.default.cronjob_runs.findUnique({
|
|
546
|
+
where: { id: runId },
|
|
547
|
+
select: { workflow_run_id: true },
|
|
548
|
+
});
|
|
549
|
+
res.json({ run_id: runId, workflow_run_id: run?.workflow_run_id ?? null });
|
|
550
|
+
}, true));
|
|
551
|
+
router.get("/runs/:id", (0, utils_1.apiHandler)(async (req, res) => {
|
|
552
|
+
const id = req.params.id;
|
|
553
|
+
const run = await client_1.default.cronjob_runs.findFirst({
|
|
554
|
+
where: {
|
|
555
|
+
id,
|
|
556
|
+
cronjob: { user_id: req.userId },
|
|
557
|
+
},
|
|
558
|
+
include: {
|
|
559
|
+
workflowRun: { select: { workflow_slug: true, args: true } },
|
|
560
|
+
cronjob: { select: { target_type: true, prompt: true, workflow_slug: true, workflow_input_json: true } },
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
if (!run) {
|
|
564
|
+
throw { status: 404, message: "Cronjob run not found" };
|
|
565
|
+
}
|
|
566
|
+
res.json({ run: serializeRun(run) });
|
|
567
|
+
}, true));
|
|
568
|
+
router.post("/runs/:id/interrupt", (0, utils_1.apiHandler)(async (req, res) => {
|
|
569
|
+
const id = req.params.id;
|
|
570
|
+
const run = await client_1.default.cronjob_runs.findFirst({
|
|
571
|
+
where: {
|
|
572
|
+
id,
|
|
573
|
+
cronjob: { user_id: req.userId },
|
|
574
|
+
},
|
|
575
|
+
select: { id: true },
|
|
576
|
+
});
|
|
577
|
+
if (!run) {
|
|
578
|
+
throw { status: 404, message: "Cronjob run not found" };
|
|
579
|
+
}
|
|
580
|
+
await (0, scheduler_1.interruptCronjobRun)(run.id);
|
|
581
|
+
res.json({ success: true });
|
|
582
|
+
}, true));
|
|
583
|
+
router.post("/runs/:id/resume", (0, utils_1.apiHandler)(async (req, res) => {
|
|
584
|
+
const id = req.params.id;
|
|
585
|
+
const run = await client_1.default.cronjob_runs.findFirst({
|
|
586
|
+
where: {
|
|
587
|
+
id,
|
|
588
|
+
cronjob: { user_id: req.userId },
|
|
589
|
+
},
|
|
590
|
+
select: { id: true },
|
|
591
|
+
});
|
|
592
|
+
if (!run) {
|
|
593
|
+
throw { status: 404, message: "Cronjob run not found" };
|
|
594
|
+
}
|
|
595
|
+
await (0, scheduler_1.resumeCronjobRun)(run.id);
|
|
596
|
+
res.json({ success: true });
|
|
597
|
+
}, true));
|
|
598
|
+
router.post("/runs/:id/terminate", (0, utils_1.apiHandler)(async (req, res) => {
|
|
599
|
+
const id = req.params.id;
|
|
600
|
+
const run = await client_1.default.cronjob_runs.findFirst({
|
|
601
|
+
where: {
|
|
602
|
+
id,
|
|
603
|
+
cronjob: { user_id: req.userId },
|
|
604
|
+
},
|
|
605
|
+
select: { id: true },
|
|
606
|
+
});
|
|
607
|
+
if (!run) {
|
|
608
|
+
throw { status: 404, message: "Cronjob run not found" };
|
|
609
|
+
}
|
|
610
|
+
await (0, scheduler_1.terminateCronjobRun)(run.id);
|
|
611
|
+
res.json({ success: true });
|
|
612
|
+
}, true));
|
|
613
|
+
async function addCronjobTodosHandler(req, res) {
|
|
614
|
+
if (!req.opencode_session_id) {
|
|
615
|
+
throw {
|
|
616
|
+
status: 400,
|
|
617
|
+
message: "This endpoint requires an opencode session token"
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
const items = assertStringArray(req.body?.items, "items");
|
|
621
|
+
const index = assertInsertIndex(req.body?.index);
|
|
622
|
+
const todoListVersion = assertTodoListVersion(req.body?.todo_list_version);
|
|
623
|
+
const now = nowMs();
|
|
624
|
+
const todos = await client_1.default.$transaction(async (tx) => {
|
|
625
|
+
const run = await getPromptCronjobRun(tx, req.opencode_session_id);
|
|
626
|
+
assertActivePromptCronjobRun(run);
|
|
627
|
+
const activeTodos = await tx.cronjob_run_todos.findMany({
|
|
628
|
+
where: {
|
|
629
|
+
run_id: run.id,
|
|
630
|
+
status: { not: "completed" },
|
|
631
|
+
},
|
|
632
|
+
orderBy: [
|
|
633
|
+
{ position: "asc" },
|
|
634
|
+
{ created_at: "asc" },
|
|
635
|
+
{ id: "asc" },
|
|
636
|
+
],
|
|
637
|
+
select: { id: true, run_id: true, content: true, status: true, position: true, summary: true, created_at: true, completed_at: true },
|
|
638
|
+
});
|
|
639
|
+
if (todoListVersion !== run.todo_list_version) {
|
|
640
|
+
throw {
|
|
641
|
+
status: 400,
|
|
642
|
+
message: `Your todo list version is stale (expected version ${run.todo_list_version}, got ${todoListVersion}). Current todo list: ${JSON.stringify(activeTodos.map(serializeCronjobTodo))}`
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
if (index > activeTodos.length) {
|
|
646
|
+
throw {
|
|
647
|
+
status: 400,
|
|
648
|
+
message: `index must be less than or equal to the current active todo count (${activeTodos.length}). If you want to append to the end of the list, use index ${activeTodos.length}. Current todo list: ${JSON.stringify(activeTodos.map(serializeCronjobTodo))}`
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
const insertAt = index;
|
|
652
|
+
const beforeTodos = activeTodos.slice(0, insertAt);
|
|
653
|
+
const afterTodos = activeTodos.slice(insertAt);
|
|
654
|
+
await Promise.all(beforeTodos.map((todo, position) => (tx.cronjob_run_todos.update({
|
|
655
|
+
where: { id: todo.id },
|
|
656
|
+
data: { position },
|
|
657
|
+
}))));
|
|
658
|
+
await Promise.all(afterTodos.map((todo, offset) => (tx.cronjob_run_todos.update({
|
|
659
|
+
where: { id: todo.id },
|
|
660
|
+
data: { position: insertAt + items.length + offset },
|
|
661
|
+
}))));
|
|
662
|
+
const insertedTodoIds = [];
|
|
663
|
+
for (const [offset, content] of items.entries()) {
|
|
664
|
+
const insertedTodo = await tx.cronjob_run_todos.create({
|
|
665
|
+
data: {
|
|
666
|
+
run_id: run.id,
|
|
667
|
+
content,
|
|
668
|
+
status: "pending",
|
|
669
|
+
position: insertAt + offset,
|
|
670
|
+
created_at: now,
|
|
671
|
+
},
|
|
672
|
+
select: { id: true },
|
|
673
|
+
});
|
|
674
|
+
insertedTodoIds.push(insertedTodo.id);
|
|
675
|
+
}
|
|
676
|
+
const normalizedTodos = await normalizeActiveCronjobTodoPositions(tx, run.id);
|
|
677
|
+
await tx.cronjob_runs.update({
|
|
678
|
+
where: { id: run.id },
|
|
679
|
+
data: { todo_list_version: { increment: 1 } },
|
|
680
|
+
});
|
|
681
|
+
return { normalizedTodos, insertedTodoIds, nextVersion: run.todo_list_version + 1 };
|
|
682
|
+
});
|
|
683
|
+
res.json({
|
|
684
|
+
success: true,
|
|
685
|
+
added_count: items.length,
|
|
686
|
+
added_todo_ids: todos.insertedTodoIds,
|
|
687
|
+
todo_list_version: todos.nextVersion,
|
|
688
|
+
todos: todos.normalizedTodos.map(serializeCronjobTodo),
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
router.post("/runs/todos/add", (0, utils_1.apiHandler)(async (req, res) => {
|
|
692
|
+
await addCronjobTodosHandler(req, res);
|
|
693
|
+
}, true));
|
|
694
|
+
router.post("/runs/todos/clear", (0, utils_1.apiHandler)(async (req, res) => {
|
|
695
|
+
if (!req.opencode_session_id) {
|
|
696
|
+
throw {
|
|
697
|
+
status: 400,
|
|
698
|
+
message: "This endpoint requires an opencode session token"
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
const todoIds = req.body?.todo_ids;
|
|
702
|
+
const explicitTodoIds = todoIds === undefined || todoIds === null ? null : assertStringArray(todoIds, "todo_ids");
|
|
703
|
+
if (!explicitTodoIds) {
|
|
704
|
+
throw {
|
|
705
|
+
status: 400,
|
|
706
|
+
message: "todo_ids is required"
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
const run = await requireActivePromptCronjobRun(req.opencode_session_id);
|
|
710
|
+
const todos = await getActiveCronjobTodos(run.id);
|
|
711
|
+
const activeTodoIds = new Set(todos.map((todo) => todo.id));
|
|
712
|
+
const idsToDelete = (explicitTodoIds ?? []).filter((todoId) => activeTodoIds.has(todoId));
|
|
713
|
+
if (idsToDelete.length === 0) {
|
|
714
|
+
throw {
|
|
715
|
+
status: 400,
|
|
716
|
+
message: "No matching cronjob todos were found to clear."
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
let clearedCount = 0;
|
|
720
|
+
const remainingTodos = await client_1.default.$transaction(async (tx) => {
|
|
721
|
+
const deleted = await tx.cronjob_run_todos.deleteMany({
|
|
722
|
+
where: {
|
|
723
|
+
run_id: run.id,
|
|
724
|
+
id: { in: idsToDelete },
|
|
725
|
+
},
|
|
726
|
+
});
|
|
727
|
+
if (deleted.count === 0) {
|
|
728
|
+
throw {
|
|
729
|
+
status: 400,
|
|
730
|
+
message: "No matching cronjob todos were found to clear."
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
clearedCount = deleted.count;
|
|
734
|
+
const normalizedTodos = await normalizeActiveCronjobTodoPositions(tx, run.id);
|
|
735
|
+
const updatedRun = await tx.cronjob_runs.update({
|
|
736
|
+
where: { id: run.id },
|
|
737
|
+
data: { todo_list_version: { increment: 1 } },
|
|
738
|
+
select: { todo_list_version: true },
|
|
739
|
+
});
|
|
740
|
+
return { normalizedTodos, nextVersion: updatedRun.todo_list_version };
|
|
741
|
+
});
|
|
742
|
+
res.json({
|
|
743
|
+
success: true,
|
|
744
|
+
cleared_count: clearedCount,
|
|
745
|
+
cleared_todo_ids: idsToDelete,
|
|
746
|
+
todo_list_version: remainingTodos.nextVersion,
|
|
747
|
+
todos: remainingTodos.normalizedTodos.map(serializeCronjobTodo),
|
|
748
|
+
});
|
|
749
|
+
}, true));
|
|
750
|
+
router.post("/runs/todos/finish-current", (0, utils_1.apiHandler)(async (req, res) => {
|
|
751
|
+
if (!req.opencode_session_id) {
|
|
752
|
+
throw {
|
|
753
|
+
status: 400,
|
|
754
|
+
message: "This endpoint requires an opencode session token"
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
const completionSummary = assertNonEmptyString(req.body?.completionSummary, "completionSummary");
|
|
758
|
+
const run = await requireActivePromptCronjobRun(req.opencode_session_id);
|
|
759
|
+
const currentTodo = await getCurrentCronjobTodoRecord(run.id);
|
|
760
|
+
if (!currentTodo) {
|
|
761
|
+
throw {
|
|
762
|
+
status: 400,
|
|
763
|
+
message: "There is no current cronjob todo item to finish."
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
const nextVersion = await client_1.default.$transaction(async (tx) => {
|
|
767
|
+
await tx.cronjob_run_todos.update({
|
|
768
|
+
where: { id: currentTodo.id },
|
|
769
|
+
data: {
|
|
770
|
+
status: "completed",
|
|
771
|
+
completed_at: nowMs(),
|
|
772
|
+
summary: completionSummary,
|
|
773
|
+
},
|
|
774
|
+
});
|
|
775
|
+
const updatedRun = await tx.cronjob_runs.update({
|
|
776
|
+
where: { id: run.id },
|
|
777
|
+
data: { todo_list_version: { increment: 1 } },
|
|
778
|
+
select: { todo_list_version: true },
|
|
779
|
+
});
|
|
780
|
+
return updatedRun.todo_list_version;
|
|
781
|
+
});
|
|
782
|
+
res.json({ success: true, todo_list_version: nextVersion });
|
|
783
|
+
}, true));
|
|
784
|
+
router.post("/runs/complete-current", (0, utils_1.apiHandler)(async (req, res) => {
|
|
785
|
+
if (!req.opencode_session_id) {
|
|
786
|
+
throw {
|
|
787
|
+
status: 400,
|
|
788
|
+
message: "This endpoint requires an opencode session token"
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
const summary = assertNonEmptyString(req.body?.summary, "summary");
|
|
792
|
+
await (0, scheduler_1.completeCurrentCronjobRun)(req.opencode_session_id, summary);
|
|
793
|
+
res.json({ success: true });
|
|
794
|
+
}, true));
|
|
795
|
+
router.post("/runs/fail-current", (0, utils_1.apiHandler)(async (req, res) => {
|
|
796
|
+
if (!req.opencode_session_id) {
|
|
797
|
+
throw {
|
|
798
|
+
status: 400,
|
|
799
|
+
message: "This endpoint requires an opencode session token"
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
const summary = assertNonEmptyString(req.body?.summary, "summary");
|
|
803
|
+
const run = await getPromptCronjobRun(client_1.default, req.opencode_session_id);
|
|
804
|
+
assertActivePromptCronjobRun(run);
|
|
805
|
+
const failedRun = await client_1.default.cronjob_runs.updateMany({
|
|
806
|
+
where: { id: run.id, status: { in: ["running", "paused"] } },
|
|
807
|
+
data: {
|
|
808
|
+
status: "failed",
|
|
809
|
+
completed_at: nowMs(),
|
|
810
|
+
summary,
|
|
811
|
+
error_message: summary,
|
|
812
|
+
},
|
|
813
|
+
});
|
|
814
|
+
if (failedRun.count !== 1) {
|
|
815
|
+
throw {
|
|
816
|
+
status: 409,
|
|
817
|
+
message: "Cronjob run is no longer active."
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
res.json({ success: true });
|
|
821
|
+
}, true));
|
|
822
|
+
exports.default = router;
|