@hasna/loops 0.3.48 → 0.3.50

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.
@@ -623,6 +623,8 @@ function writeWorkflowRunManifest(args) {
623
623
  var DEFAULT_RECOVERY_BATCH_LIMIT = 100;
624
624
  var DEFAULT_RECOVERY_SCAN_MULTIPLIER = 5;
625
625
  var LIVE_EXPIRED_RUN_GRACE_MS = 60000;
626
+ var GENERATED_ROUTE_TEMPLATE_IDS = new Set(["todos-task-worker-verifier", "event-worker-verifier"]);
627
+ var GENERATED_ROUTE_KEYS = new Set(["todos-task", "generic-event"]);
626
628
  function rowToLoop(row) {
627
629
  return {
628
630
  id: row.id,
@@ -1443,10 +1445,23 @@ class Store {
1443
1445
  })();
1444
1446
  }
1445
1447
  listWorkflows(opts = {}) {
1446
- const limit = opts.limit ?? 200;
1447
- const rows = opts.status ? this.db.query("SELECT * FROM workflow_specs WHERE status = ? ORDER BY updated_at DESC LIMIT ?").all(opts.status, limit) : this.db.query("SELECT * FROM workflow_specs ORDER BY status ASC, updated_at DESC LIMIT ?").all(limit);
1448
+ const offset = Math.max(0, opts.offset ?? 0);
1449
+ let rows;
1450
+ if (opts.status && opts.limit !== undefined) {
1451
+ rows = this.db.query("SELECT * FROM workflow_specs WHERE status = ? ORDER BY updated_at DESC LIMIT ? OFFSET ?").all(opts.status, opts.limit, offset);
1452
+ } else if (opts.status) {
1453
+ rows = this.db.query("SELECT * FROM workflow_specs WHERE status = ? ORDER BY updated_at DESC LIMIT -1 OFFSET ?").all(opts.status, offset);
1454
+ } else if (opts.limit !== undefined) {
1455
+ rows = this.db.query("SELECT * FROM workflow_specs ORDER BY status ASC, updated_at DESC LIMIT ? OFFSET ?").all(opts.limit, offset);
1456
+ } else {
1457
+ rows = this.db.query("SELECT * FROM workflow_specs ORDER BY status ASC, updated_at DESC LIMIT -1 OFFSET ?").all(offset);
1458
+ }
1448
1459
  return rows.map(rowToWorkflow);
1449
1460
  }
1461
+ countWorkflows(opts = {}) {
1462
+ const row = opts.status ? this.db.query("SELECT COUNT(*) AS count FROM workflow_specs WHERE status = ?").get(opts.status) : this.db.query("SELECT COUNT(*) AS count FROM workflow_specs").get();
1463
+ return row?.count ?? 0;
1464
+ }
1450
1465
  archiveWorkflow(idOrName) {
1451
1466
  const workflow = this.requireWorkflow(idOrName);
1452
1467
  const updated = nowIso();
@@ -1456,6 +1471,64 @@ class Store {
1456
1471
  throw new Error(`workflow not found after archive: ${workflow.id}`);
1457
1472
  return archived;
1458
1473
  }
1474
+ generatedRouteArchiveContext(args) {
1475
+ if (!args.loopId || !args.workItemId)
1476
+ return;
1477
+ const workItem = this.getWorkflowWorkItem(args.workItemId);
1478
+ if (!workItem || !GENERATED_ROUTE_KEYS.has(workItem.routeKey))
1479
+ return;
1480
+ const invocation = this.getWorkflowInvocation(workItem.invocationId);
1481
+ if (!invocation?.templateId || !GENERATED_ROUTE_TEMPLATE_IDS.has(invocation.templateId))
1482
+ return;
1483
+ const loop = this.getLoop(args.loopId);
1484
+ if (!loop || loop.schedule.type !== "once" || loop.target.type !== "workflow" || loop.target.workflowId !== args.workflowId)
1485
+ return;
1486
+ const input = loop.target.input ?? {};
1487
+ if (input.workflowWorkItemId && input.workflowWorkItemId !== workItem.id)
1488
+ return;
1489
+ if (input.workflowInvocationId && input.workflowInvocationId !== invocation.id)
1490
+ return;
1491
+ if (workItem.workflowId && workItem.workflowId !== args.workflowId)
1492
+ return;
1493
+ const workflow = this.getWorkflow(args.workflowId);
1494
+ if (!workflow)
1495
+ return;
1496
+ return { workflow, loop, workItem, invocation };
1497
+ }
1498
+ maybeArchiveGeneratedRouteWorkflow(args) {
1499
+ const context = this.generatedRouteArchiveContext(args);
1500
+ if (!context)
1501
+ return;
1502
+ const { workflow, loop, workItem } = context;
1503
+ if (!workflow || workflow.status !== "active")
1504
+ return;
1505
+ const nonTerminal = this.db.query(`SELECT COUNT(*) AS count FROM workflow_runs
1506
+ WHERE workflow_id = ? AND status NOT IN ('succeeded', 'failed', 'timed_out', 'cancelled')`).get(args.workflowId)?.count ?? 0;
1507
+ if (nonTerminal > 0)
1508
+ return;
1509
+ const res = this.db.query("UPDATE workflow_specs SET status='archived', updated_at=? WHERE id=? AND status='active'").run(args.updated, args.workflowId);
1510
+ if (res.changes === 1 && args.workflowRunId) {
1511
+ this.appendWorkflowEvent(args.workflowRunId, "workflow_archived", undefined, {
1512
+ workflowId: args.workflowId,
1513
+ loopId: loop.id,
1514
+ workItemId: workItem.id,
1515
+ routeKey: workItem.routeKey,
1516
+ reason: "terminal generated one-shot route workflow"
1517
+ });
1518
+ }
1519
+ }
1520
+ maybeArchiveTerminalGeneratedRouteWorkflow(workflowRunId, updated) {
1521
+ const run = this.getWorkflowRun(workflowRunId);
1522
+ if (!run)
1523
+ return;
1524
+ this.maybeArchiveGeneratedRouteWorkflow({
1525
+ workflowId: run.workflowId,
1526
+ loopId: run.loopId,
1527
+ workItemId: run.workItemId,
1528
+ workflowRunId,
1529
+ updated
1530
+ });
1531
+ }
1459
1532
  createWorkflowInvocation(input) {
1460
1533
  const now = nowIso();
1461
1534
  const sourceDedupeKey = input.sourceRef.dedupeKey ?? undefined;
@@ -2241,6 +2314,7 @@ class Store {
2241
2314
  if (changed) {
2242
2315
  const itemStatus = status === "succeeded" ? "succeeded" : status === "cancelled" ? "cancelled" : "failed";
2243
2316
  this.setWorkflowWorkItemsForWorkflowRun(workflowRunId, itemStatus, patch.error, finishedAt);
2317
+ this.maybeArchiveTerminalGeneratedRouteWorkflow(workflowRunId, finishedAt);
2244
2318
  }
2245
2319
  this.db.exec("COMMIT");
2246
2320
  } catch (error) {
@@ -2268,6 +2342,7 @@ class Store {
2268
2342
  WHERE workflow_run_id=$workflowRunId AND status IN ('pending', 'running')`).run({ $workflowRunId: workflowRunId, $finished: now, $reason: reason, $updated: now });
2269
2343
  this.setWorkflowWorkItemsForWorkflowRun(workflowRunId, "cancelled", reason, now);
2270
2344
  this.appendWorkflowEvent(workflowRunId, "cancelled", undefined, { reason });
2345
+ this.maybeArchiveTerminalGeneratedRouteWorkflow(workflowRunId, now);
2271
2346
  }
2272
2347
  this.db.exec("COMMIT");
2273
2348
  return this.requireWorkflowRun(workflowRunId);
@@ -2520,8 +2595,20 @@ class Store {
2520
2595
  throw new Error(`run not found after finalize: ${id}`);
2521
2596
  if (opts.claimedBy && res.changes !== 1)
2522
2597
  return run;
2523
- if (res.changes === 1)
2598
+ if (res.changes === 1) {
2524
2599
  this.setWorkflowWorkItemsForLoopRun(run, patch.error, finishedAt);
2600
+ const loop = this.getLoop(run.loopId);
2601
+ const itemStatus = workItemStatusForLoopRun(run.status, run.attempt, loop?.maxAttempts);
2602
+ if (loop?.target.type === "workflow" && itemStatus && itemStatus !== "admitted") {
2603
+ const workItemId = loop.target.input?.workflowWorkItemId ?? loop.target.input?.workItemId;
2604
+ this.maybeArchiveGeneratedRouteWorkflow({
2605
+ workflowId: loop.target.workflowId,
2606
+ loopId: loop.id,
2607
+ workItemId,
2608
+ updated: finishedAt
2609
+ });
2610
+ }
2611
+ }
2525
2612
  return run;
2526
2613
  }
2527
2614
  heartbeatRunLease(id, claimedBy, leaseMs, now = new Date, opts = {}) {
@@ -2642,6 +2729,7 @@ class Store {
2642
2729
  loopRunId: row.id
2643
2730
  });
2644
2731
  this.setWorkflowWorkItemsForWorkflowRun(workflowRow.id, "failed", "parent loop run lease expired before completion", finished);
2732
+ this.maybeArchiveTerminalGeneratedRouteWorkflow(workflowRow.id, finished);
2645
2733
  }
2646
2734
  const loop = this.getLoop(row.loop_id);
2647
2735
  const itemStatus = workItemStatusForLoopRun("abandoned", row.attempt, loop?.maxAttempts);
@@ -2649,6 +2737,15 @@ class Store {
2649
2737
  const statuses = itemStatus === "admitted" ? ["admitted", "running", "failed"] : ["admitted", "running"];
2650
2738
  const reason = itemStatus === "admitted" ? "run lease expired before completion; retry pending" : "run lease expired before completion";
2651
2739
  this.setWorkflowWorkItemsForLoop(row.loop_id, itemStatus, reason, finished, statuses);
2740
+ if (loop?.target.type === "workflow" && itemStatus !== "admitted") {
2741
+ const workItemId = loop.target.input?.workflowWorkItemId ?? loop.target.input?.workItemId;
2742
+ this.maybeArchiveGeneratedRouteWorkflow({
2743
+ workflowId: loop.target.workflowId,
2744
+ loopId: loop.id,
2745
+ workItemId,
2746
+ updated: finished
2747
+ });
2748
+ }
2652
2749
  }
2653
2750
  this.db.exec("COMMIT");
2654
2751
  } catch (error) {
@@ -5143,7 +5240,7 @@ function enableStartup(result) {
5143
5240
  // package.json
5144
5241
  var package_default = {
5145
5242
  name: "@hasna/loops",
5146
- version: "0.3.48",
5243
+ version: "0.3.50",
5147
5244
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5148
5245
  type: "module",
5149
5246
  main: "dist/index.js",