@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/daemon/index.js
CHANGED
|
@@ -329,6 +329,13 @@ function optionalPositiveInteger(value, label) {
|
|
|
329
329
|
throw new Error(`${label} must be a positive integer`);
|
|
330
330
|
return value;
|
|
331
331
|
}
|
|
332
|
+
function optionalBoolean(value, label) {
|
|
333
|
+
if (value === undefined)
|
|
334
|
+
return;
|
|
335
|
+
if (typeof value !== "boolean")
|
|
336
|
+
throw new Error(`${label} must be a boolean`);
|
|
337
|
+
return value;
|
|
338
|
+
}
|
|
332
339
|
function optionalStringArray(value, label) {
|
|
333
340
|
if (value === undefined)
|
|
334
341
|
return;
|
|
@@ -340,6 +347,18 @@ function optionalStringArray(value, label) {
|
|
|
340
347
|
}).filter(Boolean);
|
|
341
348
|
return values.length ? values : undefined;
|
|
342
349
|
}
|
|
350
|
+
function optionalAccountRef(value, label) {
|
|
351
|
+
if (value === undefined)
|
|
352
|
+
return;
|
|
353
|
+
assertObject(value, label);
|
|
354
|
+
assertString(value.profile, `${label}.profile`);
|
|
355
|
+
if (value.tool !== undefined)
|
|
356
|
+
assertString(value.tool, `${label}.tool`);
|
|
357
|
+
return {
|
|
358
|
+
profile: value.profile.trim(),
|
|
359
|
+
tool: typeof value.tool === "string" ? value.tool.trim() : undefined
|
|
360
|
+
};
|
|
361
|
+
}
|
|
343
362
|
function normalizeGoalSpec(value, label = "goal") {
|
|
344
363
|
if (value === undefined)
|
|
345
364
|
return;
|
|
@@ -386,8 +405,21 @@ function validateTarget(value, label) {
|
|
|
386
405
|
if (value.provider !== "codewith")
|
|
387
406
|
throw new Error(`${label}.authProfile is currently supported only for provider codewith`);
|
|
388
407
|
}
|
|
408
|
+
if (value.configIsolation !== undefined) {
|
|
409
|
+
assertString(value.configIsolation, `${label}.configIsolation`);
|
|
410
|
+
if (!["safe", "none"].includes(value.configIsolation))
|
|
411
|
+
throw new Error(`${label}.configIsolation must be safe or none`);
|
|
412
|
+
}
|
|
413
|
+
if (value.model !== undefined)
|
|
414
|
+
assertString(value.model, `${label}.model`);
|
|
389
415
|
if (value.variant !== undefined)
|
|
390
416
|
assertString(value.variant, `${label}.variant`);
|
|
417
|
+
if (value.agent !== undefined)
|
|
418
|
+
assertString(value.agent, `${label}.agent`);
|
|
419
|
+
if (value.provider === "cursor" && value.variant !== undefined)
|
|
420
|
+
throw new Error(`${label}.variant is not supported for provider cursor`);
|
|
421
|
+
if (value.provider === "codex" && value.agent !== undefined)
|
|
422
|
+
throw new Error(`${label}.agent is not supported for provider codex`);
|
|
391
423
|
optionalStringArray(value.addDirs, `${label}.addDirs`);
|
|
392
424
|
if (Array.isArray(value.addDirs) && value.addDirs.length > 0 && !["codewith", "codex"].includes(value.provider)) {
|
|
393
425
|
throw new Error(`${label}.addDirs is currently supported only for provider codewith or codex`);
|
|
@@ -484,8 +516,10 @@ function normalizeCreateWorkflowInput(input) {
|
|
|
484
516
|
id: step.id,
|
|
485
517
|
goal: normalizeGoalSpec(step.goal, `workflow.steps[${index}].goal`),
|
|
486
518
|
target: validateTarget(step.target, `workflow.steps[${index}].target`),
|
|
487
|
-
dependsOn: step.dependsOn ?? [],
|
|
488
|
-
continueOnFailure: step.continueOnFailure ?? false
|
|
519
|
+
dependsOn: optionalStringArray(step.dependsOn, `workflow.steps[${index}].dependsOn`) ?? [],
|
|
520
|
+
continueOnFailure: optionalBoolean(step.continueOnFailure, `workflow.steps[${index}].continueOnFailure`) ?? false,
|
|
521
|
+
timeoutMs: optionalPositiveInteger(step.timeoutMs, `workflow.steps[${index}].timeoutMs`),
|
|
522
|
+
account: optionalAccountRef(step.account, `workflow.steps[${index}].account`)
|
|
489
523
|
};
|
|
490
524
|
});
|
|
491
525
|
for (const step of steps) {
|
|
@@ -586,6 +620,11 @@ function writeWorkflowRunManifest(args) {
|
|
|
586
620
|
}
|
|
587
621
|
|
|
588
622
|
// src/lib/store.ts
|
|
623
|
+
var DEFAULT_RECOVERY_BATCH_LIMIT = 100;
|
|
624
|
+
var DEFAULT_RECOVERY_SCAN_MULTIPLIER = 5;
|
|
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"]);
|
|
589
628
|
function rowToLoop(row) {
|
|
590
629
|
return {
|
|
591
630
|
id: row.id,
|
|
@@ -852,6 +891,7 @@ class Store {
|
|
|
852
891
|
ensurePrivateStorePath(file);
|
|
853
892
|
this.rootDir = file === ":memory:" ? mkdtempSync(join3(tmpdir(), "open-loops-store-")) : dirname(file);
|
|
854
893
|
this.db = new Database(file);
|
|
894
|
+
this.db.exec("PRAGMA foreign_keys = ON;");
|
|
855
895
|
this.db.exec("PRAGMA busy_timeout = 5000;");
|
|
856
896
|
this.db.exec("PRAGMA journal_mode = WAL;");
|
|
857
897
|
if (file !== ":memory:")
|
|
@@ -915,6 +955,7 @@ class Store {
|
|
|
915
955
|
);
|
|
916
956
|
CREATE INDEX IF NOT EXISTS idx_runs_loop ON loop_runs(loop_id, created_at);
|
|
917
957
|
CREATE INDEX IF NOT EXISTS idx_runs_status ON loop_runs(status);
|
|
958
|
+
CREATE INDEX IF NOT EXISTS idx_runs_status_lease ON loop_runs(status, lease_expires_at);
|
|
918
959
|
CREATE INDEX IF NOT EXISTS idx_runs_scheduled ON loop_runs(scheduled_for);
|
|
919
960
|
|
|
920
961
|
CREATE TABLE IF NOT EXISTS daemon_lease (
|
|
@@ -1242,39 +1283,51 @@ class Store {
|
|
|
1242
1283
|
}
|
|
1243
1284
|
return rows.map(rowToLoop);
|
|
1244
1285
|
}
|
|
1245
|
-
dueLoops(now) {
|
|
1286
|
+
dueLoops(now, limit = 500) {
|
|
1246
1287
|
const rows = this.db.query(`SELECT * FROM loops
|
|
1247
1288
|
WHERE status = 'active'
|
|
1248
1289
|
AND archived_at IS NULL
|
|
1249
1290
|
AND next_run_at IS NOT NULL
|
|
1250
1291
|
AND next_run_at <= ?
|
|
1251
|
-
ORDER BY next_run_at ASC
|
|
1292
|
+
ORDER BY next_run_at ASC
|
|
1293
|
+
LIMIT ?`).all(now.toISOString(), limit);
|
|
1252
1294
|
return rows.map(rowToLoop);
|
|
1253
1295
|
}
|
|
1254
1296
|
updateLoop(id, patch, opts = {}) {
|
|
1255
|
-
const current = this.getLoop(id);
|
|
1256
|
-
if (!current)
|
|
1257
|
-
throw new Error(`loop not found: ${id}`);
|
|
1258
1297
|
const updated = (opts.now ?? new Date).toISOString();
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1298
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1299
|
+
try {
|
|
1300
|
+
const current = this.getLoop(id);
|
|
1301
|
+
if (!current)
|
|
1302
|
+
throw new Error(`loop not found: ${id}`);
|
|
1303
|
+
const merged = { ...current, ...patch, updatedAt: updated };
|
|
1304
|
+
const res = this.db.query(`UPDATE loops SET status=$status, next_run_at=$nextRun, retry_scheduled_for=$retrySlot,
|
|
1305
|
+
expires_at=$expiresAt, updated_at=$updated
|
|
1306
|
+
WHERE id=$id
|
|
1307
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1308
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1309
|
+
))`).run({
|
|
1310
|
+
$id: id,
|
|
1311
|
+
$status: merged.status,
|
|
1312
|
+
$nextRun: merged.nextRunAt ?? null,
|
|
1313
|
+
$retrySlot: merged.retryScheduledFor ?? null,
|
|
1314
|
+
$expiresAt: merged.expiresAt ?? null,
|
|
1315
|
+
$updated: merged.updatedAt,
|
|
1316
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1317
|
+
$now: updated
|
|
1318
|
+
});
|
|
1319
|
+
if (res.changes !== 1)
|
|
1320
|
+
throw new Error("daemon lease lost");
|
|
1321
|
+
if (patch.status && patch.status !== "active") {
|
|
1322
|
+
const status = patch.status === "paused" ? "deferred" : "cancelled";
|
|
1323
|
+
this.setWorkflowWorkItemsForLoop(id, status, `loop ${patch.status}`, updated);
|
|
1324
|
+
}
|
|
1325
|
+
this.db.exec("COMMIT");
|
|
1326
|
+
} catch (error) {
|
|
1327
|
+
try {
|
|
1328
|
+
this.db.exec("ROLLBACK");
|
|
1329
|
+
} catch {}
|
|
1330
|
+
throw error;
|
|
1278
1331
|
}
|
|
1279
1332
|
const after = this.getLoop(id);
|
|
1280
1333
|
if (!after)
|
|
@@ -1392,10 +1445,23 @@ class Store {
|
|
|
1392
1445
|
})();
|
|
1393
1446
|
}
|
|
1394
1447
|
listWorkflows(opts = {}) {
|
|
1395
|
-
const
|
|
1396
|
-
|
|
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
|
+
}
|
|
1397
1459
|
return rows.map(rowToWorkflow);
|
|
1398
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
|
+
}
|
|
1399
1465
|
archiveWorkflow(idOrName) {
|
|
1400
1466
|
const workflow = this.requireWorkflow(idOrName);
|
|
1401
1467
|
const updated = nowIso();
|
|
@@ -1405,6 +1471,64 @@ class Store {
|
|
|
1405
1471
|
throw new Error(`workflow not found after archive: ${workflow.id}`);
|
|
1406
1472
|
return archived;
|
|
1407
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
|
+
}
|
|
1408
1532
|
createWorkflowInvocation(input) {
|
|
1409
1533
|
const now = nowIso();
|
|
1410
1534
|
const sourceDedupeKey = input.sourceRef.dedupeKey ?? undefined;
|
|
@@ -2190,6 +2314,7 @@ class Store {
|
|
|
2190
2314
|
if (changed) {
|
|
2191
2315
|
const itemStatus = status === "succeeded" ? "succeeded" : status === "cancelled" ? "cancelled" : "failed";
|
|
2192
2316
|
this.setWorkflowWorkItemsForWorkflowRun(workflowRunId, itemStatus, patch.error, finishedAt);
|
|
2317
|
+
this.maybeArchiveTerminalGeneratedRouteWorkflow(workflowRunId, finishedAt);
|
|
2193
2318
|
}
|
|
2194
2319
|
this.db.exec("COMMIT");
|
|
2195
2320
|
} catch (error) {
|
|
@@ -2217,6 +2342,7 @@ class Store {
|
|
|
2217
2342
|
WHERE workflow_run_id=$workflowRunId AND status IN ('pending', 'running')`).run({ $workflowRunId: workflowRunId, $finished: now, $reason: reason, $updated: now });
|
|
2218
2343
|
this.setWorkflowWorkItemsForWorkflowRun(workflowRunId, "cancelled", reason, now);
|
|
2219
2344
|
this.appendWorkflowEvent(workflowRunId, "cancelled", undefined, { reason });
|
|
2345
|
+
this.maybeArchiveTerminalGeneratedRouteWorkflow(workflowRunId, now);
|
|
2220
2346
|
}
|
|
2221
2347
|
this.db.exec("COMMIT");
|
|
2222
2348
|
return this.requireWorkflowRun(workflowRunId);
|
|
@@ -2469,8 +2595,20 @@ class Store {
|
|
|
2469
2595
|
throw new Error(`run not found after finalize: ${id}`);
|
|
2470
2596
|
if (opts.claimedBy && res.changes !== 1)
|
|
2471
2597
|
return run;
|
|
2472
|
-
if (res.changes === 1)
|
|
2598
|
+
if (res.changes === 1) {
|
|
2473
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
|
+
}
|
|
2474
2612
|
return run;
|
|
2475
2613
|
}
|
|
2476
2614
|
heartbeatRunLease(id, claimedBy, leaseMs, now = new Date, opts = {}) {
|
|
@@ -2505,14 +2643,40 @@ class Store {
|
|
|
2505
2643
|
}
|
|
2506
2644
|
return rows.map(rowToRun);
|
|
2507
2645
|
}
|
|
2646
|
+
deferLiveExpiredRun(id, now, opts = {}) {
|
|
2647
|
+
const updated = now.toISOString();
|
|
2648
|
+
const deferredUntil = new Date(now.getTime() + LIVE_EXPIRED_RUN_GRACE_MS).toISOString();
|
|
2649
|
+
this.db.query(`UPDATE loop_runs SET lease_expires_at=$deferredUntil, updated_at=$updated
|
|
2650
|
+
WHERE id=$id AND status='running' AND lease_expires_at <= $now
|
|
2651
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
2652
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
2653
|
+
))`).run({
|
|
2654
|
+
$id: id,
|
|
2655
|
+
$deferredUntil: deferredUntil,
|
|
2656
|
+
$updated: updated,
|
|
2657
|
+
$now: updated,
|
|
2658
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2508
2661
|
recoverExpiredRunLeases(now = new Date, opts = {}) {
|
|
2509
|
-
const
|
|
2662
|
+
const limit = Math.max(1, Math.min(1000, Math.floor(opts.limit ?? DEFAULT_RECOVERY_BATCH_LIMIT)));
|
|
2663
|
+
const scanLimit = Math.max(limit, Math.min(5000, Math.floor(opts.scanLimit ?? limit * DEFAULT_RECOVERY_SCAN_MULTIPLIER)));
|
|
2664
|
+
const rows = this.db.query(`SELECT * FROM loop_runs
|
|
2665
|
+
WHERE status = 'running' AND lease_expires_at <= ?
|
|
2666
|
+
ORDER BY lease_expires_at ASC
|
|
2667
|
+
LIMIT ?`).all(now.toISOString(), scanLimit);
|
|
2510
2668
|
const recovered = [];
|
|
2511
2669
|
for (const row of rows) {
|
|
2512
|
-
if (
|
|
2670
|
+
if (recovered.length >= limit)
|
|
2671
|
+
break;
|
|
2672
|
+
if (row.pid && isProcessAlive(row.pid)) {
|
|
2673
|
+
this.deferLiveExpiredRun(row.id, now, opts);
|
|
2513
2674
|
continue;
|
|
2514
|
-
|
|
2675
|
+
}
|
|
2676
|
+
if (this.hasLiveWorkflowStepProcesses(row.id)) {
|
|
2677
|
+
this.deferLiveExpiredRun(row.id, now, opts);
|
|
2515
2678
|
continue;
|
|
2679
|
+
}
|
|
2516
2680
|
const finished = now.toISOString();
|
|
2517
2681
|
this.db.exec("BEGIN IMMEDIATE");
|
|
2518
2682
|
try {
|
|
@@ -2565,6 +2729,7 @@ class Store {
|
|
|
2565
2729
|
loopRunId: row.id
|
|
2566
2730
|
});
|
|
2567
2731
|
this.setWorkflowWorkItemsForWorkflowRun(workflowRow.id, "failed", "parent loop run lease expired before completion", finished);
|
|
2732
|
+
this.maybeArchiveTerminalGeneratedRouteWorkflow(workflowRow.id, finished);
|
|
2568
2733
|
}
|
|
2569
2734
|
const loop = this.getLoop(row.loop_id);
|
|
2570
2735
|
const itemStatus = workItemStatusForLoopRun("abandoned", row.attempt, loop?.maxAttempts);
|
|
@@ -2572,6 +2737,15 @@ class Store {
|
|
|
2572
2737
|
const statuses = itemStatus === "admitted" ? ["admitted", "running", "failed"] : ["admitted", "running"];
|
|
2573
2738
|
const reason = itemStatus === "admitted" ? "run lease expired before completion; retry pending" : "run lease expired before completion";
|
|
2574
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
|
+
}
|
|
2575
2749
|
}
|
|
2576
2750
|
this.db.exec("COMMIT");
|
|
2577
2751
|
} catch (error) {
|
|
@@ -2909,6 +3083,7 @@ var AUTH_ENV_KEYS = [
|
|
|
2909
3083
|
"CODEWITH_HOME",
|
|
2910
3084
|
"CODEX_HOME",
|
|
2911
3085
|
"CURSOR_CONFIG_DIR",
|
|
3086
|
+
"CURSOR_API_KEY",
|
|
2912
3087
|
"OPENCODE_CONFIG_DIR",
|
|
2913
3088
|
"AICOPILOT_CONFIG_DIR",
|
|
2914
3089
|
"ANTHROPIC_API_KEY",
|
|
@@ -3038,6 +3213,10 @@ function assertSupportedAgentOptions(target) {
|
|
|
3038
3213
|
assertStringOption(target.model, `${target.provider}.model`);
|
|
3039
3214
|
assertStringOption(target.agent, `${target.provider}.agent`);
|
|
3040
3215
|
assertStringOption(target.authProfile, `${target.provider}.authProfile`);
|
|
3216
|
+
assertStringOption(target.configIsolation, `${target.provider}.configIsolation`);
|
|
3217
|
+
if (target.configIsolation !== undefined && target.configIsolation !== "safe" && target.configIsolation !== "none") {
|
|
3218
|
+
throw new Error(`${target.provider}.configIsolation must be safe or none`);
|
|
3219
|
+
}
|
|
3041
3220
|
if (target.authProfile !== undefined && target.provider !== "codewith") {
|
|
3042
3221
|
throw new Error(`${target.provider}.authProfile is supported only for codewith`);
|
|
3043
3222
|
}
|
|
@@ -3050,6 +3229,8 @@ function assertSupportedAgentOptions(target) {
|
|
|
3050
3229
|
if (target.sandbox && !["read-only", "workspace-write", "danger-full-access", "enabled", "disabled"].includes(target.sandbox)) {
|
|
3051
3230
|
throw new Error(`${target.provider}.sandbox is not supported: ${target.sandbox}`);
|
|
3052
3231
|
}
|
|
3232
|
+
if (target.provider === "codex" && target.agent !== undefined)
|
|
3233
|
+
throw new Error("codex.agent is not supported");
|
|
3053
3234
|
if (["codewith", "codex"].includes(target.provider)) {
|
|
3054
3235
|
if (target.permissionMode && !["default", "bypass"].includes(target.permissionMode)) {
|
|
3055
3236
|
throw new Error(`${target.provider}.permissionMode supports only default or bypass`);
|
|
@@ -3064,6 +3245,8 @@ function assertSupportedAgentOptions(target) {
|
|
|
3064
3245
|
return;
|
|
3065
3246
|
}
|
|
3066
3247
|
if (target.provider === "cursor") {
|
|
3248
|
+
if (target.variant !== undefined)
|
|
3249
|
+
throw new Error("cursor.variant is not supported");
|
|
3067
3250
|
if (target.permissionMode === "auto")
|
|
3068
3251
|
throw new Error("cursor.permissionMode auto is not supported; use provider-specific extraArgs for Cursor auto-review");
|
|
3069
3252
|
if (target.sandbox !== undefined && target.sandbox !== "enabled" && target.sandbox !== "disabled") {
|
|
@@ -3105,10 +3288,8 @@ function agentArgs(target) {
|
|
|
3105
3288
|
"set -eu",
|
|
3106
3289
|
"if command -v agent >/dev/null 2>&1; then",
|
|
3107
3290
|
' exec agent "$@"',
|
|
3108
|
-
"elif command -v cursor >/dev/null 2>&1; then",
|
|
3109
|
-
' exec cursor agent "$@"',
|
|
3110
3291
|
"else",
|
|
3111
|
-
" echo 'Executable not found in PATH:
|
|
3292
|
+
" echo 'Executable not found in PATH: agent' >&2",
|
|
3112
3293
|
" exit 127",
|
|
3113
3294
|
"fi"
|
|
3114
3295
|
].join(`
|
|
@@ -3149,7 +3330,7 @@ function agentArgs(target) {
|
|
|
3149
3330
|
case "codex":
|
|
3150
3331
|
if (target.variant)
|
|
3151
3332
|
args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
|
|
3152
|
-
args.push("exec", "--json", "--ephemeral", "--sandbox", codewithLikeSandbox(target), "--skip-git-repo-check");
|
|
3333
|
+
args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", codewithLikeSandbox(target), "--skip-git-repo-check");
|
|
3153
3334
|
if (isolation === "safe")
|
|
3154
3335
|
args.push("--ignore-rules");
|
|
3155
3336
|
if (target.cwd)
|
|
@@ -3219,7 +3400,7 @@ function commandSpec(target) {
|
|
|
3219
3400
|
account: agentTarget.account,
|
|
3220
3401
|
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
|
|
3221
3402
|
nativeAuthProfile: agentTarget.authProfile ? { provider: agentTarget.provider, profile: agentTarget.authProfile } : undefined,
|
|
3222
|
-
preflightAnyOf: agentTarget.provider === "cursor" ? ["
|
|
3403
|
+
preflightAnyOf: agentTarget.provider === "cursor" ? ["agent"] : undefined,
|
|
3223
3404
|
stdin: agentTarget.prompt,
|
|
3224
3405
|
allowlist: agentTarget.allowlist
|
|
3225
3406
|
};
|
|
@@ -4602,12 +4783,14 @@ function claimDueRuns(deps) {
|
|
|
4602
4783
|
const claimed = [];
|
|
4603
4784
|
const skipped = [];
|
|
4604
4785
|
const maxClaims = Math.max(0, deps.maxClaims ?? Number.POSITIVE_INFINITY);
|
|
4786
|
+
if (maxClaims === 0)
|
|
4787
|
+
return { claims, claimed, completed: [], skipped, recovered, expired };
|
|
4605
4788
|
for (const loop of deps.store.dueLoops(now)) {
|
|
4606
|
-
if (claims.length >= maxClaims)
|
|
4789
|
+
if (claims.length + skipped.length >= maxClaims)
|
|
4607
4790
|
break;
|
|
4608
4791
|
const plan = dueSlots(loop, now);
|
|
4609
4792
|
for (const slot of plan.slots) {
|
|
4610
|
-
if (claims.length >= maxClaims)
|
|
4793
|
+
if (claims.length + skipped.length >= maxClaims)
|
|
4611
4794
|
break;
|
|
4612
4795
|
const run = claimSlot(deps, loop, slot);
|
|
4613
4796
|
if (!run)
|
|
@@ -5057,7 +5240,7 @@ function enableStartup(result) {
|
|
|
5057
5240
|
// package.json
|
|
5058
5241
|
var package_default = {
|
|
5059
5242
|
name: "@hasna/loops",
|
|
5060
|
-
version: "0.3.
|
|
5243
|
+
version: "0.3.49",
|
|
5061
5244
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
5062
5245
|
type: "module",
|
|
5063
5246
|
main: "dist/index.js",
|