@hasna/loops 0.3.47 → 0.3.49
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 +35 -3
- package/dist/cli/index.js +357 -349
- package/dist/daemon/index.js +223 -40
- package/dist/index.js +223 -58
- package/dist/lib/store.d.ts +13 -2
- package/dist/lib/store.js +206 -32
- package/dist/sdk/index.js +222 -39
- package/docs/USAGE.md +44 -5
- package/package.json +1 -1
package/dist/lib/store.js
CHANGED
|
@@ -327,6 +327,13 @@ function optionalPositiveInteger(value, label) {
|
|
|
327
327
|
throw new Error(`${label} must be a positive integer`);
|
|
328
328
|
return value;
|
|
329
329
|
}
|
|
330
|
+
function optionalBoolean(value, label) {
|
|
331
|
+
if (value === undefined)
|
|
332
|
+
return;
|
|
333
|
+
if (typeof value !== "boolean")
|
|
334
|
+
throw new Error(`${label} must be a boolean`);
|
|
335
|
+
return value;
|
|
336
|
+
}
|
|
330
337
|
function optionalStringArray(value, label) {
|
|
331
338
|
if (value === undefined)
|
|
332
339
|
return;
|
|
@@ -338,6 +345,18 @@ function optionalStringArray(value, label) {
|
|
|
338
345
|
}).filter(Boolean);
|
|
339
346
|
return values.length ? values : undefined;
|
|
340
347
|
}
|
|
348
|
+
function optionalAccountRef(value, label) {
|
|
349
|
+
if (value === undefined)
|
|
350
|
+
return;
|
|
351
|
+
assertObject(value, label);
|
|
352
|
+
assertString(value.profile, `${label}.profile`);
|
|
353
|
+
if (value.tool !== undefined)
|
|
354
|
+
assertString(value.tool, `${label}.tool`);
|
|
355
|
+
return {
|
|
356
|
+
profile: value.profile.trim(),
|
|
357
|
+
tool: typeof value.tool === "string" ? value.tool.trim() : undefined
|
|
358
|
+
};
|
|
359
|
+
}
|
|
341
360
|
function normalizeGoalSpec(value, label = "goal") {
|
|
342
361
|
if (value === undefined)
|
|
343
362
|
return;
|
|
@@ -384,8 +403,21 @@ function validateTarget(value, label) {
|
|
|
384
403
|
if (value.provider !== "codewith")
|
|
385
404
|
throw new Error(`${label}.authProfile is currently supported only for provider codewith`);
|
|
386
405
|
}
|
|
406
|
+
if (value.configIsolation !== undefined) {
|
|
407
|
+
assertString(value.configIsolation, `${label}.configIsolation`);
|
|
408
|
+
if (!["safe", "none"].includes(value.configIsolation))
|
|
409
|
+
throw new Error(`${label}.configIsolation must be safe or none`);
|
|
410
|
+
}
|
|
411
|
+
if (value.model !== undefined)
|
|
412
|
+
assertString(value.model, `${label}.model`);
|
|
387
413
|
if (value.variant !== undefined)
|
|
388
414
|
assertString(value.variant, `${label}.variant`);
|
|
415
|
+
if (value.agent !== undefined)
|
|
416
|
+
assertString(value.agent, `${label}.agent`);
|
|
417
|
+
if (value.provider === "cursor" && value.variant !== undefined)
|
|
418
|
+
throw new Error(`${label}.variant is not supported for provider cursor`);
|
|
419
|
+
if (value.provider === "codex" && value.agent !== undefined)
|
|
420
|
+
throw new Error(`${label}.agent is not supported for provider codex`);
|
|
389
421
|
optionalStringArray(value.addDirs, `${label}.addDirs`);
|
|
390
422
|
if (Array.isArray(value.addDirs) && value.addDirs.length > 0 && !["codewith", "codex"].includes(value.provider)) {
|
|
391
423
|
throw new Error(`${label}.addDirs is currently supported only for provider codewith or codex`);
|
|
@@ -482,8 +514,10 @@ function normalizeCreateWorkflowInput(input) {
|
|
|
482
514
|
id: step.id,
|
|
483
515
|
goal: normalizeGoalSpec(step.goal, `workflow.steps[${index}].goal`),
|
|
484
516
|
target: validateTarget(step.target, `workflow.steps[${index}].target`),
|
|
485
|
-
dependsOn: step.dependsOn ?? [],
|
|
486
|
-
continueOnFailure: step.continueOnFailure ?? false
|
|
517
|
+
dependsOn: optionalStringArray(step.dependsOn, `workflow.steps[${index}].dependsOn`) ?? [],
|
|
518
|
+
continueOnFailure: optionalBoolean(step.continueOnFailure, `workflow.steps[${index}].continueOnFailure`) ?? false,
|
|
519
|
+
timeoutMs: optionalPositiveInteger(step.timeoutMs, `workflow.steps[${index}].timeoutMs`),
|
|
520
|
+
account: optionalAccountRef(step.account, `workflow.steps[${index}].account`)
|
|
487
521
|
};
|
|
488
522
|
});
|
|
489
523
|
for (const step of steps) {
|
|
@@ -584,6 +618,11 @@ function writeWorkflowRunManifest(args) {
|
|
|
584
618
|
}
|
|
585
619
|
|
|
586
620
|
// src/lib/store.ts
|
|
621
|
+
var DEFAULT_RECOVERY_BATCH_LIMIT = 100;
|
|
622
|
+
var DEFAULT_RECOVERY_SCAN_MULTIPLIER = 5;
|
|
623
|
+
var LIVE_EXPIRED_RUN_GRACE_MS = 60000;
|
|
624
|
+
var GENERATED_ROUTE_TEMPLATE_IDS = new Set(["todos-task-worker-verifier", "event-worker-verifier"]);
|
|
625
|
+
var GENERATED_ROUTE_KEYS = new Set(["todos-task", "generic-event"]);
|
|
587
626
|
function rowToLoop(row) {
|
|
588
627
|
return {
|
|
589
628
|
id: row.id,
|
|
@@ -850,6 +889,7 @@ class Store {
|
|
|
850
889
|
ensurePrivateStorePath(file);
|
|
851
890
|
this.rootDir = file === ":memory:" ? mkdtempSync(join3(tmpdir(), "open-loops-store-")) : dirname(file);
|
|
852
891
|
this.db = new Database(file);
|
|
892
|
+
this.db.exec("PRAGMA foreign_keys = ON;");
|
|
853
893
|
this.db.exec("PRAGMA busy_timeout = 5000;");
|
|
854
894
|
this.db.exec("PRAGMA journal_mode = WAL;");
|
|
855
895
|
if (file !== ":memory:")
|
|
@@ -913,6 +953,7 @@ class Store {
|
|
|
913
953
|
);
|
|
914
954
|
CREATE INDEX IF NOT EXISTS idx_runs_loop ON loop_runs(loop_id, created_at);
|
|
915
955
|
CREATE INDEX IF NOT EXISTS idx_runs_status ON loop_runs(status);
|
|
956
|
+
CREATE INDEX IF NOT EXISTS idx_runs_status_lease ON loop_runs(status, lease_expires_at);
|
|
916
957
|
CREATE INDEX IF NOT EXISTS idx_runs_scheduled ON loop_runs(scheduled_for);
|
|
917
958
|
|
|
918
959
|
CREATE TABLE IF NOT EXISTS daemon_lease (
|
|
@@ -1240,39 +1281,51 @@ class Store {
|
|
|
1240
1281
|
}
|
|
1241
1282
|
return rows.map(rowToLoop);
|
|
1242
1283
|
}
|
|
1243
|
-
dueLoops(now) {
|
|
1284
|
+
dueLoops(now, limit = 500) {
|
|
1244
1285
|
const rows = this.db.query(`SELECT * FROM loops
|
|
1245
1286
|
WHERE status = 'active'
|
|
1246
1287
|
AND archived_at IS NULL
|
|
1247
1288
|
AND next_run_at IS NOT NULL
|
|
1248
1289
|
AND next_run_at <= ?
|
|
1249
|
-
ORDER BY next_run_at ASC
|
|
1290
|
+
ORDER BY next_run_at ASC
|
|
1291
|
+
LIMIT ?`).all(now.toISOString(), limit);
|
|
1250
1292
|
return rows.map(rowToLoop);
|
|
1251
1293
|
}
|
|
1252
1294
|
updateLoop(id, patch, opts = {}) {
|
|
1253
|
-
const current = this.getLoop(id);
|
|
1254
|
-
if (!current)
|
|
1255
|
-
throw new Error(`loop not found: ${id}`);
|
|
1256
1295
|
const updated = (opts.now ?? new Date).toISOString();
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1296
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1297
|
+
try {
|
|
1298
|
+
const current = this.getLoop(id);
|
|
1299
|
+
if (!current)
|
|
1300
|
+
throw new Error(`loop not found: ${id}`);
|
|
1301
|
+
const merged = { ...current, ...patch, updatedAt: updated };
|
|
1302
|
+
const res = this.db.query(`UPDATE loops SET status=$status, next_run_at=$nextRun, retry_scheduled_for=$retrySlot,
|
|
1303
|
+
expires_at=$expiresAt, updated_at=$updated
|
|
1304
|
+
WHERE id=$id
|
|
1305
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1306
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1307
|
+
))`).run({
|
|
1308
|
+
$id: id,
|
|
1309
|
+
$status: merged.status,
|
|
1310
|
+
$nextRun: merged.nextRunAt ?? null,
|
|
1311
|
+
$retrySlot: merged.retryScheduledFor ?? null,
|
|
1312
|
+
$expiresAt: merged.expiresAt ?? null,
|
|
1313
|
+
$updated: merged.updatedAt,
|
|
1314
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1315
|
+
$now: updated
|
|
1316
|
+
});
|
|
1317
|
+
if (res.changes !== 1)
|
|
1318
|
+
throw new Error("daemon lease lost");
|
|
1319
|
+
if (patch.status && patch.status !== "active") {
|
|
1320
|
+
const status = patch.status === "paused" ? "deferred" : "cancelled";
|
|
1321
|
+
this.setWorkflowWorkItemsForLoop(id, status, `loop ${patch.status}`, updated);
|
|
1322
|
+
}
|
|
1323
|
+
this.db.exec("COMMIT");
|
|
1324
|
+
} catch (error) {
|
|
1325
|
+
try {
|
|
1326
|
+
this.db.exec("ROLLBACK");
|
|
1327
|
+
} catch {}
|
|
1328
|
+
throw error;
|
|
1276
1329
|
}
|
|
1277
1330
|
const after = this.getLoop(id);
|
|
1278
1331
|
if (!after)
|
|
@@ -1390,10 +1443,23 @@ class Store {
|
|
|
1390
1443
|
})();
|
|
1391
1444
|
}
|
|
1392
1445
|
listWorkflows(opts = {}) {
|
|
1393
|
-
const
|
|
1394
|
-
|
|
1446
|
+
const offset = Math.max(0, opts.offset ?? 0);
|
|
1447
|
+
let rows;
|
|
1448
|
+
if (opts.status && opts.limit !== undefined) {
|
|
1449
|
+
rows = this.db.query("SELECT * FROM workflow_specs WHERE status = ? ORDER BY updated_at DESC LIMIT ? OFFSET ?").all(opts.status, opts.limit, offset);
|
|
1450
|
+
} else if (opts.status) {
|
|
1451
|
+
rows = this.db.query("SELECT * FROM workflow_specs WHERE status = ? ORDER BY updated_at DESC LIMIT -1 OFFSET ?").all(opts.status, offset);
|
|
1452
|
+
} else if (opts.limit !== undefined) {
|
|
1453
|
+
rows = this.db.query("SELECT * FROM workflow_specs ORDER BY status ASC, updated_at DESC LIMIT ? OFFSET ?").all(opts.limit, offset);
|
|
1454
|
+
} else {
|
|
1455
|
+
rows = this.db.query("SELECT * FROM workflow_specs ORDER BY status ASC, updated_at DESC LIMIT -1 OFFSET ?").all(offset);
|
|
1456
|
+
}
|
|
1395
1457
|
return rows.map(rowToWorkflow);
|
|
1396
1458
|
}
|
|
1459
|
+
countWorkflows(opts = {}) {
|
|
1460
|
+
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();
|
|
1461
|
+
return row?.count ?? 0;
|
|
1462
|
+
}
|
|
1397
1463
|
archiveWorkflow(idOrName) {
|
|
1398
1464
|
const workflow = this.requireWorkflow(idOrName);
|
|
1399
1465
|
const updated = nowIso();
|
|
@@ -1403,6 +1469,64 @@ class Store {
|
|
|
1403
1469
|
throw new Error(`workflow not found after archive: ${workflow.id}`);
|
|
1404
1470
|
return archived;
|
|
1405
1471
|
}
|
|
1472
|
+
generatedRouteArchiveContext(args) {
|
|
1473
|
+
if (!args.loopId || !args.workItemId)
|
|
1474
|
+
return;
|
|
1475
|
+
const workItem = this.getWorkflowWorkItem(args.workItemId);
|
|
1476
|
+
if (!workItem || !GENERATED_ROUTE_KEYS.has(workItem.routeKey))
|
|
1477
|
+
return;
|
|
1478
|
+
const invocation = this.getWorkflowInvocation(workItem.invocationId);
|
|
1479
|
+
if (!invocation?.templateId || !GENERATED_ROUTE_TEMPLATE_IDS.has(invocation.templateId))
|
|
1480
|
+
return;
|
|
1481
|
+
const loop = this.getLoop(args.loopId);
|
|
1482
|
+
if (!loop || loop.schedule.type !== "once" || loop.target.type !== "workflow" || loop.target.workflowId !== args.workflowId)
|
|
1483
|
+
return;
|
|
1484
|
+
const input = loop.target.input ?? {};
|
|
1485
|
+
if (input.workflowWorkItemId && input.workflowWorkItemId !== workItem.id)
|
|
1486
|
+
return;
|
|
1487
|
+
if (input.workflowInvocationId && input.workflowInvocationId !== invocation.id)
|
|
1488
|
+
return;
|
|
1489
|
+
if (workItem.workflowId && workItem.workflowId !== args.workflowId)
|
|
1490
|
+
return;
|
|
1491
|
+
const workflow = this.getWorkflow(args.workflowId);
|
|
1492
|
+
if (!workflow)
|
|
1493
|
+
return;
|
|
1494
|
+
return { workflow, loop, workItem, invocation };
|
|
1495
|
+
}
|
|
1496
|
+
maybeArchiveGeneratedRouteWorkflow(args) {
|
|
1497
|
+
const context = this.generatedRouteArchiveContext(args);
|
|
1498
|
+
if (!context)
|
|
1499
|
+
return;
|
|
1500
|
+
const { workflow, loop, workItem } = context;
|
|
1501
|
+
if (!workflow || workflow.status !== "active")
|
|
1502
|
+
return;
|
|
1503
|
+
const nonTerminal = this.db.query(`SELECT COUNT(*) AS count FROM workflow_runs
|
|
1504
|
+
WHERE workflow_id = ? AND status NOT IN ('succeeded', 'failed', 'timed_out', 'cancelled')`).get(args.workflowId)?.count ?? 0;
|
|
1505
|
+
if (nonTerminal > 0)
|
|
1506
|
+
return;
|
|
1507
|
+
const res = this.db.query("UPDATE workflow_specs SET status='archived', updated_at=? WHERE id=? AND status='active'").run(args.updated, args.workflowId);
|
|
1508
|
+
if (res.changes === 1 && args.workflowRunId) {
|
|
1509
|
+
this.appendWorkflowEvent(args.workflowRunId, "workflow_archived", undefined, {
|
|
1510
|
+
workflowId: args.workflowId,
|
|
1511
|
+
loopId: loop.id,
|
|
1512
|
+
workItemId: workItem.id,
|
|
1513
|
+
routeKey: workItem.routeKey,
|
|
1514
|
+
reason: "terminal generated one-shot route workflow"
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
maybeArchiveTerminalGeneratedRouteWorkflow(workflowRunId, updated) {
|
|
1519
|
+
const run = this.getWorkflowRun(workflowRunId);
|
|
1520
|
+
if (!run)
|
|
1521
|
+
return;
|
|
1522
|
+
this.maybeArchiveGeneratedRouteWorkflow({
|
|
1523
|
+
workflowId: run.workflowId,
|
|
1524
|
+
loopId: run.loopId,
|
|
1525
|
+
workItemId: run.workItemId,
|
|
1526
|
+
workflowRunId,
|
|
1527
|
+
updated
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1406
1530
|
createWorkflowInvocation(input) {
|
|
1407
1531
|
const now = nowIso();
|
|
1408
1532
|
const sourceDedupeKey = input.sourceRef.dedupeKey ?? undefined;
|
|
@@ -2188,6 +2312,7 @@ class Store {
|
|
|
2188
2312
|
if (changed) {
|
|
2189
2313
|
const itemStatus = status === "succeeded" ? "succeeded" : status === "cancelled" ? "cancelled" : "failed";
|
|
2190
2314
|
this.setWorkflowWorkItemsForWorkflowRun(workflowRunId, itemStatus, patch.error, finishedAt);
|
|
2315
|
+
this.maybeArchiveTerminalGeneratedRouteWorkflow(workflowRunId, finishedAt);
|
|
2191
2316
|
}
|
|
2192
2317
|
this.db.exec("COMMIT");
|
|
2193
2318
|
} catch (error) {
|
|
@@ -2215,6 +2340,7 @@ class Store {
|
|
|
2215
2340
|
WHERE workflow_run_id=$workflowRunId AND status IN ('pending', 'running')`).run({ $workflowRunId: workflowRunId, $finished: now, $reason: reason, $updated: now });
|
|
2216
2341
|
this.setWorkflowWorkItemsForWorkflowRun(workflowRunId, "cancelled", reason, now);
|
|
2217
2342
|
this.appendWorkflowEvent(workflowRunId, "cancelled", undefined, { reason });
|
|
2343
|
+
this.maybeArchiveTerminalGeneratedRouteWorkflow(workflowRunId, now);
|
|
2218
2344
|
}
|
|
2219
2345
|
this.db.exec("COMMIT");
|
|
2220
2346
|
return this.requireWorkflowRun(workflowRunId);
|
|
@@ -2467,8 +2593,20 @@ class Store {
|
|
|
2467
2593
|
throw new Error(`run not found after finalize: ${id}`);
|
|
2468
2594
|
if (opts.claimedBy && res.changes !== 1)
|
|
2469
2595
|
return run;
|
|
2470
|
-
if (res.changes === 1)
|
|
2596
|
+
if (res.changes === 1) {
|
|
2471
2597
|
this.setWorkflowWorkItemsForLoopRun(run, patch.error, finishedAt);
|
|
2598
|
+
const loop = this.getLoop(run.loopId);
|
|
2599
|
+
const itemStatus = workItemStatusForLoopRun(run.status, run.attempt, loop?.maxAttempts);
|
|
2600
|
+
if (loop?.target.type === "workflow" && itemStatus && itemStatus !== "admitted") {
|
|
2601
|
+
const workItemId = loop.target.input?.workflowWorkItemId ?? loop.target.input?.workItemId;
|
|
2602
|
+
this.maybeArchiveGeneratedRouteWorkflow({
|
|
2603
|
+
workflowId: loop.target.workflowId,
|
|
2604
|
+
loopId: loop.id,
|
|
2605
|
+
workItemId,
|
|
2606
|
+
updated: finishedAt
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2472
2610
|
return run;
|
|
2473
2611
|
}
|
|
2474
2612
|
heartbeatRunLease(id, claimedBy, leaseMs, now = new Date, opts = {}) {
|
|
@@ -2503,14 +2641,40 @@ class Store {
|
|
|
2503
2641
|
}
|
|
2504
2642
|
return rows.map(rowToRun);
|
|
2505
2643
|
}
|
|
2644
|
+
deferLiveExpiredRun(id, now, opts = {}) {
|
|
2645
|
+
const updated = now.toISOString();
|
|
2646
|
+
const deferredUntil = new Date(now.getTime() + LIVE_EXPIRED_RUN_GRACE_MS).toISOString();
|
|
2647
|
+
this.db.query(`UPDATE loop_runs SET lease_expires_at=$deferredUntil, updated_at=$updated
|
|
2648
|
+
WHERE id=$id AND status='running' AND lease_expires_at <= $now
|
|
2649
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
2650
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
2651
|
+
))`).run({
|
|
2652
|
+
$id: id,
|
|
2653
|
+
$deferredUntil: deferredUntil,
|
|
2654
|
+
$updated: updated,
|
|
2655
|
+
$now: updated,
|
|
2656
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2506
2659
|
recoverExpiredRunLeases(now = new Date, opts = {}) {
|
|
2507
|
-
const
|
|
2660
|
+
const limit = Math.max(1, Math.min(1000, Math.floor(opts.limit ?? DEFAULT_RECOVERY_BATCH_LIMIT)));
|
|
2661
|
+
const scanLimit = Math.max(limit, Math.min(5000, Math.floor(opts.scanLimit ?? limit * DEFAULT_RECOVERY_SCAN_MULTIPLIER)));
|
|
2662
|
+
const rows = this.db.query(`SELECT * FROM loop_runs
|
|
2663
|
+
WHERE status = 'running' AND lease_expires_at <= ?
|
|
2664
|
+
ORDER BY lease_expires_at ASC
|
|
2665
|
+
LIMIT ?`).all(now.toISOString(), scanLimit);
|
|
2508
2666
|
const recovered = [];
|
|
2509
2667
|
for (const row of rows) {
|
|
2510
|
-
if (
|
|
2668
|
+
if (recovered.length >= limit)
|
|
2669
|
+
break;
|
|
2670
|
+
if (row.pid && isProcessAlive(row.pid)) {
|
|
2671
|
+
this.deferLiveExpiredRun(row.id, now, opts);
|
|
2511
2672
|
continue;
|
|
2512
|
-
|
|
2673
|
+
}
|
|
2674
|
+
if (this.hasLiveWorkflowStepProcesses(row.id)) {
|
|
2675
|
+
this.deferLiveExpiredRun(row.id, now, opts);
|
|
2513
2676
|
continue;
|
|
2677
|
+
}
|
|
2514
2678
|
const finished = now.toISOString();
|
|
2515
2679
|
this.db.exec("BEGIN IMMEDIATE");
|
|
2516
2680
|
try {
|
|
@@ -2563,6 +2727,7 @@ class Store {
|
|
|
2563
2727
|
loopRunId: row.id
|
|
2564
2728
|
});
|
|
2565
2729
|
this.setWorkflowWorkItemsForWorkflowRun(workflowRow.id, "failed", "parent loop run lease expired before completion", finished);
|
|
2730
|
+
this.maybeArchiveTerminalGeneratedRouteWorkflow(workflowRow.id, finished);
|
|
2566
2731
|
}
|
|
2567
2732
|
const loop = this.getLoop(row.loop_id);
|
|
2568
2733
|
const itemStatus = workItemStatusForLoopRun("abandoned", row.attempt, loop?.maxAttempts);
|
|
@@ -2570,6 +2735,15 @@ class Store {
|
|
|
2570
2735
|
const statuses = itemStatus === "admitted" ? ["admitted", "running", "failed"] : ["admitted", "running"];
|
|
2571
2736
|
const reason = itemStatus === "admitted" ? "run lease expired before completion; retry pending" : "run lease expired before completion";
|
|
2572
2737
|
this.setWorkflowWorkItemsForLoop(row.loop_id, itemStatus, reason, finished, statuses);
|
|
2738
|
+
if (loop?.target.type === "workflow" && itemStatus !== "admitted") {
|
|
2739
|
+
const workItemId = loop.target.input?.workflowWorkItemId ?? loop.target.input?.workItemId;
|
|
2740
|
+
this.maybeArchiveGeneratedRouteWorkflow({
|
|
2741
|
+
workflowId: loop.target.workflowId,
|
|
2742
|
+
loopId: loop.id,
|
|
2743
|
+
workItemId,
|
|
2744
|
+
updated: finished
|
|
2745
|
+
});
|
|
2746
|
+
}
|
|
2573
2747
|
}
|
|
2574
2748
|
this.db.exec("COMMIT");
|
|
2575
2749
|
} catch (error) {
|