@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.
- package/README.md +106 -0
- package/dist/cli/index.js +606 -36
- package/dist/daemon/index.js +101 -4
- package/dist/index.js +428 -13
- package/dist/lib/store.d.ts +7 -0
- package/dist/lib/store.js +100 -3
- package/dist/lib/templates.d.ts +24 -4
- package/dist/sdk/index.js +100 -3
- package/dist/types.d.ts +5 -0
- package/docs/USAGE.md +114 -2
- package/package.json +1 -1
package/dist/daemon/index.js
CHANGED
|
@@ -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
|
|
1447
|
-
|
|
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.
|
|
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",
|