@hasna/loops 0.3.54 → 0.3.56

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) {
@@ -5383,6 +5557,21 @@ function gitRootFor(path) {
5383
5557
  return;
5384
5558
  }
5385
5559
  }
5560
+ function gitCommonDirFor(path) {
5561
+ if (!existsSync3(path))
5562
+ return;
5563
+ try {
5564
+ const raw = execFileSync("git", ["-C", path, "rev-parse", "--git-common-dir"], {
5565
+ encoding: "utf8",
5566
+ stdio: ["ignore", "pipe", "ignore"]
5567
+ }).trim();
5568
+ if (!raw)
5569
+ return;
5570
+ return isAbsolute2(raw) ? raw : resolve2(path, raw);
5571
+ } catch {
5572
+ return;
5573
+ }
5574
+ }
5386
5575
  function prepareWorktreeCommand(plan) {
5387
5576
  const repo = shellQuote2(plan.repoRoot);
5388
5577
  const path = shellQuote2(plan.path);
@@ -5499,6 +5688,7 @@ function worktreePlan(input, seed) {
5499
5688
  root,
5500
5689
  path: worktreePath,
5501
5690
  branch,
5691
+ gitMetadataDir: gitCommonDirFor(repoRoot),
5502
5692
  prepareStep
5503
5693
  };
5504
5694
  }
@@ -5546,6 +5736,10 @@ function agentTarget(input, prompt, role, seed, plan) {
5546
5736
  assertNativeAuthProfileSupport(input, provider);
5547
5737
  const sandbox = input.sandbox ?? (provider === "codewith" || provider === "codex" ? "workspace-write" : provider === "cursor" ? "enabled" : undefined);
5548
5738
  failClosedSandbox(input, provider, sandbox);
5739
+ const addDirs = [...input.addDirs ?? []];
5740
+ if (plan.enabled && plan.gitMetadataDir && (provider === "codewith" || provider === "codex") && sandbox === "workspace-write") {
5741
+ addDirs.push(plan.gitMetadataDir);
5742
+ }
5549
5743
  return {
5550
5744
  type: "agent",
5551
5745
  provider,
@@ -5554,7 +5748,7 @@ function agentTarget(input, prompt, role, seed, plan) {
5554
5748
  model: input.model,
5555
5749
  variant: input.variant,
5556
5750
  agent: input.agent,
5557
- addDirs: input.addDirs,
5751
+ addDirs: addDirs.length ? [...new Set(addDirs)] : undefined,
5558
5752
  authProfile: provider === "codewith" ? authProfileForRole(input, role, seed) : undefined,
5559
5753
  configIsolation: "safe",
5560
5754
  permissionMode: input.permissionMode ?? "bypass",
@@ -5576,7 +5770,7 @@ function agentTarget(input, prompt, role, seed, plan) {
5576
5770
  ...input.projectGroup ? { projectGroup: input.projectGroup } : {}
5577
5771
  },
5578
5772
  account: accountForRole(input, role, seed),
5579
- timeoutMs: 45 * 60000
5773
+ timeoutMs: agentTimeoutMs(input)
5580
5774
  };
5581
5775
  }
5582
5776
  function workflowStepsWithWorktree(plan, steps) {
@@ -6019,18 +6213,15 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
6019
6213
  name: "Worker",
6020
6214
  description: "Implement the todos task and record evidence.",
6021
6215
  target: agentTarget(input, workerPrompt, "worker", input.taskId, plan),
6022
- timeoutMs: 45 * 60000
6216
+ timeoutMs: agentTimeoutMs(input)
6023
6217
  },
6024
6218
  {
6025
6219
  id: "verifier",
6026
6220
  name: "Verifier",
6027
6221
  description: "Adversarially verify worker output and update todos.",
6028
6222
  dependsOn: ["worker"],
6029
- target: {
6030
- ...agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
6031
- idleTimeoutMs: 10 * 60000
6032
- },
6033
- timeoutMs: 30 * 60000
6223
+ target: agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
6224
+ timeoutMs: agentTimeoutMs(input)
6034
6225
  }
6035
6226
  ])
6036
6227
  };
@@ -6191,7 +6382,7 @@ function renderTaskLifecycleWorkflow(input) {
6191
6382
  name: "Triage",
6192
6383
  description: "Check task eligibility, duplicates, dependencies, and automation gates.",
6193
6384
  target: agentTarget(input, triagePrompt, "triage", input.taskId, plan),
6194
- timeoutMs: 20 * 60000
6385
+ timeoutMs: agentTimeoutMs(input)
6195
6386
  },
6196
6387
  {
6197
6388
  id: "triage-gate",
@@ -6213,7 +6404,7 @@ function renderTaskLifecycleWorkflow(input) {
6213
6404
  description: "Create a concise implementation plan and split unsafe scope before work starts.",
6214
6405
  dependsOn: ["triage-gate"],
6215
6406
  target: agentTarget(input, plannerPrompt, "planner", input.taskId, plan),
6216
- timeoutMs: 25 * 60000
6407
+ timeoutMs: agentTimeoutMs(input)
6217
6408
  },
6218
6409
  {
6219
6410
  id: "planner-gate",
@@ -6235,18 +6426,15 @@ function renderTaskLifecycleWorkflow(input) {
6235
6426
  description: "Implement the todos task according to triage and planner evidence.",
6236
6427
  dependsOn: ["planner-gate"],
6237
6428
  target: agentTarget(input, workerPrompt, "worker", input.taskId, plan),
6238
- timeoutMs: 45 * 60000
6429
+ timeoutMs: agentTimeoutMs(input)
6239
6430
  },
6240
6431
  {
6241
6432
  id: "verifier",
6242
6433
  name: "Verifier",
6243
6434
  description: "Adversarially verify worker output and update todos.",
6244
6435
  dependsOn: ["worker"],
6245
- target: {
6246
- ...agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
6247
- idleTimeoutMs: 10 * 60000
6248
- },
6249
- timeoutMs: 30 * 60000
6436
+ target: agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
6437
+ timeoutMs: agentTimeoutMs(input)
6250
6438
  }
6251
6439
  ])
6252
6440
  };
@@ -6316,18 +6504,15 @@ function renderEventWorkerVerifierWorkflow(input) {
6316
6504
  name: "Worker",
6317
6505
  description: "Handle the Hasna event and record evidence.",
6318
6506
  target: agentTarget(input, workerPrompt, "worker", seed, plan),
6319
- timeoutMs: 45 * 60000
6507
+ timeoutMs: agentTimeoutMs(input)
6320
6508
  },
6321
6509
  {
6322
6510
  id: "verifier",
6323
6511
  name: "Verifier",
6324
6512
  description: "Adversarially verify event handling.",
6325
6513
  dependsOn: ["worker"],
6326
- target: {
6327
- ...agentTarget(input, verifierPrompt, "verifier", seed, plan),
6328
- idleTimeoutMs: 10 * 60000
6329
- },
6330
- timeoutMs: 30 * 60000
6514
+ target: agentTarget(input, verifierPrompt, "verifier", seed, plan),
6515
+ timeoutMs: agentTimeoutMs(input)
6331
6516
  }
6332
6517
  ])
6333
6518
  };
@@ -6339,7 +6524,7 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
6339
6524
  throw new Error("projectPath is required");
6340
6525
  const seed = `${input.projectPath}:${input.objective}`;
6341
6526
  const plan = worktreePlan(input, seed);
6342
- const timeoutMs = input.timeoutMs && Number.isFinite(input.timeoutMs) ? input.timeoutMs : 45 * 60000;
6527
+ const timeoutMs = agentTimeoutMs(input);
6343
6528
  const workerPrompt = [
6344
6529
  `/goal ${input.objective}`,
6345
6530
  "",
@@ -6377,11 +6562,8 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
6377
6562
  name: "Verifier",
6378
6563
  description: "Adversarially verify the bounded objective result.",
6379
6564
  dependsOn: ["worker"],
6380
- target: {
6381
- ...agentTarget(input, verifierPrompt, "verifier", seed, plan),
6382
- idleTimeoutMs: 10 * 60000
6383
- },
6384
- timeoutMs: Math.min(timeoutMs, 30 * 60000)
6565
+ target: agentTarget(input, verifierPrompt, "verifier", seed, plan),
6566
+ timeoutMs
6385
6567
  }
6386
6568
  ])
6387
6569
  };
@@ -6410,7 +6592,7 @@ function renderLifecycleBoundedTemplate(id, values) {
6410
6592
  worktreeMode: values.worktreeMode ?? (id === REPORT_ONLY_TEMPLATE_ID ? "main" : "required"),
6411
6593
  worktreeRoot: values.worktreeRoot,
6412
6594
  worktreeBranchPrefix: values.worktreeBranchPrefix,
6413
- timeoutMs: values.timeoutMs ? Number(values.timeoutMs) : undefined
6595
+ timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
6414
6596
  };
6415
6597
  if (id === TASK_LIFECYCLE_TEMPLATE_ID) {
6416
6598
  const taskId = values.taskId ?? "";
@@ -6447,6 +6629,7 @@ function renderLifecycleBoundedTemplate(id, values) {
6447
6629
  worktreeMode: values.worktreeMode ?? "required",
6448
6630
  worktreeRoot: values.worktreeRoot,
6449
6631
  worktreeBranchPrefix: values.worktreeBranchPrefix,
6632
+ timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
6450
6633
  eventId: values.eventId,
6451
6634
  eventType: values.eventType
6452
6635
  });
@@ -6513,6 +6696,8 @@ function renderDeterministicCheckCreateTaskWorkflow(values) {
6513
6696
  if (!checkCommand.trim())
6514
6697
  throw new Error("checkCommand is required");
6515
6698
  const seed = `${projectPath}:${checkCommand}`;
6699
+ const timeoutMs = parseDeterministicTimeoutMs(values.timeoutMs, 5 * 60000);
6700
+ const idleTimeoutMs = parseDeterministicTimeoutMs(values.idleTimeoutMs, 60000, "idleTimeoutMs");
6516
6701
  return {
6517
6702
  name: values.name ?? `deterministic-check-${stableIndex(seed, 4294967295).toString(16).padStart(8, "0")}`,
6518
6703
  description: values.description ?? "Deterministic check that writes compact evidence and upserts one deduped todos task when the expectation is not met.",
@@ -6527,10 +6712,10 @@ function renderDeterministicCheckCreateTaskWorkflow(values) {
6527
6712
  command: "bash",
6528
6713
  args: ["-lc", checkCommand],
6529
6714
  cwd: projectPath,
6530
- timeoutMs: values.timeoutMs ? Number(values.timeoutMs) : 5 * 60000,
6531
- idleTimeoutMs: values.idleTimeoutMs ? Number(values.idleTimeoutMs) : 60000
6715
+ timeoutMs,
6716
+ idleTimeoutMs
6532
6717
  },
6533
- timeoutMs: values.timeoutMs ? Number(values.timeoutMs) : 5 * 60000
6718
+ timeoutMs
6534
6719
  }
6535
6720
  ]
6536
6721
  };
@@ -6568,6 +6753,7 @@ function renderBuiltinLoopTemplate(id, values) {
6568
6753
  worktreeMode: values.worktreeMode,
6569
6754
  worktreeRoot: values.worktreeRoot,
6570
6755
  worktreeBranchPrefix: values.worktreeBranchPrefix,
6756
+ timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
6571
6757
  eventId: values.eventId,
6572
6758
  eventType: values.eventType
6573
6759
  });
@@ -6599,7 +6785,8 @@ function renderBuiltinLoopTemplate(id, values) {
6599
6785
  manualBreakGlass: booleanVar(values.manualBreakGlass),
6600
6786
  worktreeMode: values.worktreeMode,
6601
6787
  worktreeRoot: values.worktreeRoot,
6602
- worktreeBranchPrefix: values.worktreeBranchPrefix
6788
+ worktreeBranchPrefix: values.worktreeBranchPrefix,
6789
+ timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
6603
6790
  });
6604
6791
  }
6605
6792
  if (id === BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID) {
@@ -6627,7 +6814,7 @@ function renderBuiltinLoopTemplate(id, values) {
6627
6814
  worktreeMode: values.worktreeMode,
6628
6815
  worktreeRoot: values.worktreeRoot,
6629
6816
  worktreeBranchPrefix: values.worktreeBranchPrefix,
6630
- timeoutMs: values.timeoutMs ? Number(values.timeoutMs) : undefined
6817
+ timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
6631
6818
  });
6632
6819
  }
6633
6820
  throw new Error(`unknown template: ${id}`);
@@ -6862,7 +7049,11 @@ function runDoctor(store) {
6862
7049
  if (loop.target.type === "workflow") {
6863
7050
  const workflow = store.requireWorkflow(loop.target.workflowId);
6864
7051
  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 });
7052
+ preflightTarget({
7053
+ ...step.target,
7054
+ account: step.account ?? step.target.account,
7055
+ timeoutMs: step.timeoutMs !== undefined ? step.timeoutMs : step.target.timeoutMs
7056
+ }, { loopId: loop.id, loopName: loop.name, workflowId: workflow.id, workflowName: workflow.name, workflowStepId: step.id }, { machine: loop.machine });
6866
7057
  }
6867
7058
  } else {
6868
7059
  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;