@hasna/loops 0.3.54 → 0.3.55

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
@@ -329,6 +329,15 @@ function optionalPositiveInteger(value, label) {
329
329
  throw new Error(`${label} must be a positive integer`);
330
330
  return value;
331
331
  }
332
+ function optionalTimeoutMs(value, label) {
333
+ if (value === undefined)
334
+ return;
335
+ if (value === null)
336
+ return null;
337
+ if (!Number.isInteger(value) || value <= 0)
338
+ throw new Error(`${label} must be a positive integer or null for unlimited`);
339
+ return value;
340
+ }
332
341
  function optionalBoolean(value, label) {
333
342
  if (value === undefined)
334
343
  return;
@@ -417,7 +426,7 @@ function validateTarget(value, label, opts) {
417
426
  assertObject(value, label);
418
427
  if (value.type === "command") {
419
428
  assertString(value.command, `${label}.command`);
420
- optionalPositiveInteger(value.timeoutMs, `${label}.timeoutMs`);
429
+ optionalTimeoutMs(value.timeoutMs, `${label}.timeoutMs`);
421
430
  optionalPositiveInteger(value.idleTimeoutMs, `${label}.idleTimeoutMs`);
422
431
  if (value.shell !== true && /\s/.test(value.command.trim())) {
423
432
  throw new Error(`${label}.command must be an executable without spaces when shell is false; put flags in args or set shell true`);
@@ -436,7 +445,7 @@ function validateTarget(value, label, opts) {
436
445
  const providers = ["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"];
437
446
  if (!providers.includes(value.provider))
438
447
  throw new Error(`${label}.provider must be one of ${providers.join(", ")}`);
439
- optionalPositiveInteger(value.timeoutMs, `${label}.timeoutMs`);
448
+ optionalTimeoutMs(value.timeoutMs, `${label}.timeoutMs`);
440
449
  optionalPositiveInteger(value.idleTimeoutMs, `${label}.idleTimeoutMs`);
441
450
  if (value.authProfile !== undefined) {
442
451
  assertString(value.authProfile, `${label}.authProfile`);
@@ -559,7 +568,7 @@ function normalizeCreateWorkflowInput(input, opts = {}) {
559
568
  target: validateTarget(step.target, `workflow.steps[${index}].target`, opts),
560
569
  dependsOn: optionalStringArray(step.dependsOn, `workflow.steps[${index}].dependsOn`) ?? [],
561
570
  continueOnFailure: optionalBoolean(step.continueOnFailure, `workflow.steps[${index}].continueOnFailure`) ?? false,
562
- timeoutMs: optionalPositiveInteger(step.timeoutMs, `workflow.steps[${index}].timeoutMs`),
571
+ timeoutMs: optionalTimeoutMs(step.timeoutMs, `workflow.steps[${index}].timeoutMs`),
563
572
  account: optionalAccountRef(step.account, `workflow.steps[${index}].account`)
564
573
  };
565
574
  });
@@ -1305,6 +1314,17 @@ class Store {
1305
1314
  const row = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 1").get(name);
1306
1315
  return row ? rowToLoop(row) : undefined;
1307
1316
  }
1317
+ requireUniqueLoop(idOrName) {
1318
+ const byId = this.getLoop(idOrName);
1319
+ if (byId)
1320
+ return byId;
1321
+ const rows = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 2").all(idOrName);
1322
+ if (rows.length === 0)
1323
+ throw new Error(`loop not found: ${idOrName}`);
1324
+ if (rows.length > 1)
1325
+ throw new Error(`ambiguous loop name: ${idOrName}; use a loop id`);
1326
+ return rowToLoop(rows[0]);
1327
+ }
1308
1328
  requireLoop(idOrName) {
1309
1329
  return this.getLoop(idOrName) ?? this.findLoopByName(idOrName) ?? (() => {
1310
1330
  throw new Error(`loop not found: ${idOrName}`);
@@ -1379,6 +1399,130 @@ class Store {
1379
1399
  throw new Error(`loop not found after update: ${id}`);
1380
1400
  return after;
1381
1401
  }
1402
+ activeLoopReferenceCount(workflowId) {
1403
+ const rows = this.db.query("SELECT target_json FROM loops WHERE archived_at IS NULL").all();
1404
+ let count = 0;
1405
+ for (const row of rows) {
1406
+ try {
1407
+ const target = JSON.parse(row.target_json);
1408
+ if (target.type === "workflow" && target.workflowId === workflowId)
1409
+ count += 1;
1410
+ } catch {}
1411
+ }
1412
+ return count;
1413
+ }
1414
+ archiveWorkflowIfUnreferenced(workflowId, updated) {
1415
+ if (this.activeLoopReferenceCount(workflowId) > 0)
1416
+ return;
1417
+ const workflow = this.getWorkflow(workflowId);
1418
+ if (!workflow || workflow.status !== "active")
1419
+ return;
1420
+ const res = this.db.query("UPDATE workflow_specs SET status='archived', updated_at=? WHERE id=? AND status='active'").run(updated, workflowId);
1421
+ if (res.changes !== 1)
1422
+ return;
1423
+ return this.getWorkflow(workflowId);
1424
+ }
1425
+ retargetWorkflowLoop(idOrName, workflowId, opts = {}) {
1426
+ const updated = (opts.now ?? new Date).toISOString();
1427
+ this.db.exec("BEGIN IMMEDIATE");
1428
+ try {
1429
+ const current = this.requireLoop(idOrName);
1430
+ if (current.target.type !== "workflow")
1431
+ throw new Error(`loop is not a workflow loop: ${idOrName}`);
1432
+ if (this.hasRunningRun(current.id))
1433
+ throw new Error(`refusing to retarget running loop: ${current.id}`);
1434
+ const workflow = this.requireWorkflow(workflowId);
1435
+ const target = { ...current.target, workflowId: workflow.id };
1436
+ if (opts.workflowTimeoutMs !== undefined)
1437
+ target.timeoutMs = opts.workflowTimeoutMs;
1438
+ const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
1439
+ WHERE id=$id
1440
+ AND ($daemonLeaseId IS NULL OR EXISTS (
1441
+ SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
1442
+ ))`).run({
1443
+ $id: current.id,
1444
+ $target: JSON.stringify(target),
1445
+ $updated: updated,
1446
+ $daemonLeaseId: opts.daemonLeaseId ?? null,
1447
+ $now: updated
1448
+ });
1449
+ if (res.changes !== 1)
1450
+ throw new Error("daemon lease lost");
1451
+ this.db.exec("COMMIT");
1452
+ const after = this.getLoop(current.id);
1453
+ if (!after)
1454
+ throw new Error(`loop not found after retarget: ${current.id}`);
1455
+ return after;
1456
+ } catch (error) {
1457
+ try {
1458
+ this.db.exec("ROLLBACK");
1459
+ } catch {}
1460
+ throw error;
1461
+ }
1462
+ }
1463
+ createAndRetargetWorkflowLoop(idOrName, workflowInput, opts = {}) {
1464
+ const normalized = normalizeCreateWorkflowInput(workflowInput);
1465
+ const updated = (opts.now ?? new Date).toISOString();
1466
+ this.db.exec("BEGIN IMMEDIATE");
1467
+ try {
1468
+ const current = this.requireUniqueLoop(idOrName);
1469
+ if (current.target.type !== "workflow")
1470
+ throw new Error(`loop is not a workflow loop: ${idOrName}`);
1471
+ if (this.hasRunningRun(current.id))
1472
+ throw new Error(`refusing to retarget running loop: ${current.id}`);
1473
+ const previousWorkflow = this.requireWorkflow(current.target.workflowId);
1474
+ const workflow = {
1475
+ id: genId(),
1476
+ name: normalized.name,
1477
+ description: normalized.description,
1478
+ version: normalized.version ?? 1,
1479
+ status: "active",
1480
+ goal: normalized.goal,
1481
+ steps: normalized.steps,
1482
+ createdAt: updated,
1483
+ updatedAt: updated
1484
+ };
1485
+ this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
1486
+ VALUES ($id, $name, $description, $version, $status, $goal, $steps, $created, $updated)`).run({
1487
+ $id: workflow.id,
1488
+ $name: workflow.name,
1489
+ $description: workflow.description ?? null,
1490
+ $version: workflow.version,
1491
+ $status: workflow.status,
1492
+ $goal: workflow.goal ? JSON.stringify(workflow.goal) : null,
1493
+ $steps: JSON.stringify(workflow.steps),
1494
+ $created: workflow.createdAt,
1495
+ $updated: workflow.updatedAt
1496
+ });
1497
+ const target = { ...current.target, workflowId: workflow.id };
1498
+ if (opts.workflowTimeoutMs !== undefined)
1499
+ target.timeoutMs = opts.workflowTimeoutMs;
1500
+ const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
1501
+ WHERE id=$id
1502
+ AND ($daemonLeaseId IS NULL OR EXISTS (
1503
+ SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
1504
+ ))`).run({
1505
+ $id: current.id,
1506
+ $target: JSON.stringify(target),
1507
+ $updated: updated,
1508
+ $daemonLeaseId: opts.daemonLeaseId ?? null,
1509
+ $now: updated
1510
+ });
1511
+ if (res.changes !== 1)
1512
+ throw new Error("daemon lease lost");
1513
+ const archivedOld = opts.archiveOld ? this.archiveWorkflowIfUnreferenced(previousWorkflow.id, updated) : undefined;
1514
+ this.db.exec("COMMIT");
1515
+ const loop = this.getLoop(current.id);
1516
+ if (!loop)
1517
+ throw new Error(`loop not found after retarget: ${current.id}`);
1518
+ return { loop, workflow, previousWorkflow, archivedOld };
1519
+ } catch (error) {
1520
+ try {
1521
+ this.db.exec("ROLLBACK");
1522
+ } catch {}
1523
+ throw error;
1524
+ }
1525
+ }
1382
1526
  renameLoop(id, name, opts = {}) {
1383
1527
  const current = this.getLoop(id);
1384
1528
  if (!current)
@@ -3480,7 +3624,7 @@ function commandSpec(target) {
3480
3624
  cwd: commandTarget.cwd,
3481
3625
  shell: commandTarget.shell,
3482
3626
  env: commandTarget.env,
3483
- timeoutMs: commandTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
3627
+ timeoutMs: commandTarget.timeoutMs === undefined ? DEFAULT_TIMEOUT_MS : commandTarget.timeoutMs,
3484
3628
  idleTimeoutMs: commandTarget.idleTimeoutMs,
3485
3629
  account: commandTarget.account,
3486
3630
  accountTool: commandTarget.account?.tool
@@ -3491,7 +3635,7 @@ function commandSpec(target) {
3491
3635
  command: providerCommand(agentTarget.provider),
3492
3636
  args: agentArgs(agentTarget),
3493
3637
  cwd: agentTarget.cwd,
3494
- timeoutMs: agentTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
3638
+ timeoutMs: agentTarget.timeoutMs ?? null,
3495
3639
  idleTimeoutMs: agentTarget.idleTimeoutMs,
3496
3640
  account: agentTarget.account,
3497
3641
  accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
@@ -3679,12 +3823,12 @@ async function executeRemoteSpec(spec, machine, metadata, opts) {
3679
3823
  if (opts.signal?.aborted)
3680
3824
  abortHandler();
3681
3825
  opts.signal?.addEventListener("abort", abortHandler, { once: true });
3682
- const timer = setTimeout(() => {
3826
+ const timer = typeof spec.timeoutMs === "number" ? setTimeout(() => {
3683
3827
  timedOut = true;
3684
3828
  if (child.pid)
3685
3829
  killProcessGroup(child.pid);
3686
- }, spec.timeoutMs);
3687
- timer.unref();
3830
+ }, spec.timeoutMs) : undefined;
3831
+ timer?.unref();
3688
3832
  let idleTimer;
3689
3833
  const resetIdleTimer = () => {
3690
3834
  if (!spec.idleTimeoutMs)
@@ -3716,7 +3860,8 @@ async function executeRemoteSpec(spec, machine, metadata, opts) {
3716
3860
  } catch (err) {
3717
3861
  error = err instanceof Error ? err.message : String(err);
3718
3862
  } finally {
3719
- clearTimeout(timer);
3863
+ if (timer)
3864
+ clearTimeout(timer);
3720
3865
  if (idleTimer)
3721
3866
  clearTimeout(idleTimer);
3722
3867
  opts.signal?.removeEventListener("abort", abortHandler);
@@ -3858,12 +4003,12 @@ async function executeTarget(target, metadata = {}, opts = {}) {
3858
4003
  if (opts.signal?.aborted)
3859
4004
  abortHandler();
3860
4005
  opts.signal?.addEventListener("abort", abortHandler, { once: true });
3861
- const timer = setTimeout(() => {
4006
+ const timer = typeof spec.timeoutMs === "number" ? setTimeout(() => {
3862
4007
  timedOut = true;
3863
4008
  if (child.pid)
3864
4009
  killProcessGroup(child.pid);
3865
- }, spec.timeoutMs);
3866
- timer.unref();
4010
+ }, spec.timeoutMs) : undefined;
4011
+ timer?.unref();
3867
4012
  let idleTimer;
3868
4013
  const resetIdleTimer = () => {
3869
4014
  if (!spec.idleTimeoutMs)
@@ -3895,7 +4040,8 @@ async function executeTarget(target, metadata = {}, opts = {}) {
3895
4040
  } catch (err) {
3896
4041
  error = err instanceof Error ? err.message : String(err);
3897
4042
  } finally {
3898
- clearTimeout(timer);
4043
+ if (timer)
4044
+ clearTimeout(timer);
3899
4045
  if (idleTimer)
3900
4046
  clearTimeout(idleTimer);
3901
4047
  opts.signal?.removeEventListener("abort", abortHandler);
@@ -4320,7 +4466,7 @@ ${result.stderr}`);
4320
4466
  // src/lib/workflow-runner.ts
4321
4467
  function targetWithStepAccount(step) {
4322
4468
  const account = step.account ?? step.target.account;
4323
- const timeoutMs = step.timeoutMs ?? step.target.timeoutMs;
4469
+ const timeoutMs = step.timeoutMs !== undefined ? step.timeoutMs : step.target.timeoutMs;
4324
4470
  if (!account && timeoutMs === step.target.timeoutMs)
4325
4471
  return step.target;
4326
4472
  return { ...step.target, account, timeoutMs };
@@ -4616,7 +4762,8 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
4616
4762
  })
4617
4763
  });
4618
4764
  }
4619
- const controller = loop.target.timeoutMs ? new AbortController : undefined;
4765
+ const workflowTimeoutMs = typeof loop.target.timeoutMs === "number" ? loop.target.timeoutMs : undefined;
4766
+ const controller = workflowTimeoutMs !== undefined ? new AbortController : undefined;
4620
4767
  let workflowTimedOut = false;
4621
4768
  const externalAbort = () => controller?.abort();
4622
4769
  if (controller && opts.signal?.aborted)
@@ -4626,13 +4773,13 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
4626
4773
  const timer = controller ? setTimeout(() => {
4627
4774
  workflowTimedOut = true;
4628
4775
  controller.abort();
4629
- }, loop.target.timeoutMs) : undefined;
4776
+ }, workflowTimeoutMs) : undefined;
4630
4777
  timer?.unref();
4631
4778
  try {
4632
4779
  return await executeWorkflow(store, workflow, {
4633
4780
  ...opts,
4634
4781
  signal: controller?.signal ?? opts.signal,
4635
- signalTimeoutMessage: () => workflowTimedOut && loop.target.type === "workflow" ? `workflow timed out after ${loop.target.timeoutMs}ms` : undefined,
4782
+ signalTimeoutMessage: () => workflowTimedOut && loop.target.type === "workflow" ? `workflow timed out after ${workflowTimeoutMs}ms` : undefined,
4636
4783
  loop,
4637
4784
  loopRun: run,
4638
4785
  scheduledFor: run.scheduledFor,
@@ -5123,7 +5270,8 @@ var TEMPLATE_SUMMARIES = [
5123
5270
  { name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
5124
5271
  { name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
5125
5272
  { name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
5126
- { name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated task/event worktree branches." }
5273
+ { name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated task/event worktree branches." },
5274
+ { name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." }
5127
5275
  ]
5128
5276
  },
5129
5277
  {
@@ -5153,7 +5301,8 @@ var TEMPLATE_SUMMARIES = [
5153
5301
  { name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
5154
5302
  { name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
5155
5303
  { name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
5156
- { name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated event worktree branches." }
5304
+ { name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated event worktree branches." },
5305
+ { name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." }
5157
5306
  ]
5158
5307
  },
5159
5308
  {
@@ -5181,7 +5330,7 @@ var TEMPLATE_SUMMARIES = [
5181
5330
  { name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
5182
5331
  { name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
5183
5332
  { name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated bounded-agent worktree branches." },
5184
- { name: "timeoutMs", default: "2700000", description: "Step timeout in milliseconds." }
5333
+ { name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." }
5185
5334
  ]
5186
5335
  },
5187
5336
  {
@@ -5200,7 +5349,8 @@ var TEMPLATE_SUMMARIES = [
5200
5349
  { name: "accountPool", description: "Comma-separated OpenAccounts profiles for non-Codewith providers." },
5201
5350
  { name: "provider", default: "codewith", description: "Agent provider." },
5202
5351
  { name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
5203
- { name: "worktreeMode", default: "required", description: "Worktree isolation mode." }
5352
+ { name: "worktreeMode", default: "required", description: "Worktree isolation mode." },
5353
+ { name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." }
5204
5354
  ]
5205
5355
  },
5206
5356
  {
@@ -5305,6 +5455,30 @@ function taskLabel(input) {
5305
5455
  const head = input.taskTitle?.trim() || input.taskId;
5306
5456
  return head.length > 160 ? `${head.slice(0, 157)}...` : head;
5307
5457
  }
5458
+ var UNLIMITED_AGENT_TIMEOUT_MS = null;
5459
+ function agentTimeoutMs(input) {
5460
+ return input.timeoutMs === undefined ? UNLIMITED_AGENT_TIMEOUT_MS : input.timeoutMs;
5461
+ }
5462
+ function parseTemplateTimeoutMs(raw) {
5463
+ if (raw === undefined || raw.trim() === "")
5464
+ return;
5465
+ const normalized = raw.trim().toLowerCase();
5466
+ if (["unlimited", "none", "null", "never", "off", "false"].includes(normalized))
5467
+ return null;
5468
+ const value = Number(raw);
5469
+ if (!Number.isInteger(value) || value <= 0) {
5470
+ throw new Error("timeoutMs must be a positive integer number of milliseconds, or unlimited/none/null");
5471
+ }
5472
+ return value;
5473
+ }
5474
+ function parseDeterministicTimeoutMs(raw, fallbackMs, label = "timeoutMs") {
5475
+ if (raw === undefined || raw.trim() === "")
5476
+ return fallbackMs;
5477
+ const value = Number(raw);
5478
+ if (!Number.isInteger(value) || value <= 0)
5479
+ throw new Error(`${label} must be a positive integer number of milliseconds`);
5480
+ return value;
5481
+ }
5308
5482
  function stableIndex(seed, size) {
5309
5483
  let hash = 2166136261;
5310
5484
  for (let i = 0;i < seed.length; i += 1) {
@@ -5576,7 +5750,7 @@ function agentTarget(input, prompt, role, seed, plan) {
5576
5750
  ...input.projectGroup ? { projectGroup: input.projectGroup } : {}
5577
5751
  },
5578
5752
  account: accountForRole(input, role, seed),
5579
- timeoutMs: 45 * 60000
5753
+ timeoutMs: agentTimeoutMs(input)
5580
5754
  };
5581
5755
  }
5582
5756
  function workflowStepsWithWorktree(plan, steps) {
@@ -6019,18 +6193,15 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
6019
6193
  name: "Worker",
6020
6194
  description: "Implement the todos task and record evidence.",
6021
6195
  target: agentTarget(input, workerPrompt, "worker", input.taskId, plan),
6022
- timeoutMs: 45 * 60000
6196
+ timeoutMs: agentTimeoutMs(input)
6023
6197
  },
6024
6198
  {
6025
6199
  id: "verifier",
6026
6200
  name: "Verifier",
6027
6201
  description: "Adversarially verify worker output and update todos.",
6028
6202
  dependsOn: ["worker"],
6029
- target: {
6030
- ...agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
6031
- idleTimeoutMs: 10 * 60000
6032
- },
6033
- timeoutMs: 30 * 60000
6203
+ target: agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
6204
+ timeoutMs: agentTimeoutMs(input)
6034
6205
  }
6035
6206
  ])
6036
6207
  };
@@ -6191,7 +6362,7 @@ function renderTaskLifecycleWorkflow(input) {
6191
6362
  name: "Triage",
6192
6363
  description: "Check task eligibility, duplicates, dependencies, and automation gates.",
6193
6364
  target: agentTarget(input, triagePrompt, "triage", input.taskId, plan),
6194
- timeoutMs: 20 * 60000
6365
+ timeoutMs: agentTimeoutMs(input)
6195
6366
  },
6196
6367
  {
6197
6368
  id: "triage-gate",
@@ -6213,7 +6384,7 @@ function renderTaskLifecycleWorkflow(input) {
6213
6384
  description: "Create a concise implementation plan and split unsafe scope before work starts.",
6214
6385
  dependsOn: ["triage-gate"],
6215
6386
  target: agentTarget(input, plannerPrompt, "planner", input.taskId, plan),
6216
- timeoutMs: 25 * 60000
6387
+ timeoutMs: agentTimeoutMs(input)
6217
6388
  },
6218
6389
  {
6219
6390
  id: "planner-gate",
@@ -6235,18 +6406,15 @@ function renderTaskLifecycleWorkflow(input) {
6235
6406
  description: "Implement the todos task according to triage and planner evidence.",
6236
6407
  dependsOn: ["planner-gate"],
6237
6408
  target: agentTarget(input, workerPrompt, "worker", input.taskId, plan),
6238
- timeoutMs: 45 * 60000
6409
+ timeoutMs: agentTimeoutMs(input)
6239
6410
  },
6240
6411
  {
6241
6412
  id: "verifier",
6242
6413
  name: "Verifier",
6243
6414
  description: "Adversarially verify worker output and update todos.",
6244
6415
  dependsOn: ["worker"],
6245
- target: {
6246
- ...agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
6247
- idleTimeoutMs: 10 * 60000
6248
- },
6249
- timeoutMs: 30 * 60000
6416
+ target: agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
6417
+ timeoutMs: agentTimeoutMs(input)
6250
6418
  }
6251
6419
  ])
6252
6420
  };
@@ -6316,18 +6484,15 @@ function renderEventWorkerVerifierWorkflow(input) {
6316
6484
  name: "Worker",
6317
6485
  description: "Handle the Hasna event and record evidence.",
6318
6486
  target: agentTarget(input, workerPrompt, "worker", seed, plan),
6319
- timeoutMs: 45 * 60000
6487
+ timeoutMs: agentTimeoutMs(input)
6320
6488
  },
6321
6489
  {
6322
6490
  id: "verifier",
6323
6491
  name: "Verifier",
6324
6492
  description: "Adversarially verify event handling.",
6325
6493
  dependsOn: ["worker"],
6326
- target: {
6327
- ...agentTarget(input, verifierPrompt, "verifier", seed, plan),
6328
- idleTimeoutMs: 10 * 60000
6329
- },
6330
- timeoutMs: 30 * 60000
6494
+ target: agentTarget(input, verifierPrompt, "verifier", seed, plan),
6495
+ timeoutMs: agentTimeoutMs(input)
6331
6496
  }
6332
6497
  ])
6333
6498
  };
@@ -6339,7 +6504,7 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
6339
6504
  throw new Error("projectPath is required");
6340
6505
  const seed = `${input.projectPath}:${input.objective}`;
6341
6506
  const plan = worktreePlan(input, seed);
6342
- const timeoutMs = input.timeoutMs && Number.isFinite(input.timeoutMs) ? input.timeoutMs : 45 * 60000;
6507
+ const timeoutMs = agentTimeoutMs(input);
6343
6508
  const workerPrompt = [
6344
6509
  `/goal ${input.objective}`,
6345
6510
  "",
@@ -6377,11 +6542,8 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
6377
6542
  name: "Verifier",
6378
6543
  description: "Adversarially verify the bounded objective result.",
6379
6544
  dependsOn: ["worker"],
6380
- target: {
6381
- ...agentTarget(input, verifierPrompt, "verifier", seed, plan),
6382
- idleTimeoutMs: 10 * 60000
6383
- },
6384
- timeoutMs: Math.min(timeoutMs, 30 * 60000)
6545
+ target: agentTarget(input, verifierPrompt, "verifier", seed, plan),
6546
+ timeoutMs
6385
6547
  }
6386
6548
  ])
6387
6549
  };
@@ -6410,7 +6572,7 @@ function renderLifecycleBoundedTemplate(id, values) {
6410
6572
  worktreeMode: values.worktreeMode ?? (id === REPORT_ONLY_TEMPLATE_ID ? "main" : "required"),
6411
6573
  worktreeRoot: values.worktreeRoot,
6412
6574
  worktreeBranchPrefix: values.worktreeBranchPrefix,
6413
- timeoutMs: values.timeoutMs ? Number(values.timeoutMs) : undefined
6575
+ timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
6414
6576
  };
6415
6577
  if (id === TASK_LIFECYCLE_TEMPLATE_ID) {
6416
6578
  const taskId = values.taskId ?? "";
@@ -6447,6 +6609,7 @@ function renderLifecycleBoundedTemplate(id, values) {
6447
6609
  worktreeMode: values.worktreeMode ?? "required",
6448
6610
  worktreeRoot: values.worktreeRoot,
6449
6611
  worktreeBranchPrefix: values.worktreeBranchPrefix,
6612
+ timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
6450
6613
  eventId: values.eventId,
6451
6614
  eventType: values.eventType
6452
6615
  });
@@ -6513,6 +6676,8 @@ function renderDeterministicCheckCreateTaskWorkflow(values) {
6513
6676
  if (!checkCommand.trim())
6514
6677
  throw new Error("checkCommand is required");
6515
6678
  const seed = `${projectPath}:${checkCommand}`;
6679
+ const timeoutMs = parseDeterministicTimeoutMs(values.timeoutMs, 5 * 60000);
6680
+ const idleTimeoutMs = parseDeterministicTimeoutMs(values.idleTimeoutMs, 60000, "idleTimeoutMs");
6516
6681
  return {
6517
6682
  name: values.name ?? `deterministic-check-${stableIndex(seed, 4294967295).toString(16).padStart(8, "0")}`,
6518
6683
  description: values.description ?? "Deterministic check that writes compact evidence and upserts one deduped todos task when the expectation is not met.",
@@ -6527,10 +6692,10 @@ function renderDeterministicCheckCreateTaskWorkflow(values) {
6527
6692
  command: "bash",
6528
6693
  args: ["-lc", checkCommand],
6529
6694
  cwd: projectPath,
6530
- timeoutMs: values.timeoutMs ? Number(values.timeoutMs) : 5 * 60000,
6531
- idleTimeoutMs: values.idleTimeoutMs ? Number(values.idleTimeoutMs) : 60000
6695
+ timeoutMs,
6696
+ idleTimeoutMs
6532
6697
  },
6533
- timeoutMs: values.timeoutMs ? Number(values.timeoutMs) : 5 * 60000
6698
+ timeoutMs
6534
6699
  }
6535
6700
  ]
6536
6701
  };
@@ -6568,6 +6733,7 @@ function renderBuiltinLoopTemplate(id, values) {
6568
6733
  worktreeMode: values.worktreeMode,
6569
6734
  worktreeRoot: values.worktreeRoot,
6570
6735
  worktreeBranchPrefix: values.worktreeBranchPrefix,
6736
+ timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
6571
6737
  eventId: values.eventId,
6572
6738
  eventType: values.eventType
6573
6739
  });
@@ -6599,7 +6765,8 @@ function renderBuiltinLoopTemplate(id, values) {
6599
6765
  manualBreakGlass: booleanVar(values.manualBreakGlass),
6600
6766
  worktreeMode: values.worktreeMode,
6601
6767
  worktreeRoot: values.worktreeRoot,
6602
- worktreeBranchPrefix: values.worktreeBranchPrefix
6768
+ worktreeBranchPrefix: values.worktreeBranchPrefix,
6769
+ timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
6603
6770
  });
6604
6771
  }
6605
6772
  if (id === BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID) {
@@ -6627,7 +6794,7 @@ function renderBuiltinLoopTemplate(id, values) {
6627
6794
  worktreeMode: values.worktreeMode,
6628
6795
  worktreeRoot: values.worktreeRoot,
6629
6796
  worktreeBranchPrefix: values.worktreeBranchPrefix,
6630
- timeoutMs: values.timeoutMs ? Number(values.timeoutMs) : undefined
6797
+ timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
6631
6798
  });
6632
6799
  }
6633
6800
  throw new Error(`unknown template: ${id}`);
@@ -6862,7 +7029,11 @@ function runDoctor(store) {
6862
7029
  if (loop.target.type === "workflow") {
6863
7030
  const workflow = store.requireWorkflow(loop.target.workflowId);
6864
7031
  for (const step of workflowExecutionOrder(workflow)) {
6865
- preflightTarget({ ...step.target, account: step.account ?? step.target.account, timeoutMs: step.timeoutMs ?? step.target.timeoutMs }, { loopId: loop.id, loopName: loop.name, workflowId: workflow.id, workflowName: workflow.name, workflowStepId: step.id }, { machine: loop.machine });
7032
+ preflightTarget({
7033
+ ...step.target,
7034
+ account: step.account ?? step.target.account,
7035
+ timeoutMs: step.timeoutMs !== undefined ? step.timeoutMs : step.target.timeoutMs
7036
+ }, { loopId: loop.id, loopName: loop.name, workflowId: workflow.id, workflowName: workflow.name, workflowStepId: step.id }, { machine: loop.machine });
6866
7037
  }
6867
7038
  } else {
6868
7039
  preflightTarget(loop.target, { loopId: loop.id, loopName: loop.name }, { machine: loop.machine });
@@ -1,4 +1,4 @@
1
- import type { CreateLoopInput, CreateWorkflowInvocationInput, CreateWorkflowInput, Goal, GoalAutoExecute, GoalPlanNode, GoalRun, GoalStatus, Loop, LoopRun, LoopStatus, RunStatus, WorkflowEvent, WorkflowInvocation, WorkflowRun, WorkflowRunStatus, WorkflowSpec, WorkflowStepRun, WorkflowWorkItem, WorkflowWorkItemStatus, UpsertWorkflowWorkItemInput } from "../types.js";
1
+ import type { CreateLoopInput, CreateWorkflowInvocationInput, CreateWorkflowInput, Goal, GoalAutoExecute, GoalPlanNode, GoalRun, GoalStatus, Loop, LoopRun, LoopStatus, RunStatus, TimeoutMs, WorkflowEvent, WorkflowInvocation, WorkflowRun, WorkflowRunStatus, WorkflowSpec, WorkflowStepRun, WorkflowWorkItem, WorkflowWorkItemStatus, UpsertWorkflowWorkItemInput } from "../types.js";
2
2
  interface DaemonLeaseFence {
3
3
  daemonLeaseId?: string;
4
4
  now?: Date;
@@ -74,6 +74,7 @@ export declare class Store {
74
74
  createLoop(input: CreateLoopInput, from?: Date): Loop;
75
75
  getLoop(id: string): Loop | undefined;
76
76
  findLoopByName(name: string): Loop | undefined;
77
+ requireUniqueLoop(idOrName: string): Loop;
77
78
  requireLoop(idOrName: string): Loop;
78
79
  listLoops(opts?: {
79
80
  status?: LoopStatus;
@@ -83,6 +84,20 @@ export declare class Store {
83
84
  }): Loop[];
84
85
  dueLoops(now: Date, limit?: number): Loop[];
85
86
  updateLoop(id: string, patch: Partial<Pick<Loop, "status" | "nextRunAt" | "retryScheduledFor" | "expiresAt">>, opts?: DaemonLeaseFence): Loop;
87
+ private activeLoopReferenceCount;
88
+ private archiveWorkflowIfUnreferenced;
89
+ retargetWorkflowLoop(idOrName: string, workflowId: string, opts?: DaemonLeaseFence & {
90
+ workflowTimeoutMs?: TimeoutMs;
91
+ }): Loop;
92
+ createAndRetargetWorkflowLoop(idOrName: string, workflowInput: CreateWorkflowInput, opts?: DaemonLeaseFence & {
93
+ workflowTimeoutMs?: TimeoutMs;
94
+ archiveOld?: boolean;
95
+ }): {
96
+ loop: Loop;
97
+ workflow: WorkflowSpec;
98
+ previousWorkflow: WorkflowSpec;
99
+ archivedOld?: WorkflowSpec;
100
+ };
86
101
  renameLoop(id: string, name: string, opts?: DaemonLeaseFence): Loop;
87
102
  archiveLoop(idOrName: string): Loop;
88
103
  unarchiveLoop(idOrName: string): Loop;