@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/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`).all(now.toISOString());
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
- const merged = { ...current, ...patch, updatedAt: updated };
1258
- this.db.query(`UPDATE loops SET status=$status, next_run_at=$nextRun, retry_scheduled_for=$retrySlot,
1259
- expires_at=$expiresAt, updated_at=$updated
1260
- WHERE id=$id
1261
- AND ($daemonLeaseId IS NULL OR EXISTS (
1262
- SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
1263
- ))`).run({
1264
- $id: id,
1265
- $status: merged.status,
1266
- $nextRun: merged.nextRunAt ?? null,
1267
- $retrySlot: merged.retryScheduledFor ?? null,
1268
- $expiresAt: merged.expiresAt ?? null,
1269
- $updated: merged.updatedAt,
1270
- $daemonLeaseId: opts.daemonLeaseId ?? null,
1271
- $now: updated
1272
- });
1273
- if (patch.status && patch.status !== "active") {
1274
- const status = patch.status === "paused" ? "deferred" : "cancelled";
1275
- this.setWorkflowWorkItemsForLoop(id, status, `loop ${patch.status}`, updated);
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 limit = opts.limit ?? 200;
1394
- 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);
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 rows = this.db.query("SELECT * FROM loop_runs WHERE status = 'running' AND lease_expires_at <= ?").all(now.toISOString());
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 (row.pid && isProcessAlive(row.pid))
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
- if (this.hasLiveWorkflowStepProcesses(row.id))
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: cursor agent or agent' >&2",
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" ? ["cursor", "agent"] : undefined,
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
- "cursor agent",
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"]
@@ -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
- recoverExpiredRunLeases(now?: Date, opts?: DaemonLeaseFence): LoopRun[];
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;