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/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().min(1),
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
- var pools = /* @__PURE__ */ new Map();
176
- function getPool(databaseUrl) {
177
- const existing = pools.get(databaseUrl);
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
- // ../server/src/store.ts
193
- function mapTask(row) {
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 mapRun(row) {
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(pool) {
241
- this.pool = pool;
367
+ constructor(db) {
368
+ this.db = db;
242
369
  }
243
- pool;
370
+ db;
244
371
  async createTask(input) {
245
372
  const taskId = makeId("task");
246
- const result = await this.pool.query(
247
- `
248
- insert into tasks (id, kind, adapter, payload, requires, status)
249
- values ($1, $2, $3, $4::jsonb, $5::text[], 'queued')
250
- returning *
251
- `,
252
- [taskId, input.kind, input.adapter, JSON.stringify(input.payload), input.requires ?? []]
253
- );
254
- return mapTask(result.rows[0]);
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 result = status ? await this.pool.query("select * from tasks where status = $1 order by created_at desc limit 100", [status]) : await this.pool.query("select * from tasks order by created_at desc limit 100");
258
- return result.rows.map((row) => mapTask(row));
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 result = await this.pool.query("select * from tasks where id = $1", [taskId]);
262
- if (result.rowCount === 0) {
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 taskId = filters?.taskId;
269
- const status = filters?.status;
270
- const result = taskId && status ? await this.pool.query("select * from runs where task_id = $1 and status = $2 order by created_at desc", [
271
- taskId,
272
- status
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 result = await this.pool.query("select * from runs where id = $1", [runId]);
278
- if (result.rowCount === 0) {
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.pool.query("select * from nodes where name = $1", [name]);
285
- if (existing.rowCount && existing.rowCount > 0) {
286
- const row = existing.rows[0];
287
- const nodeId2 = String(row.id);
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 result = await this.pool.query(
301
- `
302
- insert into nodes (id, name, capabilities, status, last_heartbeat_at)
303
- values ($1, $2, $3::text[], 'online', now())
304
- returning *
305
- `,
306
- [nodeId, name, capabilities]
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 result = await this.pool.query(
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
- const client = await this.pool.connect();
340
- try {
341
- await client.query("begin");
342
- await this.touchNode(client, nodeId, capabilities);
343
- const taskResult = await client.query(
344
- `
345
- select *
346
- from tasks
347
- where status = 'queued'
348
- and requires <@ $1::text[]
349
- order by created_at asc
350
- limit 1
351
- for update skip locked
352
- `,
353
- [capabilities]
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 runId = makeId("run");
362
- const attemptResult = await client.query("select coalesce(max(attempt), 0) as max_attempt from runs where task_id = $1", [
363
- taskId
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 runResult = await client.query(
367
- `
368
- insert into runs (id, task_id, node_id, attempt, status, started_at)
369
- values ($1, $2, $3, $4, 'assigned', now())
370
- returning *
371
- `,
372
- [runId, taskId, nodeId, attempt]
373
- );
374
- await client.query("update tasks set status = 'assigned', updated_at = now() where id = $1", [taskId]);
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: mapTask(taskRow),
378
- run: mapRun(runResult.rows[0])
454
+ task: mapRawTask(taskRow),
455
+ run: mapRawRun(runResult.rows[0])
379
456
  };
380
- } catch (error) {
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
- const client = await this.pool.connect();
389
- try {
390
- await client.query("begin");
391
- const currentResult = await client.query(
392
- `
393
- select r.*, t.status as task_status
394
- from runs r
395
- join tasks t on t.id = r.task_id
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
- await client.query("commit");
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 run = mapRun(runResult.rows[0]);
425
- const nextTaskStatus = status === "succeeded" || status === "failed" || status === "cancelled" ? status : "running";
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 client.query("update tasks set status = $2, updated_at = now() where id = $1", [run.taskId, nextTaskStatus]);
478
+ await tx.update(tasks).set({ status: nextTaskStatus, updatedAt: sql2`now()` }).where(eq(tasks.id, updatedRun.taskId));
428
479
  }
429
- await client.query("commit");
430
- return run;
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
- let inserted = 0;
440
- for (const log of logs) {
441
- await this.pool.query(
442
- `
443
- insert into run_logs (run_id, step_name, stream, message)
444
- values ($1, $2, $3, $4)
445
- `,
446
- [runId, log.stepName ?? null, log.stream, log.message]
447
- );
448
- inserted += 1;
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 result = await this.pool.query("select * from run_logs where run_id = $1 order by timestamp asc", [runId]);
454
- return result.rows;
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 result = await this.pool.query(
459
- `
460
- insert into artifacts (id, run_id, type, name, path, metadata)
461
- values ($1, $2, $3, $4, $5, $6::jsonb)
462
- returning *
463
- `,
464
- [artifactId, runId, input.type, input.name, input.path, input.metadata ? JSON.stringify(input.metadata) : null]
465
- );
466
- return mapArtifact(result.rows[0]);
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 result = await this.pool.query("select * from artifacts where run_id = $1 order by created_at asc", [runId]);
470
- return result.rows.map((row) => mapArtifact(row));
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 result = await this.pool.query(
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
- const client = await this.pool.connect();
489
- try {
490
- await client.query("begin");
491
- const taskResult = await client.query("select * from tasks where id = $1 for update", [taskId]);
492
- if (taskResult.rowCount === 0) {
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 client.query(
504
- `
505
- select * from runs
506
- where task_id = $1
507
- order by attempt desc
508
- limit 1
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 client.query(
517
- `
518
- update runs
519
- set status = 'cancelled', ended_at = now(), updated_at = now(), error = coalesce(error, 'task cancelled')
520
- where id = $1
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 updateTaskResult = await client.query(
528
- `
529
- update tasks
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
- const client = await this.pool.connect();
547
- try {
548
- await client.query("begin");
549
- const seqResult = await client.query(
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 result = await client.query(
555
- `
556
- insert into run_input_events (run_id, sequence, kind, payload)
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 result = await this.pool.query(
573
- "select * from run_input_events where run_id = $1 and sequence > $2 order by sequence asc",
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.pool.query(
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
- async touchNode(client, nodeId, capabilities) {
585
- await client.query(
586
- `
587
- update nodes
588
- set capabilities = $2::text[], status = 'online', last_heartbeat_at = now(), updated_at = now()
589
- where id = $1
590
- `,
591
- [nodeId, capabilities]
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 pool = getPool(config.databaseUrl);
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 tasks = await store.listTasks(status);
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 runs = await store.listRuns({ taskId, status });
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 artifacts = await store.listRunArtifacts(runId);
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 pool.end();
1453
+ await closeStore();
1046
1454
  server.close();
1047
1455
  process.exit(0);
1048
1456
  };