workplane 0.3.1 → 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/README.md +80 -86
- package/dist/cli.js +5 -2
- package/dist/cli.js.map +1 -1
- package/dist/migrate.js +260 -41
- package/dist/migrate.js.map +1 -1
- package/dist/migrations/pg/0000_majestic_norman_osborn.sql +75 -0
- package/dist/migrations/pg/meta/0000_snapshot.json +583 -0
- package/dist/migrations/pg/meta/_journal.json +13 -0
- package/dist/migrations/sqlite/0000_dry_zarek.sql +75 -0
- package/dist/migrations/sqlite/meta/0000_snapshot.json +535 -0
- package/dist/migrations/sqlite/meta/_journal.json +13 -0
- package/dist/node.js +20 -17
- package/dist/node.js.map +1 -1
- package/dist/schema.sqlite.sql +74 -0
- package/dist/server.js +743 -335
- package/dist/server.js.map +1 -1
- package/dist/setup.js +410 -0
- package/dist/setup.js.map +1 -0
- package/package.json +8 -2
package/dist/server.js
CHANGED
|
@@ -128,20 +128,23 @@ import { z } from "zod";
|
|
|
128
128
|
|
|
129
129
|
// ../core/src/env.ts
|
|
130
130
|
import { config as dotenvConfig } from "dotenv";
|
|
131
|
+
import { homedir } from "os";
|
|
132
|
+
import { join } from "path";
|
|
131
133
|
var envLoaded = false;
|
|
132
134
|
function loadLocalEnv() {
|
|
133
135
|
if (envLoaded) {
|
|
134
136
|
return;
|
|
135
137
|
}
|
|
138
|
+
dotenvConfig({ path: ".env.local" });
|
|
139
|
+
dotenvConfig({ path: join(homedir(), ".workplane", ".env") });
|
|
136
140
|
dotenvConfig({ path: ".env" });
|
|
137
|
-
dotenvConfig({ path: ".env.local", override: true });
|
|
138
141
|
envLoaded = true;
|
|
139
142
|
}
|
|
140
143
|
|
|
141
144
|
// ../core/src/config.ts
|
|
142
145
|
loadLocalEnv();
|
|
143
146
|
var serverEnvSchema = z.object({
|
|
144
|
-
DATABASE_URL: z.string().
|
|
147
|
+
DATABASE_URL: z.string().optional().default("sqlite://~/.workplane/workplane.db"),
|
|
145
148
|
WORKPLANE_SERVER_PORT: z.string().optional().default("8787").transform((value) => Number.parseInt(value, 10)),
|
|
146
149
|
WORKPLANE_NODE_TOKEN: z.string().optional(),
|
|
147
150
|
WORKPLANE_OPERATOR_TOKEN: z.string().optional(),
|
|
@@ -171,17 +174,16 @@ function parseCsv(value) {
|
|
|
171
174
|
}
|
|
172
175
|
|
|
173
176
|
// ../db/src/client.ts
|
|
177
|
+
import { mkdirSync } from "fs";
|
|
178
|
+
import { homedir as homedir2 } from "os";
|
|
179
|
+
import { dirname, join as join2 } from "path";
|
|
180
|
+
import { drizzle as drizzlePg } from "drizzle-orm/node-postgres";
|
|
181
|
+
import { drizzle as drizzleSqlite } from "drizzle-orm/better-sqlite3";
|
|
182
|
+
import Database from "better-sqlite3";
|
|
174
183
|
import { Pool } from "pg";
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (existing) {
|
|
179
|
-
return existing;
|
|
180
|
-
}
|
|
181
|
-
const pool = new Pool({ connectionString: databaseUrl });
|
|
182
|
-
pools.set(databaseUrl, pool);
|
|
183
|
-
return pool;
|
|
184
|
-
}
|
|
184
|
+
|
|
185
|
+
// ../db/src/pg-store.ts
|
|
186
|
+
import { and, asc, desc, eq, gt, sql as sql2 } from "drizzle-orm";
|
|
185
187
|
|
|
186
188
|
// ../core/src/ids.ts
|
|
187
189
|
import { randomUUID } from "crypto";
|
|
@@ -189,8 +191,155 @@ function makeId(prefix) {
|
|
|
189
191
|
return `${prefix}_${randomUUID().replace(/-/g, "").slice(0, 12)}`;
|
|
190
192
|
}
|
|
191
193
|
|
|
192
|
-
// ../
|
|
193
|
-
|
|
194
|
+
// ../db/src/schema/pg.ts
|
|
195
|
+
var pg_exports = {};
|
|
196
|
+
__export(pg_exports, {
|
|
197
|
+
artifacts: () => artifacts,
|
|
198
|
+
nodes: () => nodes,
|
|
199
|
+
runInputEvents: () => runInputEvents,
|
|
200
|
+
runLogs: () => runLogs,
|
|
201
|
+
runs: () => runs,
|
|
202
|
+
tasks: () => tasks
|
|
203
|
+
});
|
|
204
|
+
import { isNull, sql } from "drizzle-orm";
|
|
205
|
+
import { bigint, bigserial, index, integer, jsonb, pgTable, text, timestamp, uniqueIndex } from "drizzle-orm/pg-core";
|
|
206
|
+
var tasks = pgTable("tasks", {
|
|
207
|
+
id: text("id").primaryKey(),
|
|
208
|
+
kind: text("kind").notNull(),
|
|
209
|
+
adapter: text("adapter").notNull(),
|
|
210
|
+
payload: jsonb("payload").$type().notNull(),
|
|
211
|
+
requires: text("requires").array().notNull().default(sql`'{}'::text[]`),
|
|
212
|
+
status: text("status").notNull(),
|
|
213
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
214
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
215
|
+
}, (table) => ({
|
|
216
|
+
statusCreatedAtIdx: index("idx_tasks_status_created_at").on(table.status, table.createdAt)
|
|
217
|
+
}));
|
|
218
|
+
var nodes = pgTable("nodes", {
|
|
219
|
+
id: text("id").primaryKey(),
|
|
220
|
+
name: text("name").notNull(),
|
|
221
|
+
capabilities: text("capabilities").array().notNull().default(sql`'{}'::text[]`),
|
|
222
|
+
status: text("status").notNull().default("online"),
|
|
223
|
+
lastHeartbeatAt: timestamp("last_heartbeat_at", { withTimezone: true }),
|
|
224
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
225
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
226
|
+
});
|
|
227
|
+
var runs = pgTable("runs", {
|
|
228
|
+
id: text("id").primaryKey(),
|
|
229
|
+
taskId: text("task_id").notNull().references(() => tasks.id),
|
|
230
|
+
nodeId: text("node_id").notNull().references(() => nodes.id),
|
|
231
|
+
attempt: integer("attempt").notNull(),
|
|
232
|
+
status: text("status").notNull(),
|
|
233
|
+
startedAt: timestamp("started_at", { withTimezone: true }),
|
|
234
|
+
endedAt: timestamp("ended_at", { withTimezone: true }),
|
|
235
|
+
error: text("error"),
|
|
236
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
237
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
238
|
+
}, (table) => ({
|
|
239
|
+
taskIdIdx: index("idx_runs_task_id").on(table.taskId),
|
|
240
|
+
nodeIdIdx: index("idx_runs_node_id").on(table.nodeId)
|
|
241
|
+
}));
|
|
242
|
+
var runLogs = pgTable("run_logs", {
|
|
243
|
+
id: bigserial("id", { mode: "number" }).primaryKey(),
|
|
244
|
+
runId: text("run_id").notNull().references(() => runs.id),
|
|
245
|
+
stepName: text("step_name"),
|
|
246
|
+
stream: text("stream").notNull(),
|
|
247
|
+
message: text("message").notNull(),
|
|
248
|
+
timestamp: timestamp("timestamp", { withTimezone: true }).notNull().defaultNow()
|
|
249
|
+
}, (table) => ({
|
|
250
|
+
runIdTimestampIdx: index("idx_run_logs_run_id_timestamp").on(table.runId, table.timestamp)
|
|
251
|
+
}));
|
|
252
|
+
var artifacts = pgTable("artifacts", {
|
|
253
|
+
id: text("id").primaryKey(),
|
|
254
|
+
runId: text("run_id").notNull().references(() => runs.id),
|
|
255
|
+
type: text("type").notNull(),
|
|
256
|
+
name: text("name").notNull(),
|
|
257
|
+
path: text("path").notNull(),
|
|
258
|
+
metadata: jsonb("metadata").$type(),
|
|
259
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
260
|
+
}, (table) => ({
|
|
261
|
+
runIdIdx: index("idx_artifacts_run_id").on(table.runId)
|
|
262
|
+
}));
|
|
263
|
+
var runInputEvents = pgTable("run_input_events", {
|
|
264
|
+
id: bigserial("id", { mode: "number" }).primaryKey(),
|
|
265
|
+
runId: text("run_id").notNull().references(() => runs.id),
|
|
266
|
+
sequence: bigint("sequence", { mode: "number" }).notNull(),
|
|
267
|
+
kind: text("kind").notNull(),
|
|
268
|
+
payload: jsonb("payload").$type().notNull(),
|
|
269
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
270
|
+
deliveredAt: timestamp("delivered_at", { withTimezone: true })
|
|
271
|
+
}, (table) => ({
|
|
272
|
+
runSequenceUniqueIdx: uniqueIndex("idx_run_input_events_run_sequence").on(table.runId, table.sequence),
|
|
273
|
+
undeliveredIdx: index("idx_run_input_events_undelivered").on(table.runId, table.sequence).where(isNull(table.deliveredAt))
|
|
274
|
+
}));
|
|
275
|
+
|
|
276
|
+
// ../db/src/pg-store.ts
|
|
277
|
+
function toTaskRecord(row) {
|
|
278
|
+
return {
|
|
279
|
+
id: row.id,
|
|
280
|
+
kind: row.kind,
|
|
281
|
+
adapter: row.adapter,
|
|
282
|
+
payload: row.payload,
|
|
283
|
+
requires: row.requires ?? [],
|
|
284
|
+
status: row.status,
|
|
285
|
+
createdAt: row.createdAt.toISOString(),
|
|
286
|
+
updatedAt: row.updatedAt.toISOString()
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function toRunRecord(row) {
|
|
290
|
+
return {
|
|
291
|
+
id: row.id,
|
|
292
|
+
taskId: row.taskId,
|
|
293
|
+
nodeId: row.nodeId,
|
|
294
|
+
attempt: row.attempt,
|
|
295
|
+
status: row.status,
|
|
296
|
+
startedAt: row.startedAt?.toISOString() ?? null,
|
|
297
|
+
endedAt: row.endedAt?.toISOString() ?? null,
|
|
298
|
+
error: row.error ?? null
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function toNodeRecord(row) {
|
|
302
|
+
return {
|
|
303
|
+
id: row.id,
|
|
304
|
+
name: row.name,
|
|
305
|
+
capabilities: row.capabilities ?? [],
|
|
306
|
+
status: row.status,
|
|
307
|
+
lastHeartbeatAt: row.lastHeartbeatAt?.toISOString() ?? null
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function toLogRecord(row) {
|
|
311
|
+
return {
|
|
312
|
+
id: row.id,
|
|
313
|
+
runId: row.runId,
|
|
314
|
+
stepName: row.stepName ?? null,
|
|
315
|
+
stream: row.stream,
|
|
316
|
+
message: row.message,
|
|
317
|
+
timestamp: row.timestamp.toISOString()
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function toArtifactRecord(row) {
|
|
321
|
+
return {
|
|
322
|
+
id: row.id,
|
|
323
|
+
runId: row.runId,
|
|
324
|
+
type: row.type,
|
|
325
|
+
name: row.name,
|
|
326
|
+
path: row.path,
|
|
327
|
+
metadata: row.metadata ?? null,
|
|
328
|
+
createdAt: row.createdAt.toISOString()
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function toInputEventRecord(row) {
|
|
332
|
+
return {
|
|
333
|
+
id: row.id,
|
|
334
|
+
runId: row.runId,
|
|
335
|
+
sequence: row.sequence,
|
|
336
|
+
kind: row.kind,
|
|
337
|
+
payload: row.payload,
|
|
338
|
+
createdAt: row.createdAt.toISOString(),
|
|
339
|
+
deliveredAt: row.deliveredAt?.toISOString() ?? null
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function mapRawTask(row) {
|
|
194
343
|
return {
|
|
195
344
|
id: String(row.id),
|
|
196
345
|
kind: String(row.kind),
|
|
@@ -202,7 +351,7 @@ function mapTask(row) {
|
|
|
202
351
|
updatedAt: new Date(String(row.updated_at)).toISOString()
|
|
203
352
|
};
|
|
204
353
|
}
|
|
205
|
-
function
|
|
354
|
+
function mapRawRun(row) {
|
|
206
355
|
return {
|
|
207
356
|
id: String(row.id),
|
|
208
357
|
taskId: String(row.task_id),
|
|
@@ -214,385 +363,645 @@ function mapRun(row) {
|
|
|
214
363
|
error: row.error ? String(row.error) : null
|
|
215
364
|
};
|
|
216
365
|
}
|
|
217
|
-
function mapInputEvent(row) {
|
|
218
|
-
return {
|
|
219
|
-
id: Number(row.id),
|
|
220
|
-
runId: String(row.run_id),
|
|
221
|
-
sequence: Number(row.sequence),
|
|
222
|
-
kind: row.kind,
|
|
223
|
-
payload: row.payload ?? {},
|
|
224
|
-
createdAt: new Date(String(row.created_at)).toISOString(),
|
|
225
|
-
deliveredAt: row.delivered_at ? new Date(String(row.delivered_at)).toISOString() : null
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
function mapArtifact(row) {
|
|
229
|
-
return {
|
|
230
|
-
id: String(row.id),
|
|
231
|
-
runId: String(row.run_id),
|
|
232
|
-
type: String(row.type),
|
|
233
|
-
name: String(row.name),
|
|
234
|
-
path: String(row.path),
|
|
235
|
-
metadata: row.metadata ?? null,
|
|
236
|
-
createdAt: new Date(String(row.created_at)).toISOString()
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
366
|
var PgStore = class {
|
|
240
|
-
constructor(
|
|
241
|
-
this.
|
|
367
|
+
constructor(db) {
|
|
368
|
+
this.db = db;
|
|
242
369
|
}
|
|
243
|
-
|
|
370
|
+
db;
|
|
244
371
|
async createTask(input) {
|
|
245
372
|
const taskId = makeId("task");
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
);
|
|
254
|
-
return
|
|
373
|
+
const [row] = await this.db.insert(tasks).values({
|
|
374
|
+
id: taskId,
|
|
375
|
+
kind: input.kind,
|
|
376
|
+
adapter: input.adapter,
|
|
377
|
+
payload: input.payload,
|
|
378
|
+
requires: input.requires ?? [],
|
|
379
|
+
status: "queued"
|
|
380
|
+
}).returning();
|
|
381
|
+
return toTaskRecord(row);
|
|
255
382
|
}
|
|
256
383
|
async listTasks(status) {
|
|
257
|
-
const
|
|
258
|
-
return
|
|
384
|
+
const rows = await this.db.select().from(tasks).where(status ? eq(tasks.status, status) : void 0).orderBy(desc(tasks.createdAt)).limit(100);
|
|
385
|
+
return rows.map(toTaskRecord);
|
|
259
386
|
}
|
|
260
387
|
async getTask(taskId) {
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
return null;
|
|
264
|
-
}
|
|
265
|
-
return mapTask(result.rows[0]);
|
|
388
|
+
const [row] = await this.db.select().from(tasks).where(eq(tasks.id, taskId));
|
|
389
|
+
return row ? toTaskRecord(row) : null;
|
|
266
390
|
}
|
|
267
391
|
async listRuns(filters) {
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
]) : taskId ? await this.pool.query("select * from runs where task_id = $1 order by created_at desc", [taskId]) : status ? await this.pool.query("select * from runs where status = $1 order by created_at desc limit 100", [status]) : await this.pool.query("select * from runs order by created_at desc limit 100");
|
|
274
|
-
return result.rows.map((row) => mapRun(row));
|
|
392
|
+
const conditions = [];
|
|
393
|
+
if (filters?.taskId) conditions.push(eq(runs.taskId, filters.taskId));
|
|
394
|
+
if (filters?.status) conditions.push(eq(runs.status, filters.status));
|
|
395
|
+
const rows = await this.db.select().from(runs).where(conditions.length ? and(...conditions) : void 0).orderBy(desc(runs.createdAt)).limit(100);
|
|
396
|
+
return rows.map(toRunRecord);
|
|
275
397
|
}
|
|
276
398
|
async getRun(runId) {
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
return null;
|
|
280
|
-
}
|
|
281
|
-
return mapRun(result.rows[0]);
|
|
399
|
+
const [row] = await this.db.select().from(runs).where(eq(runs.id, runId));
|
|
400
|
+
return row ? toRunRecord(row) : null;
|
|
282
401
|
}
|
|
283
402
|
async registerNode(name, capabilities, preferredId) {
|
|
284
|
-
const existing = await this.
|
|
285
|
-
if (existing
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
const updated = await this.pool.query(
|
|
289
|
-
`
|
|
290
|
-
update nodes
|
|
291
|
-
set capabilities = $2::text[], status = 'online', last_heartbeat_at = now(), updated_at = now()
|
|
292
|
-
where id = $1
|
|
293
|
-
returning *
|
|
294
|
-
`,
|
|
295
|
-
[nodeId2, capabilities]
|
|
296
|
-
);
|
|
297
|
-
return this.mapNodeRecord(updated.rows[0]);
|
|
403
|
+
const [existing] = await this.db.select().from(nodes).where(eq(nodes.name, name));
|
|
404
|
+
if (existing) {
|
|
405
|
+
const [updated] = await this.db.update(nodes).set({ capabilities, status: "online", lastHeartbeatAt: sql2`now()`, updatedAt: sql2`now()` }).where(eq(nodes.id, existing.id)).returning();
|
|
406
|
+
return toNodeRecord(updated);
|
|
298
407
|
}
|
|
299
408
|
const nodeId = preferredId ?? makeId("node");
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
);
|
|
308
|
-
return this.mapNodeRecord(result.rows[0]);
|
|
409
|
+
const [inserted] = await this.db.insert(nodes).values({
|
|
410
|
+
id: nodeId,
|
|
411
|
+
name,
|
|
412
|
+
capabilities,
|
|
413
|
+
status: "online",
|
|
414
|
+
lastHeartbeatAt: sql2`now()`
|
|
415
|
+
}).returning();
|
|
416
|
+
return toNodeRecord(inserted);
|
|
309
417
|
}
|
|
310
418
|
async getRunCancellationState(runId) {
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
select r.status as run_status, t.status as task_status
|
|
314
|
-
from runs r
|
|
315
|
-
join tasks t on t.id = r.task_id
|
|
316
|
-
where r.id = $1
|
|
317
|
-
`,
|
|
318
|
-
[runId]
|
|
319
|
-
);
|
|
320
|
-
if (result.rowCount === 0) {
|
|
321
|
-
return null;
|
|
322
|
-
}
|
|
323
|
-
const row = result.rows[0];
|
|
324
|
-
return {
|
|
325
|
-
runStatus: String(row.run_status),
|
|
326
|
-
taskStatus: String(row.task_status)
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
mapNodeRecord(row) {
|
|
330
|
-
return {
|
|
331
|
-
id: String(row.id),
|
|
332
|
-
name: String(row.name),
|
|
333
|
-
capabilities: row.capabilities ?? [],
|
|
334
|
-
status: row.status,
|
|
335
|
-
lastHeartbeatAt: row.last_heartbeat_at ? new Date(String(row.last_heartbeat_at)).toISOString() : null
|
|
336
|
-
};
|
|
419
|
+
const [row] = await this.db.select({ runStatus: runs.status, taskStatus: tasks.status }).from(runs).innerJoin(tasks, eq(tasks.id, runs.taskId)).where(eq(runs.id, runId));
|
|
420
|
+
return row ?? null;
|
|
337
421
|
}
|
|
338
422
|
async pollNode(nodeId, capabilities) {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
);
|
|
355
|
-
if (taskResult.rowCount === 0) {
|
|
356
|
-
await client.query("commit");
|
|
357
|
-
return null;
|
|
358
|
-
}
|
|
423
|
+
return await this.db.transaction(async (tx) => {
|
|
424
|
+
await tx.execute(sql2`
|
|
425
|
+
UPDATE nodes
|
|
426
|
+
SET capabilities = ${capabilities}::text[], status = 'online',
|
|
427
|
+
last_heartbeat_at = now(), updated_at = now()
|
|
428
|
+
WHERE id = ${nodeId}
|
|
429
|
+
`);
|
|
430
|
+
const taskResult = await tx.execute(sql2`
|
|
431
|
+
SELECT * FROM tasks
|
|
432
|
+
WHERE status = 'queued' AND requires <@ ${capabilities}::text[]
|
|
433
|
+
ORDER BY created_at ASC
|
|
434
|
+
LIMIT 1
|
|
435
|
+
FOR UPDATE SKIP LOCKED
|
|
436
|
+
`);
|
|
437
|
+
if (taskResult.rows.length === 0) return null;
|
|
359
438
|
const taskRow = taskResult.rows[0];
|
|
360
439
|
const taskId = String(taskRow.id);
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
]);
|
|
440
|
+
const attemptResult = await tx.execute(sql2`
|
|
441
|
+
SELECT COALESCE(MAX(attempt), 0) AS max_attempt FROM runs WHERE task_id = ${taskId}
|
|
442
|
+
`);
|
|
365
443
|
const attempt = Number(attemptResult.rows[0].max_attempt) + 1;
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
await client.query("commit");
|
|
444
|
+
const runId = makeId("run");
|
|
445
|
+
const runResult = await tx.execute(sql2`
|
|
446
|
+
INSERT INTO runs (id, task_id, node_id, attempt, status, started_at)
|
|
447
|
+
VALUES (${runId}, ${taskId}, ${nodeId}, ${attempt}, 'assigned', now())
|
|
448
|
+
RETURNING *
|
|
449
|
+
`);
|
|
450
|
+
await tx.execute(sql2`
|
|
451
|
+
UPDATE tasks SET status = 'assigned', updated_at = now() WHERE id = ${taskId}
|
|
452
|
+
`);
|
|
376
453
|
return {
|
|
377
|
-
task:
|
|
378
|
-
run:
|
|
454
|
+
task: mapRawTask(taskRow),
|
|
455
|
+
run: mapRawRun(runResult.rows[0])
|
|
379
456
|
};
|
|
380
|
-
}
|
|
381
|
-
await client.query("rollback");
|
|
382
|
-
throw error;
|
|
383
|
-
} finally {
|
|
384
|
-
client.release();
|
|
385
|
-
}
|
|
457
|
+
});
|
|
386
458
|
}
|
|
387
459
|
async updateRunStatus(runId, status, error) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
where r.id = $1
|
|
397
|
-
for update
|
|
398
|
-
`,
|
|
399
|
-
[runId]
|
|
400
|
-
);
|
|
401
|
-
if (currentResult.rowCount === 0) {
|
|
402
|
-
await client.query("rollback");
|
|
403
|
-
return null;
|
|
404
|
-
}
|
|
460
|
+
return await this.db.transaction(async (tx) => {
|
|
461
|
+
const currentResult = await tx.execute(sql2`
|
|
462
|
+
SELECT r.*, t.status AS task_status
|
|
463
|
+
FROM runs r JOIN tasks t ON t.id = r.task_id
|
|
464
|
+
WHERE r.id = ${runId}
|
|
465
|
+
FOR UPDATE
|
|
466
|
+
`);
|
|
467
|
+
if (currentResult.rows.length === 0) return null;
|
|
405
468
|
const currentRow = currentResult.rows[0];
|
|
406
469
|
const currentTaskStatus = String(currentRow.task_status);
|
|
407
470
|
if (currentTaskStatus === "cancelled" && status !== "cancelled") {
|
|
408
|
-
|
|
409
|
-
return mapRun(currentRow);
|
|
410
|
-
}
|
|
411
|
-
const runResult = await client.query(
|
|
412
|
-
`
|
|
413
|
-
update runs
|
|
414
|
-
set status = $2, error = $3, ended_at = case when $2 in ('succeeded', 'failed', 'cancelled') then now() else ended_at end, updated_at = now()
|
|
415
|
-
where id = $1
|
|
416
|
-
returning *
|
|
417
|
-
`,
|
|
418
|
-
[runId, status, error ?? null]
|
|
419
|
-
);
|
|
420
|
-
if (runResult.rowCount === 0) {
|
|
421
|
-
await client.query("rollback");
|
|
422
|
-
return null;
|
|
471
|
+
return mapRawRun(currentRow);
|
|
423
472
|
}
|
|
424
|
-
const
|
|
425
|
-
const
|
|
473
|
+
const isTerminal = status === "succeeded" || status === "failed" || status === "cancelled";
|
|
474
|
+
const [updatedRun] = await tx.update(runs).set(isTerminal ? { status, error: error ?? null, endedAt: sql2`now()`, updatedAt: sql2`now()` } : { status, error: error ?? null, updatedAt: sql2`now()` }).where(eq(runs.id, runId)).returning();
|
|
475
|
+
if (!updatedRun) return null;
|
|
476
|
+
const nextTaskStatus = isTerminal ? status : "running";
|
|
426
477
|
if (currentTaskStatus !== "cancelled" || status === "cancelled") {
|
|
427
|
-
await
|
|
478
|
+
await tx.update(tasks).set({ status: nextTaskStatus, updatedAt: sql2`now()` }).where(eq(tasks.id, updatedRun.taskId));
|
|
428
479
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
} catch (error2) {
|
|
432
|
-
await client.query("rollback");
|
|
433
|
-
throw error2;
|
|
434
|
-
} finally {
|
|
435
|
-
client.release();
|
|
436
|
-
}
|
|
480
|
+
return toRunRecord(updatedRun);
|
|
481
|
+
});
|
|
437
482
|
}
|
|
438
483
|
async appendRunLogs(runId, logs) {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
450
|
-
return inserted;
|
|
484
|
+
if (logs.length === 0) return 0;
|
|
485
|
+
await this.db.insert(runLogs).values(
|
|
486
|
+
logs.map((log) => ({
|
|
487
|
+
runId,
|
|
488
|
+
stepName: log.stepName ?? null,
|
|
489
|
+
stream: log.stream,
|
|
490
|
+
message: log.message
|
|
491
|
+
}))
|
|
492
|
+
);
|
|
493
|
+
return logs.length;
|
|
451
494
|
}
|
|
452
495
|
async getRunLogs(runId) {
|
|
453
|
-
const
|
|
454
|
-
return
|
|
496
|
+
const rows = await this.db.select().from(runLogs).where(eq(runLogs.runId, runId)).orderBy(asc(runLogs.timestamp));
|
|
497
|
+
return rows.map(toLogRecord);
|
|
455
498
|
}
|
|
456
499
|
async createArtifact(runId, input) {
|
|
457
500
|
const artifactId = makeId("artifact");
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
);
|
|
466
|
-
return
|
|
501
|
+
const [row] = await this.db.insert(artifacts).values({
|
|
502
|
+
id: artifactId,
|
|
503
|
+
runId,
|
|
504
|
+
type: input.type,
|
|
505
|
+
name: input.name,
|
|
506
|
+
path: input.path,
|
|
507
|
+
metadata: input.metadata ?? null
|
|
508
|
+
}).returning();
|
|
509
|
+
return toArtifactRecord(row);
|
|
467
510
|
}
|
|
468
511
|
async listRunArtifacts(runId) {
|
|
469
|
-
const
|
|
470
|
-
return
|
|
512
|
+
const rows = await this.db.select().from(artifacts).where(eq(artifacts.runId, runId)).orderBy(asc(artifacts.createdAt));
|
|
513
|
+
return rows.map(toArtifactRecord);
|
|
471
514
|
}
|
|
472
515
|
async retryTask(taskId) {
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
update tasks
|
|
476
|
-
set status = 'queued', updated_at = now()
|
|
477
|
-
where id = $1 and status = 'failed'
|
|
478
|
-
returning *
|
|
479
|
-
`,
|
|
480
|
-
[taskId]
|
|
481
|
-
);
|
|
482
|
-
if (result.rowCount === 0) {
|
|
483
|
-
return null;
|
|
484
|
-
}
|
|
485
|
-
return mapTask(result.rows[0]);
|
|
516
|
+
const [row] = await this.db.update(tasks).set({ status: "queued", updatedAt: sql2`now()` }).where(and(eq(tasks.id, taskId), eq(tasks.status, "failed"))).returning();
|
|
517
|
+
return row ? toTaskRecord(row) : null;
|
|
486
518
|
}
|
|
487
519
|
async cancelTask(taskId) {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
if (taskResult.
|
|
493
|
-
await client.query("rollback");
|
|
494
|
-
return null;
|
|
495
|
-
}
|
|
520
|
+
return await this.db.transaction(async (tx) => {
|
|
521
|
+
const taskResult = await tx.execute(sql2`
|
|
522
|
+
SELECT * FROM tasks WHERE id = ${taskId} FOR UPDATE
|
|
523
|
+
`);
|
|
524
|
+
if (taskResult.rows.length === 0) return null;
|
|
496
525
|
const taskRow = taskResult.rows[0];
|
|
497
526
|
const taskStatus = String(taskRow.status);
|
|
498
527
|
if (taskStatus === "succeeded" || taskStatus === "failed" || taskStatus === "cancelled") {
|
|
499
|
-
await client.query("rollback");
|
|
500
528
|
return null;
|
|
501
529
|
}
|
|
502
530
|
if (taskStatus === "assigned" || taskStatus === "running") {
|
|
503
|
-
const runResult = await
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
for update
|
|
510
|
-
`,
|
|
511
|
-
[taskId]
|
|
512
|
-
);
|
|
513
|
-
if (runResult.rowCount !== 0) {
|
|
514
|
-
const runStatus = String(runResult.rows[0].status);
|
|
531
|
+
const runResult = await tx.execute(sql2`
|
|
532
|
+
SELECT * FROM runs WHERE task_id = ${taskId} ORDER BY attempt DESC LIMIT 1 FOR UPDATE
|
|
533
|
+
`);
|
|
534
|
+
if (runResult.rows.length > 0) {
|
|
535
|
+
const runRow = runResult.rows[0];
|
|
536
|
+
const runStatus = String(runRow.status);
|
|
515
537
|
if (runStatus !== "succeeded" && runStatus !== "failed" && runStatus !== "cancelled") {
|
|
516
|
-
await
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
`,
|
|
522
|
-
[runResult.rows[0].id]
|
|
523
|
-
);
|
|
538
|
+
await tx.execute(sql2`
|
|
539
|
+
UPDATE runs SET status = 'cancelled', ended_at = now(), updated_at = now(),
|
|
540
|
+
error = COALESCE(error, 'task cancelled')
|
|
541
|
+
WHERE id = ${runRow.id}
|
|
542
|
+
`);
|
|
524
543
|
}
|
|
525
544
|
}
|
|
526
545
|
}
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
set status = 'cancelled', updated_at = now()
|
|
531
|
-
where id = $1
|
|
532
|
-
returning *
|
|
533
|
-
`,
|
|
534
|
-
[taskId]
|
|
535
|
-
);
|
|
536
|
-
await client.query("commit");
|
|
537
|
-
return mapTask(updateTaskResult.rows[0]);
|
|
538
|
-
} catch (error) {
|
|
539
|
-
await client.query("rollback");
|
|
540
|
-
throw error;
|
|
541
|
-
} finally {
|
|
542
|
-
client.release();
|
|
543
|
-
}
|
|
546
|
+
const [updatedTask] = await tx.update(tasks).set({ status: "cancelled", updatedAt: sql2`now()` }).where(eq(tasks.id, taskId)).returning();
|
|
547
|
+
return toTaskRecord(updatedTask);
|
|
548
|
+
});
|
|
544
549
|
}
|
|
545
550
|
async appendInputEvent(runId, input) {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
"select coalesce(max(sequence), 0) + 1 as next_seq from run_input_events where run_id = $1",
|
|
551
|
-
[runId]
|
|
552
|
-
);
|
|
551
|
+
return await this.db.transaction(async (tx) => {
|
|
552
|
+
const seqResult = await tx.execute(sql2`
|
|
553
|
+
SELECT COALESCE(MAX(sequence), 0) + 1 AS next_seq FROM run_input_events WHERE run_id = ${runId}
|
|
554
|
+
`);
|
|
553
555
|
const sequence = Number(seqResult.rows[0].next_seq);
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
values ($1, $2, $3, $4::jsonb)
|
|
558
|
-
returning *
|
|
559
|
-
`,
|
|
560
|
-
[runId, sequence, input.kind, JSON.stringify(input.payload)]
|
|
561
|
-
);
|
|
562
|
-
await client.query("commit");
|
|
563
|
-
return mapInputEvent(result.rows[0]);
|
|
564
|
-
} catch (error) {
|
|
565
|
-
await client.query("rollback");
|
|
566
|
-
throw error;
|
|
567
|
-
} finally {
|
|
568
|
-
client.release();
|
|
569
|
-
}
|
|
556
|
+
const [inserted] = await tx.insert(runInputEvents).values({ runId, sequence, kind: input.kind, payload: input.payload }).returning();
|
|
557
|
+
return toInputEventRecord(inserted);
|
|
558
|
+
});
|
|
570
559
|
}
|
|
571
560
|
async getInputEvents(runId, afterSequence) {
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
[runId, afterSequence]
|
|
575
|
-
);
|
|
576
|
-
return result.rows.map((row) => mapInputEvent(row));
|
|
561
|
+
const rows = await this.db.select().from(runInputEvents).where(and(eq(runInputEvents.runId, runId), gt(runInputEvents.sequence, afterSequence))).orderBy(asc(runInputEvents.sequence));
|
|
562
|
+
return rows.map(toInputEventRecord);
|
|
577
563
|
}
|
|
578
564
|
async markInputDelivered(runId, sequence) {
|
|
579
|
-
await this.
|
|
580
|
-
"update run_input_events set delivered_at = now() where run_id = $1 and sequence = $2",
|
|
581
|
-
[runId, sequence]
|
|
582
|
-
);
|
|
565
|
+
await this.db.update(runInputEvents).set({ deliveredAt: sql2`now()` }).where(and(eq(runInputEvents.runId, runId), eq(runInputEvents.sequence, sequence)));
|
|
583
566
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
// ../db/src/sqlite-store.ts
|
|
570
|
+
import { and as and2, asc as asc2, desc as desc2, eq as eq2, gt as gt2 } from "drizzle-orm";
|
|
571
|
+
|
|
572
|
+
// ../db/src/schema/sqlite.ts
|
|
573
|
+
var sqlite_exports = {};
|
|
574
|
+
__export(sqlite_exports, {
|
|
575
|
+
artifacts: () => artifacts2,
|
|
576
|
+
nodes: () => nodes2,
|
|
577
|
+
runInputEvents: () => runInputEvents2,
|
|
578
|
+
runLogs: () => runLogs2,
|
|
579
|
+
runs: () => runs2,
|
|
580
|
+
tasks: () => tasks2
|
|
581
|
+
});
|
|
582
|
+
import { isNull as isNull2, sql as sql3 } from "drizzle-orm";
|
|
583
|
+
import { index as index2, integer as integer2, sqliteTable, text as text2, uniqueIndex as uniqueIndex2 } from "drizzle-orm/sqlite-core";
|
|
584
|
+
var nowDefault = sql3`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`;
|
|
585
|
+
var tasks2 = sqliteTable("tasks", {
|
|
586
|
+
id: text2("id").primaryKey(),
|
|
587
|
+
kind: text2("kind").notNull(),
|
|
588
|
+
adapter: text2("adapter").notNull(),
|
|
589
|
+
payload: text2("payload", { mode: "json" }).$type().notNull().default({}),
|
|
590
|
+
requires: text2("requires", { mode: "json" }).$type().notNull().default([]),
|
|
591
|
+
status: text2("status").notNull(),
|
|
592
|
+
createdAt: text2("created_at").notNull().default(nowDefault),
|
|
593
|
+
updatedAt: text2("updated_at").notNull().default(nowDefault)
|
|
594
|
+
}, (table) => ({
|
|
595
|
+
statusCreatedAtIdx: index2("idx_tasks_status_created_at").on(table.status, table.createdAt)
|
|
596
|
+
}));
|
|
597
|
+
var nodes2 = sqliteTable("nodes", {
|
|
598
|
+
id: text2("id").primaryKey(),
|
|
599
|
+
name: text2("name").notNull(),
|
|
600
|
+
capabilities: text2("capabilities", { mode: "json" }).$type().notNull().default([]),
|
|
601
|
+
status: text2("status").notNull().default("online"),
|
|
602
|
+
lastHeartbeatAt: text2("last_heartbeat_at"),
|
|
603
|
+
createdAt: text2("created_at").notNull().default(nowDefault),
|
|
604
|
+
updatedAt: text2("updated_at").notNull().default(nowDefault)
|
|
605
|
+
});
|
|
606
|
+
var runs2 = sqliteTable("runs", {
|
|
607
|
+
id: text2("id").primaryKey(),
|
|
608
|
+
taskId: text2("task_id").notNull().references(() => tasks2.id),
|
|
609
|
+
nodeId: text2("node_id").notNull().references(() => nodes2.id),
|
|
610
|
+
attempt: integer2("attempt").notNull(),
|
|
611
|
+
status: text2("status").notNull(),
|
|
612
|
+
startedAt: text2("started_at"),
|
|
613
|
+
endedAt: text2("ended_at"),
|
|
614
|
+
error: text2("error"),
|
|
615
|
+
createdAt: text2("created_at").notNull().default(nowDefault),
|
|
616
|
+
updatedAt: text2("updated_at").notNull().default(nowDefault)
|
|
617
|
+
}, (table) => ({
|
|
618
|
+
taskIdIdx: index2("idx_runs_task_id").on(table.taskId),
|
|
619
|
+
nodeIdIdx: index2("idx_runs_node_id").on(table.nodeId)
|
|
620
|
+
}));
|
|
621
|
+
var runLogs2 = sqliteTable("run_logs", {
|
|
622
|
+
id: integer2("id").primaryKey({ autoIncrement: true }),
|
|
623
|
+
runId: text2("run_id").notNull().references(() => runs2.id),
|
|
624
|
+
stepName: text2("step_name"),
|
|
625
|
+
stream: text2("stream").notNull(),
|
|
626
|
+
message: text2("message").notNull(),
|
|
627
|
+
timestamp: text2("timestamp").notNull().default(nowDefault)
|
|
628
|
+
}, (table) => ({
|
|
629
|
+
runIdTimestampIdx: index2("idx_run_logs_run_id_timestamp").on(table.runId, table.timestamp)
|
|
630
|
+
}));
|
|
631
|
+
var artifacts2 = sqliteTable("artifacts", {
|
|
632
|
+
id: text2("id").primaryKey(),
|
|
633
|
+
runId: text2("run_id").notNull().references(() => runs2.id),
|
|
634
|
+
type: text2("type").notNull(),
|
|
635
|
+
name: text2("name").notNull(),
|
|
636
|
+
path: text2("path").notNull(),
|
|
637
|
+
metadata: text2("metadata", { mode: "json" }).$type(),
|
|
638
|
+
createdAt: text2("created_at").notNull().default(nowDefault)
|
|
639
|
+
}, (table) => ({
|
|
640
|
+
runIdIdx: index2("idx_artifacts_run_id").on(table.runId)
|
|
641
|
+
}));
|
|
642
|
+
var runInputEvents2 = sqliteTable("run_input_events", {
|
|
643
|
+
id: integer2("id").primaryKey({ autoIncrement: true }),
|
|
644
|
+
runId: text2("run_id").notNull().references(() => runs2.id),
|
|
645
|
+
sequence: integer2("sequence").notNull(),
|
|
646
|
+
kind: text2("kind").notNull(),
|
|
647
|
+
payload: text2("payload", { mode: "json" }).$type().notNull().default({}),
|
|
648
|
+
createdAt: text2("created_at").notNull().default(nowDefault),
|
|
649
|
+
deliveredAt: text2("delivered_at")
|
|
650
|
+
}, (table) => ({
|
|
651
|
+
runSequenceUniqueIdx: uniqueIndex2("idx_run_input_events_run_sequence").on(table.runId, table.sequence),
|
|
652
|
+
undeliveredIdx: index2("idx_run_input_events_undelivered").on(table.runId, table.sequence).where(isNull2(table.deliveredAt))
|
|
653
|
+
}));
|
|
654
|
+
|
|
655
|
+
// ../db/src/sqlite-store.ts
|
|
656
|
+
function nowIso() {
|
|
657
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
658
|
+
}
|
|
659
|
+
function toTaskRecord2(row) {
|
|
660
|
+
return {
|
|
661
|
+
id: row.id,
|
|
662
|
+
kind: row.kind,
|
|
663
|
+
adapter: row.adapter,
|
|
664
|
+
payload: row.payload,
|
|
665
|
+
requires: row.requires ?? [],
|
|
666
|
+
status: row.status,
|
|
667
|
+
createdAt: row.createdAt,
|
|
668
|
+
updatedAt: row.updatedAt
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
function toRunRecord2(row) {
|
|
672
|
+
return {
|
|
673
|
+
id: row.id,
|
|
674
|
+
taskId: row.taskId,
|
|
675
|
+
nodeId: row.nodeId,
|
|
676
|
+
attempt: row.attempt,
|
|
677
|
+
status: row.status,
|
|
678
|
+
startedAt: row.startedAt ?? null,
|
|
679
|
+
endedAt: row.endedAt ?? null,
|
|
680
|
+
error: row.error ?? null
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
function toNodeRecord2(row) {
|
|
684
|
+
return {
|
|
685
|
+
id: row.id,
|
|
686
|
+
name: row.name,
|
|
687
|
+
capabilities: row.capabilities ?? [],
|
|
688
|
+
status: row.status,
|
|
689
|
+
lastHeartbeatAt: row.lastHeartbeatAt ?? null
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
function toLogRecord2(row) {
|
|
693
|
+
return {
|
|
694
|
+
id: row.id,
|
|
695
|
+
runId: row.runId,
|
|
696
|
+
stepName: row.stepName ?? null,
|
|
697
|
+
stream: row.stream,
|
|
698
|
+
message: row.message,
|
|
699
|
+
timestamp: row.timestamp
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
function toArtifactRecord2(row) {
|
|
703
|
+
return {
|
|
704
|
+
id: row.id,
|
|
705
|
+
runId: row.runId,
|
|
706
|
+
type: row.type,
|
|
707
|
+
name: row.name,
|
|
708
|
+
path: row.path,
|
|
709
|
+
metadata: row.metadata ?? null,
|
|
710
|
+
createdAt: row.createdAt
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
function toInputEventRecord2(row) {
|
|
714
|
+
return {
|
|
715
|
+
id: row.id,
|
|
716
|
+
runId: row.runId,
|
|
717
|
+
sequence: row.sequence,
|
|
718
|
+
kind: row.kind,
|
|
719
|
+
payload: row.payload,
|
|
720
|
+
createdAt: row.createdAt,
|
|
721
|
+
deliveredAt: row.deliveredAt ?? null
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
function mapRawTask2(row) {
|
|
725
|
+
return {
|
|
726
|
+
id: row.id,
|
|
727
|
+
kind: row.kind,
|
|
728
|
+
adapter: row.adapter,
|
|
729
|
+
payload: JSON.parse(row.payload),
|
|
730
|
+
requires: JSON.parse(row.requires),
|
|
731
|
+
status: row.status,
|
|
732
|
+
createdAt: row.created_at,
|
|
733
|
+
updatedAt: row.updated_at
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
function mapRawRun2(row) {
|
|
737
|
+
return {
|
|
738
|
+
id: row.id,
|
|
739
|
+
taskId: row.task_id,
|
|
740
|
+
nodeId: row.node_id,
|
|
741
|
+
attempt: row.attempt,
|
|
742
|
+
status: row.status,
|
|
743
|
+
startedAt: row.started_at ?? null,
|
|
744
|
+
endedAt: row.ended_at ?? null,
|
|
745
|
+
error: row.error ?? null
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
var SqliteStore = class {
|
|
749
|
+
constructor(db) {
|
|
750
|
+
this.db = db;
|
|
751
|
+
}
|
|
752
|
+
db;
|
|
753
|
+
async createTask(input) {
|
|
754
|
+
const now = nowIso();
|
|
755
|
+
const [row] = await this.db.insert(tasks2).values({
|
|
756
|
+
id: makeId("task"),
|
|
757
|
+
kind: input.kind,
|
|
758
|
+
adapter: input.adapter,
|
|
759
|
+
payload: input.payload,
|
|
760
|
+
requires: input.requires ?? [],
|
|
761
|
+
status: "queued",
|
|
762
|
+
createdAt: now,
|
|
763
|
+
updatedAt: now
|
|
764
|
+
}).returning();
|
|
765
|
+
return toTaskRecord2(row);
|
|
766
|
+
}
|
|
767
|
+
async listTasks(status) {
|
|
768
|
+
const rows = await this.db.select().from(tasks2).where(status ? eq2(tasks2.status, status) : void 0).orderBy(desc2(tasks2.createdAt)).limit(100);
|
|
769
|
+
return rows.map(toTaskRecord2);
|
|
770
|
+
}
|
|
771
|
+
async getTask(taskId) {
|
|
772
|
+
const [row] = await this.db.select().from(tasks2).where(eq2(tasks2.id, taskId));
|
|
773
|
+
return row ? toTaskRecord2(row) : null;
|
|
774
|
+
}
|
|
775
|
+
async listRuns(filters) {
|
|
776
|
+
const conds = [];
|
|
777
|
+
if (filters?.taskId) conds.push(eq2(runs2.taskId, filters.taskId));
|
|
778
|
+
if (filters?.status) conds.push(eq2(runs2.status, filters.status));
|
|
779
|
+
const rows = await this.db.select().from(runs2).where(conds.length ? and2(...conds) : void 0).orderBy(desc2(runs2.createdAt)).limit(100);
|
|
780
|
+
return rows.map(toRunRecord2);
|
|
781
|
+
}
|
|
782
|
+
async getRun(runId) {
|
|
783
|
+
const [row] = await this.db.select().from(runs2).where(eq2(runs2.id, runId));
|
|
784
|
+
return row ? toRunRecord2(row) : null;
|
|
785
|
+
}
|
|
786
|
+
async registerNode(name, capabilities, preferredId) {
|
|
787
|
+
const now = nowIso();
|
|
788
|
+
const [existing] = await this.db.select().from(nodes2).where(eq2(nodes2.name, name));
|
|
789
|
+
if (existing) {
|
|
790
|
+
const [updated] = await this.db.update(nodes2).set({ capabilities, status: "online", lastHeartbeatAt: now, updatedAt: now }).where(eq2(nodes2.id, existing.id)).returning();
|
|
791
|
+
return toNodeRecord2(updated);
|
|
792
|
+
}
|
|
793
|
+
const [inserted] = await this.db.insert(nodes2).values({
|
|
794
|
+
id: preferredId ?? makeId("node"),
|
|
795
|
+
name,
|
|
796
|
+
capabilities,
|
|
797
|
+
status: "online",
|
|
798
|
+
lastHeartbeatAt: now,
|
|
799
|
+
createdAt: now,
|
|
800
|
+
updatedAt: now
|
|
801
|
+
}).returning();
|
|
802
|
+
return toNodeRecord2(inserted);
|
|
803
|
+
}
|
|
804
|
+
async getRunCancellationState(runId) {
|
|
805
|
+
const [row] = await this.db.select({ runStatus: runs2.status, taskStatus: tasks2.status }).from(runs2).innerJoin(tasks2, eq2(tasks2.id, runs2.taskId)).where(eq2(runs2.id, runId));
|
|
806
|
+
return row ? { runStatus: row.runStatus, taskStatus: row.taskStatus ?? "" } : null;
|
|
807
|
+
}
|
|
808
|
+
// Transaction methods use this.db.$client directly because better-sqlite3
|
|
809
|
+
// rejects async callbacks — all operations inside must be synchronous.
|
|
810
|
+
async pollNode(nodeId, capabilities) {
|
|
811
|
+
const now = nowIso();
|
|
812
|
+
await this.db.update(nodes2).set({ capabilities, status: "online", lastHeartbeatAt: now, updatedAt: now }).where(eq2(nodes2.id, nodeId));
|
|
813
|
+
const raw = this.db.$client;
|
|
814
|
+
return raw.transaction(() => {
|
|
815
|
+
const capSet = new Set(capabilities);
|
|
816
|
+
const queued = raw.prepare(
|
|
817
|
+
"SELECT * FROM tasks WHERE status = 'queued' ORDER BY created_at ASC"
|
|
818
|
+
).all();
|
|
819
|
+
const task = queued.find(
|
|
820
|
+
(t) => JSON.parse(t.requires).every((r) => capSet.has(r))
|
|
821
|
+
);
|
|
822
|
+
if (!task) return null;
|
|
823
|
+
const { max_attempt } = raw.prepare(
|
|
824
|
+
"SELECT COALESCE(MAX(attempt), 0) AS max_attempt FROM runs WHERE task_id = ?"
|
|
825
|
+
).get(task.id);
|
|
826
|
+
const runId = makeId("run");
|
|
827
|
+
raw.prepare(
|
|
828
|
+
"INSERT INTO runs (id, task_id, node_id, attempt, status, started_at, created_at, updated_at) VALUES (?, ?, ?, ?, 'assigned', ?, ?, ?)"
|
|
829
|
+
).run(runId, task.id, nodeId, max_attempt + 1, now, now, now);
|
|
830
|
+
raw.prepare("UPDATE tasks SET status = 'assigned', updated_at = ? WHERE id = ?").run(now, task.id);
|
|
831
|
+
return {
|
|
832
|
+
task: mapRawTask2(task),
|
|
833
|
+
run: mapRawRun2(raw.prepare("SELECT * FROM runs WHERE id = ?").get(runId))
|
|
834
|
+
};
|
|
835
|
+
})();
|
|
836
|
+
}
|
|
837
|
+
async updateRunStatus(runId, status, error) {
|
|
838
|
+
const raw = this.db.$client;
|
|
839
|
+
return raw.transaction(() => {
|
|
840
|
+
const current = raw.prepare(
|
|
841
|
+
"SELECT r.*, t.status AS task_status FROM runs r JOIN tasks t ON t.id = r.task_id WHERE r.id = ?"
|
|
842
|
+
).get(runId);
|
|
843
|
+
if (!current) return null;
|
|
844
|
+
if (current.task_status === "cancelled" && status !== "cancelled") {
|
|
845
|
+
return mapRawRun2(current);
|
|
846
|
+
}
|
|
847
|
+
const now = nowIso();
|
|
848
|
+
const isTerminal = status === "succeeded" || status === "failed" || status === "cancelled";
|
|
849
|
+
if (isTerminal) {
|
|
850
|
+
raw.prepare(
|
|
851
|
+
"UPDATE runs SET status = ?, error = ?, ended_at = ?, updated_at = ? WHERE id = ?"
|
|
852
|
+
).run(status, error ?? null, now, now, runId);
|
|
853
|
+
} else {
|
|
854
|
+
raw.prepare(
|
|
855
|
+
"UPDATE runs SET status = ?, error = ?, updated_at = ? WHERE id = ?"
|
|
856
|
+
).run(status, error ?? null, now, runId);
|
|
857
|
+
}
|
|
858
|
+
const updatedRun = raw.prepare("SELECT * FROM runs WHERE id = ?").get(runId);
|
|
859
|
+
const nextTaskStatus = isTerminal ? status : "running";
|
|
860
|
+
if (current.task_status !== "cancelled" || status === "cancelled") {
|
|
861
|
+
raw.prepare("UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?").run(
|
|
862
|
+
nextTaskStatus,
|
|
863
|
+
now,
|
|
864
|
+
updatedRun.task_id
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
return mapRawRun2(updatedRun);
|
|
868
|
+
})();
|
|
869
|
+
}
|
|
870
|
+
async appendRunLogs(runId, logs) {
|
|
871
|
+
if (logs.length === 0) return 0;
|
|
872
|
+
const now = nowIso();
|
|
873
|
+
await this.db.insert(runLogs2).values(
|
|
874
|
+
logs.map((log) => ({
|
|
875
|
+
runId,
|
|
876
|
+
stepName: log.stepName ?? null,
|
|
877
|
+
stream: log.stream,
|
|
878
|
+
message: log.message,
|
|
879
|
+
timestamp: now
|
|
880
|
+
}))
|
|
592
881
|
);
|
|
882
|
+
return logs.length;
|
|
883
|
+
}
|
|
884
|
+
async getRunLogs(runId) {
|
|
885
|
+
const rows = await this.db.select().from(runLogs2).where(eq2(runLogs2.runId, runId)).orderBy(asc2(runLogs2.timestamp));
|
|
886
|
+
return rows.map(toLogRecord2);
|
|
887
|
+
}
|
|
888
|
+
async createArtifact(runId, input) {
|
|
889
|
+
const now = nowIso();
|
|
890
|
+
const [row] = await this.db.insert(artifacts2).values({
|
|
891
|
+
id: makeId("artifact"),
|
|
892
|
+
runId,
|
|
893
|
+
type: input.type,
|
|
894
|
+
name: input.name,
|
|
895
|
+
path: input.path,
|
|
896
|
+
metadata: input.metadata ?? null,
|
|
897
|
+
createdAt: now
|
|
898
|
+
}).returning();
|
|
899
|
+
return toArtifactRecord2(row);
|
|
900
|
+
}
|
|
901
|
+
async listRunArtifacts(runId) {
|
|
902
|
+
const rows = await this.db.select().from(artifacts2).where(eq2(artifacts2.runId, runId)).orderBy(asc2(artifacts2.createdAt));
|
|
903
|
+
return rows.map(toArtifactRecord2);
|
|
904
|
+
}
|
|
905
|
+
async retryTask(taskId) {
|
|
906
|
+
const [row] = await this.db.update(tasks2).set({ status: "queued", updatedAt: nowIso() }).where(and2(eq2(tasks2.id, taskId), eq2(tasks2.status, "failed"))).returning();
|
|
907
|
+
return row ? toTaskRecord2(row) : null;
|
|
908
|
+
}
|
|
909
|
+
async cancelTask(taskId) {
|
|
910
|
+
const raw = this.db.$client;
|
|
911
|
+
return raw.transaction(() => {
|
|
912
|
+
const taskRow = raw.prepare("SELECT * FROM tasks WHERE id = ?").get(taskId);
|
|
913
|
+
if (!taskRow) return null;
|
|
914
|
+
const terminal = /* @__PURE__ */ new Set(["succeeded", "failed", "cancelled"]);
|
|
915
|
+
if (terminal.has(taskRow.status)) return null;
|
|
916
|
+
const now = nowIso();
|
|
917
|
+
if (taskRow.status === "assigned" || taskRow.status === "running") {
|
|
918
|
+
const latestRun = raw.prepare(
|
|
919
|
+
"SELECT * FROM runs WHERE task_id = ? ORDER BY attempt DESC LIMIT 1"
|
|
920
|
+
).get(taskId);
|
|
921
|
+
if (latestRun && !terminal.has(latestRun.status)) {
|
|
922
|
+
raw.prepare(
|
|
923
|
+
"UPDATE runs SET status = 'cancelled', ended_at = ?, updated_at = ?, error = COALESCE(error, 'task cancelled') WHERE id = ?"
|
|
924
|
+
).run(now, now, latestRun.id);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
raw.prepare("UPDATE tasks SET status = 'cancelled', updated_at = ? WHERE id = ?").run(now, taskId);
|
|
928
|
+
return mapRawTask2(raw.prepare("SELECT * FROM tasks WHERE id = ?").get(taskId));
|
|
929
|
+
})();
|
|
930
|
+
}
|
|
931
|
+
async appendInputEvent(runId, input) {
|
|
932
|
+
const raw = this.db.$client;
|
|
933
|
+
const now = nowIso();
|
|
934
|
+
const event = raw.transaction(() => {
|
|
935
|
+
const { next_seq } = raw.prepare(
|
|
936
|
+
"SELECT COALESCE(MAX(sequence), 0) + 1 AS next_seq FROM run_input_events WHERE run_id = ?"
|
|
937
|
+
).get(runId);
|
|
938
|
+
raw.prepare(
|
|
939
|
+
"INSERT INTO run_input_events (run_id, sequence, kind, payload, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
940
|
+
).run(runId, next_seq, input.kind, JSON.stringify(input.payload), now);
|
|
941
|
+
return raw.prepare(
|
|
942
|
+
"SELECT * FROM run_input_events WHERE run_id = ? AND sequence = ?"
|
|
943
|
+
).get(runId, next_seq);
|
|
944
|
+
})();
|
|
945
|
+
return {
|
|
946
|
+
id: event.id,
|
|
947
|
+
runId: event.run_id,
|
|
948
|
+
sequence: event.sequence,
|
|
949
|
+
kind: event.kind,
|
|
950
|
+
payload: JSON.parse(event.payload),
|
|
951
|
+
createdAt: event.created_at,
|
|
952
|
+
deliveredAt: event.delivered_at ?? null
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
async getInputEvents(runId, afterSequence) {
|
|
956
|
+
const rows = await this.db.select().from(runInputEvents2).where(and2(eq2(runInputEvents2.runId, runId), gt2(runInputEvents2.sequence, afterSequence))).orderBy(asc2(runInputEvents2.sequence));
|
|
957
|
+
return rows.map(toInputEventRecord2);
|
|
958
|
+
}
|
|
959
|
+
async markInputDelivered(runId, sequence) {
|
|
960
|
+
await this.db.update(runInputEvents2).set({ deliveredAt: nowIso() }).where(and2(eq2(runInputEvents2.runId, runId), eq2(runInputEvents2.sequence, sequence)));
|
|
593
961
|
}
|
|
594
962
|
};
|
|
595
963
|
|
|
964
|
+
// ../db/src/client.ts
|
|
965
|
+
var pools = /* @__PURE__ */ new Map();
|
|
966
|
+
function getPool(databaseUrl) {
|
|
967
|
+
const existing = pools.get(databaseUrl);
|
|
968
|
+
if (existing) return existing;
|
|
969
|
+
const pool = new Pool({ connectionString: databaseUrl });
|
|
970
|
+
pools.set(databaseUrl, pool);
|
|
971
|
+
return pool;
|
|
972
|
+
}
|
|
973
|
+
function getDrizzle(pool) {
|
|
974
|
+
return drizzlePg(pool, { schema: pg_exports });
|
|
975
|
+
}
|
|
976
|
+
function parseSqlitePath(url) {
|
|
977
|
+
const path = url.startsWith("sqlite://") ? url.slice("sqlite://".length) : url;
|
|
978
|
+
if (!path) return join2(homedir2(), ".workplane", "workplane.db");
|
|
979
|
+
if (path.startsWith("~/")) return join2(homedir2(), path.slice(2));
|
|
980
|
+
return path;
|
|
981
|
+
}
|
|
982
|
+
function createStore(databaseUrl) {
|
|
983
|
+
const url = databaseUrl ?? "";
|
|
984
|
+
if (url.startsWith("postgres://") || url.startsWith("postgresql://")) {
|
|
985
|
+
const pool = getPool(url);
|
|
986
|
+
return {
|
|
987
|
+
store: new PgStore(getDrizzle(pool)),
|
|
988
|
+
close: () => pool.end()
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
const filePath = parseSqlitePath(url);
|
|
992
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
993
|
+
const sqlite = new Database(filePath);
|
|
994
|
+
sqlite.pragma("journal_mode = WAL");
|
|
995
|
+
sqlite.pragma("synchronous = NORMAL");
|
|
996
|
+
const db = drizzleSqlite(sqlite, { schema: sqlite_exports });
|
|
997
|
+
return {
|
|
998
|
+
store: new SqliteStore(db),
|
|
999
|
+
close: async () => {
|
|
1000
|
+
sqlite.close();
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
|
|
596
1005
|
// ../server/src/workflows-vanilla.ts
|
|
597
1006
|
var VanillaWorkflows = class {
|
|
598
1007
|
constructor(store) {
|
|
@@ -856,8 +1265,7 @@ async function buildWorkflows(store, config) {
|
|
|
856
1265
|
}
|
|
857
1266
|
async function main() {
|
|
858
1267
|
const config = loadServerConfig();
|
|
859
|
-
const
|
|
860
|
-
const store = new PgStore(pool);
|
|
1268
|
+
const { store, close: closeStore } = createStore(config.databaseUrl);
|
|
861
1269
|
const { workflows, shutdown: shutdownWorkflows } = await buildWorkflows(store, config);
|
|
862
1270
|
const server = createServer(async (req, res) => {
|
|
863
1271
|
try {
|
|
@@ -889,8 +1297,8 @@ async function main() {
|
|
|
889
1297
|
}
|
|
890
1298
|
if (req.method === "GET" && url.pathname === "/tasks") {
|
|
891
1299
|
const status = parseStatus(url.searchParams.get("status"));
|
|
892
|
-
const
|
|
893
|
-
writeJson(res, 200, { tasks });
|
|
1300
|
+
const tasks3 = await store.listTasks(status);
|
|
1301
|
+
writeJson(res, 200, { tasks: tasks3 });
|
|
894
1302
|
return;
|
|
895
1303
|
}
|
|
896
1304
|
if (req.method === "GET" && /^\/tasks\/[^/]+$/.test(url.pathname)) {
|
|
@@ -926,8 +1334,8 @@ async function main() {
|
|
|
926
1334
|
if (req.method === "GET" && url.pathname === "/runs") {
|
|
927
1335
|
const taskId = url.searchParams.get("taskId") ?? void 0;
|
|
928
1336
|
const status = parseStatus(url.searchParams.get("status"));
|
|
929
|
-
const
|
|
930
|
-
writeJson(res, 200, { runs });
|
|
1337
|
+
const runs3 = await store.listRuns({ taskId, status });
|
|
1338
|
+
writeJson(res, 200, { runs: runs3 });
|
|
931
1339
|
return;
|
|
932
1340
|
}
|
|
933
1341
|
if (req.method === "GET" && /^\/runs\/[^/]+$/.test(url.pathname)) {
|
|
@@ -948,8 +1356,8 @@ async function main() {
|
|
|
948
1356
|
}
|
|
949
1357
|
if (req.method === "GET" && /^\/runs\/[^/]+\/artifacts$/.test(url.pathname)) {
|
|
950
1358
|
const runId = url.pathname.split("/")[2];
|
|
951
|
-
const
|
|
952
|
-
writeJson(res, 200, { artifacts });
|
|
1359
|
+
const artifacts3 = await store.listRunArtifacts(runId);
|
|
1360
|
+
writeJson(res, 200, { artifacts: artifacts3 });
|
|
953
1361
|
return;
|
|
954
1362
|
}
|
|
955
1363
|
if (req.method === "POST" && url.pathname === "/nodes/register") {
|
|
@@ -1042,7 +1450,7 @@ async function main() {
|
|
|
1042
1450
|
});
|
|
1043
1451
|
const shutdown = async () => {
|
|
1044
1452
|
await shutdownWorkflows();
|
|
1045
|
-
await
|
|
1453
|
+
await closeStore();
|
|
1046
1454
|
server.close();
|
|
1047
1455
|
process.exit(0);
|
|
1048
1456
|
};
|