@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/index.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) {
|
|
@@ -2899,6 +3073,7 @@ var AUTH_ENV_KEYS = [
|
|
|
2899
3073
|
"CODEWITH_HOME",
|
|
2900
3074
|
"CODEX_HOME",
|
|
2901
3075
|
"CURSOR_CONFIG_DIR",
|
|
3076
|
+
"CURSOR_API_KEY",
|
|
2902
3077
|
"OPENCODE_CONFIG_DIR",
|
|
2903
3078
|
"AICOPILOT_CONFIG_DIR",
|
|
2904
3079
|
"ANTHROPIC_API_KEY",
|
|
@@ -3028,6 +3203,10 @@ function assertSupportedAgentOptions(target) {
|
|
|
3028
3203
|
assertStringOption(target.model, `${target.provider}.model`);
|
|
3029
3204
|
assertStringOption(target.agent, `${target.provider}.agent`);
|
|
3030
3205
|
assertStringOption(target.authProfile, `${target.provider}.authProfile`);
|
|
3206
|
+
assertStringOption(target.configIsolation, `${target.provider}.configIsolation`);
|
|
3207
|
+
if (target.configIsolation !== undefined && target.configIsolation !== "safe" && target.configIsolation !== "none") {
|
|
3208
|
+
throw new Error(`${target.provider}.configIsolation must be safe or none`);
|
|
3209
|
+
}
|
|
3031
3210
|
if (target.authProfile !== undefined && target.provider !== "codewith") {
|
|
3032
3211
|
throw new Error(`${target.provider}.authProfile is supported only for codewith`);
|
|
3033
3212
|
}
|
|
@@ -3040,6 +3219,8 @@ function assertSupportedAgentOptions(target) {
|
|
|
3040
3219
|
if (target.sandbox && !["read-only", "workspace-write", "danger-full-access", "enabled", "disabled"].includes(target.sandbox)) {
|
|
3041
3220
|
throw new Error(`${target.provider}.sandbox is not supported: ${target.sandbox}`);
|
|
3042
3221
|
}
|
|
3222
|
+
if (target.provider === "codex" && target.agent !== undefined)
|
|
3223
|
+
throw new Error("codex.agent is not supported");
|
|
3043
3224
|
if (["codewith", "codex"].includes(target.provider)) {
|
|
3044
3225
|
if (target.permissionMode && !["default", "bypass"].includes(target.permissionMode)) {
|
|
3045
3226
|
throw new Error(`${target.provider}.permissionMode supports only default or bypass`);
|
|
@@ -3054,6 +3235,8 @@ function assertSupportedAgentOptions(target) {
|
|
|
3054
3235
|
return;
|
|
3055
3236
|
}
|
|
3056
3237
|
if (target.provider === "cursor") {
|
|
3238
|
+
if (target.variant !== undefined)
|
|
3239
|
+
throw new Error("cursor.variant is not supported");
|
|
3057
3240
|
if (target.permissionMode === "auto")
|
|
3058
3241
|
throw new Error("cursor.permissionMode auto is not supported; use provider-specific extraArgs for Cursor auto-review");
|
|
3059
3242
|
if (target.sandbox !== undefined && target.sandbox !== "enabled" && target.sandbox !== "disabled") {
|
|
@@ -3095,10 +3278,8 @@ function agentArgs(target) {
|
|
|
3095
3278
|
"set -eu",
|
|
3096
3279
|
"if command -v agent >/dev/null 2>&1; then",
|
|
3097
3280
|
' exec agent "$@"',
|
|
3098
|
-
"elif command -v cursor >/dev/null 2>&1; then",
|
|
3099
|
-
' exec cursor agent "$@"',
|
|
3100
3281
|
"else",
|
|
3101
|
-
" echo 'Executable not found in PATH:
|
|
3282
|
+
" echo 'Executable not found in PATH: agent' >&2",
|
|
3102
3283
|
" exit 127",
|
|
3103
3284
|
"fi"
|
|
3104
3285
|
].join(`
|
|
@@ -3139,7 +3320,7 @@ function agentArgs(target) {
|
|
|
3139
3320
|
case "codex":
|
|
3140
3321
|
if (target.variant)
|
|
3141
3322
|
args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
|
|
3142
|
-
args.push("exec", "--json", "--ephemeral", "--sandbox", codewithLikeSandbox(target), "--skip-git-repo-check");
|
|
3323
|
+
args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", codewithLikeSandbox(target), "--skip-git-repo-check");
|
|
3143
3324
|
if (isolation === "safe")
|
|
3144
3325
|
args.push("--ignore-rules");
|
|
3145
3326
|
if (target.cwd)
|
|
@@ -3209,7 +3390,7 @@ function commandSpec(target) {
|
|
|
3209
3390
|
account: agentTarget.account,
|
|
3210
3391
|
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
|
|
3211
3392
|
nativeAuthProfile: agentTarget.authProfile ? { provider: agentTarget.provider, profile: agentTarget.authProfile } : undefined,
|
|
3212
|
-
preflightAnyOf: agentTarget.provider === "cursor" ? ["
|
|
3393
|
+
preflightAnyOf: agentTarget.provider === "cursor" ? ["agent"] : undefined,
|
|
3213
3394
|
stdin: agentTarget.prompt,
|
|
3214
3395
|
allowlist: agentTarget.allowlist
|
|
3215
3396
|
};
|
|
@@ -4592,12 +4773,14 @@ function claimDueRuns(deps) {
|
|
|
4592
4773
|
const claimed = [];
|
|
4593
4774
|
const skipped = [];
|
|
4594
4775
|
const maxClaims = Math.max(0, deps.maxClaims ?? Number.POSITIVE_INFINITY);
|
|
4776
|
+
if (maxClaims === 0)
|
|
4777
|
+
return { claims, claimed, completed: [], skipped, recovered, expired };
|
|
4595
4778
|
for (const loop of deps.store.dueLoops(now)) {
|
|
4596
|
-
if (claims.length >= maxClaims)
|
|
4779
|
+
if (claims.length + skipped.length >= maxClaims)
|
|
4597
4780
|
break;
|
|
4598
4781
|
const plan = dueSlots(loop, now);
|
|
4599
4782
|
for (const slot of plan.slots) {
|
|
4600
|
-
if (claims.length >= maxClaims)
|
|
4783
|
+
if (claims.length + skipped.length >= maxClaims)
|
|
4601
4784
|
break;
|
|
4602
4785
|
const run = claimSlot(deps, loop, slot);
|
|
4603
4786
|
if (!run)
|
|
@@ -5868,35 +6051,17 @@ async function stopDaemon(opts = {}) {
|
|
|
5868
6051
|
// src/lib/doctor.ts
|
|
5869
6052
|
var PROVIDER_COMMANDS = [
|
|
5870
6053
|
"claude",
|
|
5871
|
-
"
|
|
6054
|
+
"agent",
|
|
5872
6055
|
"codewith",
|
|
5873
6056
|
"aicopilot",
|
|
5874
6057
|
"opencode",
|
|
5875
6058
|
"codex"
|
|
5876
6059
|
];
|
|
5877
6060
|
function hasCommand(command) {
|
|
5878
|
-
if (command === "cursor agent") {
|
|
5879
|
-
return hasCommand("cursor") || hasCommand("agent");
|
|
5880
|
-
}
|
|
5881
6061
|
const result = spawnSync3("sh", ["-c", 'command -v "$1" >/dev/null', "sh", command], { stdio: "ignore" });
|
|
5882
6062
|
return (result.status ?? 1) === 0;
|
|
5883
6063
|
}
|
|
5884
6064
|
function commandVersion(command) {
|
|
5885
|
-
if (command === "cursor agent") {
|
|
5886
|
-
const cursorResult = spawnSync3("cursor", ["agent", "--version"], {
|
|
5887
|
-
encoding: "utf8",
|
|
5888
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
5889
|
-
});
|
|
5890
|
-
if ((cursorResult.status ?? 1) === 0)
|
|
5891
|
-
return (cursorResult.stdout || cursorResult.stderr).trim().split(/\r?\n/)[0];
|
|
5892
|
-
const agentResult = spawnSync3("agent", ["--version"], {
|
|
5893
|
-
encoding: "utf8",
|
|
5894
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
5895
|
-
});
|
|
5896
|
-
if ((agentResult.status ?? 1) === 0)
|
|
5897
|
-
return (agentResult.stdout || agentResult.stderr).trim().split(/\r?\n/)[0];
|
|
5898
|
-
return;
|
|
5899
|
-
}
|
|
5900
6065
|
const result = spawnSync3(command, ["--version"], {
|
|
5901
6066
|
encoding: "utf8",
|
|
5902
6067
|
stdio: ["ignore", "pipe", "pipe"]
|
package/dist/lib/store.d.ts
CHANGED
|
@@ -81,7 +81,7 @@ export declare class Store {
|
|
|
81
81
|
archived?: boolean;
|
|
82
82
|
includeArchived?: boolean;
|
|
83
83
|
}): Loop[];
|
|
84
|
-
dueLoops(now: Date): Loop[];
|
|
84
|
+
dueLoops(now: Date, limit?: number): Loop[];
|
|
85
85
|
updateLoop(id: string, patch: Partial<Pick<Loop, "status" | "nextRunAt" | "retryScheduledFor" | "expiresAt">>, opts?: DaemonLeaseFence): Loop;
|
|
86
86
|
renameLoop(id: string, name: string, opts?: DaemonLeaseFence): Loop;
|
|
87
87
|
archiveLoop(idOrName: string): Loop;
|
|
@@ -94,8 +94,15 @@ export declare class Store {
|
|
|
94
94
|
listWorkflows(opts?: {
|
|
95
95
|
status?: WorkflowSpec["status"];
|
|
96
96
|
limit?: number;
|
|
97
|
+
offset?: number;
|
|
97
98
|
}): WorkflowSpec[];
|
|
99
|
+
countWorkflows(opts?: {
|
|
100
|
+
status?: WorkflowSpec["status"];
|
|
101
|
+
}): number;
|
|
98
102
|
archiveWorkflow(idOrName: string): WorkflowSpec;
|
|
103
|
+
private generatedRouteArchiveContext;
|
|
104
|
+
private maybeArchiveGeneratedRouteWorkflow;
|
|
105
|
+
private maybeArchiveTerminalGeneratedRouteWorkflow;
|
|
99
106
|
createWorkflowInvocation(input: CreateWorkflowInvocationInput): WorkflowInvocation;
|
|
100
107
|
getWorkflowInvocation(id: string): WorkflowInvocation | undefined;
|
|
101
108
|
listWorkflowInvocations(opts?: {
|
|
@@ -195,7 +202,11 @@ export declare class Store {
|
|
|
195
202
|
status?: RunStatus;
|
|
196
203
|
limit?: number;
|
|
197
204
|
}): LoopRun[];
|
|
198
|
-
|
|
205
|
+
private deferLiveExpiredRun;
|
|
206
|
+
recoverExpiredRunLeases(now?: Date, opts?: DaemonLeaseFence & {
|
|
207
|
+
limit?: number;
|
|
208
|
+
scanLimit?: number;
|
|
209
|
+
}): LoopRun[];
|
|
199
210
|
expireLoops(now?: Date, opts?: DaemonLeaseFence): Loop[];
|
|
200
211
|
countLoops(status?: LoopStatus, opts?: {
|
|
201
212
|
archived?: boolean;
|