@hasna/loops 0.3.13 → 0.3.15

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.
@@ -482,6 +482,8 @@ function rowToLoop(row) {
482
482
  name: row.name,
483
483
  description: row.description ?? undefined,
484
484
  status: row.status,
485
+ archivedAt: row.archived_at ?? undefined,
486
+ archivedFromStatus: row.archived_from_status ? row.archived_from_status : undefined,
485
487
  schedule: JSON.parse(row.schedule_json),
486
488
  target: JSON.parse(row.target_json),
487
489
  goal: row.goal_json ? JSON.parse(row.goal_json) : undefined,
@@ -691,6 +693,8 @@ class Store {
691
693
  name TEXT NOT NULL,
692
694
  description TEXT,
693
695
  status TEXT NOT NULL,
696
+ archived_at TEXT,
697
+ archived_from_status TEXT,
694
698
  schedule_json TEXT NOT NULL,
695
699
  target_json TEXT NOT NULL,
696
700
  goal_json TEXT,
@@ -891,6 +895,8 @@ class Store {
891
895
  `);
892
896
  this.addColumnIfMissing("loops", "machine_json", "TEXT");
893
897
  this.addColumnIfMissing("loops", "goal_json", "TEXT");
898
+ this.addColumnIfMissing("loops", "archived_at", "TEXT");
899
+ this.addColumnIfMissing("loops", "archived_from_status", "TEXT");
894
900
  this.addColumnIfMissing("loop_runs", "goal_run_id", "TEXT");
895
901
  this.addColumnIfMissing("workflow_specs", "goal_json", "TEXT");
896
902
  this.addColumnIfMissing("workflow_runs", "goal_run_id", "TEXT");
@@ -899,6 +905,7 @@ class Store {
899
905
  this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0001_initial_and_workflows", nowIso());
900
906
  this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0002_loop_machines", nowIso());
901
907
  this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0003_goals", nowIso());
908
+ this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0004_loop_archive_metadata", nowIso());
902
909
  }
903
910
  addColumnIfMissing(table, column, definition) {
904
911
  const columns = this.db.query(`PRAGMA table_info(${table})`).all();
@@ -975,12 +982,26 @@ class Store {
975
982
  }
976
983
  listLoops(opts = {}) {
977
984
  const limit = opts.limit ?? 200;
978
- const rows = opts.status ? this.db.query("SELECT * FROM loops WHERE status = ? ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit) : this.db.query("SELECT * FROM loops ORDER BY status ASC, next_run_at ASC LIMIT ?").all(limit);
985
+ let rows;
986
+ if (opts.status && opts.archived) {
987
+ rows = this.db.query("SELECT * FROM loops WHERE status = ? AND archived_at IS NOT NULL ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit);
988
+ } else if (opts.status && opts.includeArchived) {
989
+ rows = this.db.query("SELECT * FROM loops WHERE status = ? ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit);
990
+ } else if (opts.status) {
991
+ rows = this.db.query("SELECT * FROM loops WHERE status = ? AND archived_at IS NULL ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit);
992
+ } else if (opts.archived) {
993
+ rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NOT NULL ORDER BY archived_at DESC LIMIT ?").all(limit);
994
+ } else if (opts.includeArchived) {
995
+ rows = this.db.query("SELECT * FROM loops ORDER BY status ASC, next_run_at ASC LIMIT ?").all(limit);
996
+ } else {
997
+ rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NULL ORDER BY status ASC, next_run_at ASC LIMIT ?").all(limit);
998
+ }
979
999
  return rows.map(rowToLoop);
980
1000
  }
981
1001
  dueLoops(now) {
982
1002
  const rows = this.db.query(`SELECT * FROM loops
983
1003
  WHERE status = 'active'
1004
+ AND archived_at IS NULL
984
1005
  AND next_run_at IS NOT NULL
985
1006
  AND next_run_at <= ?
986
1007
  ORDER BY next_run_at ASC`).all(now.toISOString());
@@ -1012,6 +1033,44 @@ class Store {
1012
1033
  throw new Error(`loop not found after update: ${id}`);
1013
1034
  return after;
1014
1035
  }
1036
+ archiveLoop(idOrName) {
1037
+ const loop = this.requireLoop(idOrName);
1038
+ if (loop.archivedAt)
1039
+ return loop;
1040
+ const updated = nowIso();
1041
+ const archivedStatus = loop.status === "active" ? "paused" : loop.status;
1042
+ this.db.query(`UPDATE loops
1043
+ SET status=$status, archived_at=$archivedAt, archived_from_status=$archivedFromStatus, updated_at=$updated
1044
+ WHERE id=$id`).run({
1045
+ $id: loop.id,
1046
+ $status: archivedStatus,
1047
+ $archivedAt: updated,
1048
+ $archivedFromStatus: loop.status,
1049
+ $updated: updated
1050
+ });
1051
+ const archived = this.getLoop(loop.id);
1052
+ if (!archived)
1053
+ throw new Error(`loop not found after archive: ${loop.id}`);
1054
+ return archived;
1055
+ }
1056
+ unarchiveLoop(idOrName) {
1057
+ const loop = this.requireLoop(idOrName);
1058
+ if (!loop.archivedAt)
1059
+ return loop;
1060
+ const updated = nowIso();
1061
+ const restoredStatus = loop.archivedFromStatus ?? loop.status;
1062
+ this.db.query(`UPDATE loops
1063
+ SET status=$status, archived_at=NULL, archived_from_status=NULL, updated_at=$updated
1064
+ WHERE id=$id`).run({
1065
+ $id: loop.id,
1066
+ $status: restoredStatus,
1067
+ $updated: updated
1068
+ });
1069
+ const unarchived = this.getLoop(loop.id);
1070
+ if (!unarchived)
1071
+ throw new Error(`loop not found after unarchive: ${loop.id}`);
1072
+ return unarchived;
1073
+ }
1015
1074
  deleteLoop(idOrName) {
1016
1075
  const loop = this.requireLoop(idOrName);
1017
1076
  const res = this.db.query("DELETE FROM loops WHERE id = ?").run(loop.id);
@@ -1786,10 +1845,16 @@ class Store {
1786
1845
  }
1787
1846
  claimRun(loop, scheduledFor, runnerId, now = new Date, opts = {}) {
1788
1847
  const startedAt = now.toISOString();
1789
- const leaseExpiresAt = new Date(now.getTime() + loop.leaseMs).toISOString();
1790
1848
  this.db.exec("BEGIN IMMEDIATE");
1791
1849
  try {
1792
1850
  this.assertDaemonLeaseFence(opts, startedAt);
1851
+ const currentLoop = this.getLoop(loop.id);
1852
+ if (!currentLoop || currentLoop.archivedAt) {
1853
+ this.db.exec("COMMIT");
1854
+ return;
1855
+ }
1856
+ loop = currentLoop;
1857
+ const leaseExpiresAt = new Date(now.getTime() + loop.leaseMs).toISOString();
1793
1858
  const existing = this.getRunBySlot(loop.id, scheduledFor);
1794
1859
  if (existing) {
1795
1860
  if (existing.status === "running") {
@@ -2007,7 +2072,7 @@ class Store {
2007
2072
  return recovered;
2008
2073
  }
2009
2074
  expireLoops(now = new Date, opts = {}) {
2010
- const rows = this.db.query("SELECT * FROM loops WHERE status = 'active' AND expires_at IS NOT NULL AND expires_at <= ?").all(now.toISOString());
2075
+ const rows = this.db.query("SELECT * FROM loops WHERE status = 'active' AND archived_at IS NULL AND expires_at IS NOT NULL AND expires_at <= ?").all(now.toISOString());
2011
2076
  const expired = [];
2012
2077
  for (const row of rows) {
2013
2078
  const updated = this.updateLoop(row.id, { status: "expired", nextRunAt: undefined }, opts);
@@ -2016,8 +2081,21 @@ class Store {
2016
2081
  }
2017
2082
  return expired;
2018
2083
  }
2019
- countLoops(status) {
2020
- const row = status ? this.db.query("SELECT COUNT(*) AS count FROM loops WHERE status = ?").get(status) : this.db.query("SELECT COUNT(*) AS count FROM loops").get();
2084
+ countLoops(status, opts = {}) {
2085
+ let row;
2086
+ if (status && opts.archived) {
2087
+ row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE status = ? AND archived_at IS NOT NULL").get(status);
2088
+ } else if (status && opts.includeArchived) {
2089
+ row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE status = ?").get(status);
2090
+ } else if (status) {
2091
+ row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE status = ? AND archived_at IS NULL").get(status);
2092
+ } else if (opts.archived) {
2093
+ row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE archived_at IS NOT NULL").get();
2094
+ } else if (opts.includeArchived) {
2095
+ row = this.db.query("SELECT COUNT(*) AS count FROM loops").get();
2096
+ } else {
2097
+ row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE archived_at IS NULL").get();
2098
+ }
2021
2099
  return row?.count ?? 0;
2022
2100
  }
2023
2101
  countRuns(status) {
@@ -3597,12 +3675,16 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
3597
3675
 
3598
3676
  // src/lib/scheduler.ts
3599
3677
  function manualRunScheduledFor(loop, now = new Date) {
3678
+ if (loop.archivedAt)
3679
+ return now.toISOString();
3600
3680
  if (loop.status === "active" && loop.nextRunAt && new Date(loop.nextRunAt).getTime() <= now.getTime()) {
3601
3681
  return loop.retryScheduledFor ?? loop.nextRunAt;
3602
3682
  }
3603
3683
  return now.toISOString();
3604
3684
  }
3605
3685
  function shouldAdvanceManualRun(loop, scheduledFor, now = new Date) {
3686
+ if (loop.archivedAt)
3687
+ return false;
3606
3688
  if (loop.status !== "active")
3607
3689
  return false;
3608
3690
  if (!loop.nextRunAt || new Date(loop.nextRunAt).getTime() > now.getTime())
@@ -3610,6 +3692,8 @@ function shouldAdvanceManualRun(loop, scheduledFor, now = new Date) {
3610
3692
  return scheduledFor === (loop.retryScheduledFor ?? loop.nextRunAt);
3611
3693
  }
3612
3694
  function manualRunSource(loop, scheduledFor, now = new Date) {
3695
+ if (loop.archivedAt)
3696
+ return "ad_hoc";
3613
3697
  if (loop.status !== "active")
3614
3698
  return "ad_hoc";
3615
3699
  if (!loop.nextRunAt || new Date(loop.nextRunAt).getTime() > now.getTime())
@@ -3628,7 +3712,7 @@ function advanceLoop(store, loop, run, finishedAt, succeeded, opts = {}) {
3628
3712
  if (run.status === "running")
3629
3713
  return;
3630
3714
  const current = store.getLoop(loop.id);
3631
- if (!current || current.status !== "active")
3715
+ if (!current || current.status !== "active" || current.archivedAt)
3632
3716
  return;
3633
3717
  if (current.retryScheduledFor && current.retryScheduledFor !== run.scheduledFor)
3634
3718
  return;
@@ -3963,7 +4047,8 @@ function daemonStatus(store, path = pidFilePath()) {
3963
4047
  active: store.countLoops("active"),
3964
4048
  paused: store.countLoops("paused"),
3965
4049
  stopped: store.countLoops("stopped"),
3966
- expired: store.countLoops("expired")
4050
+ expired: store.countLoops("expired"),
4051
+ archived: store.countLoops(undefined, { archived: true })
3967
4052
  },
3968
4053
  runs: {
3969
4054
  total: store.countRuns(),
@@ -4276,7 +4361,7 @@ function enableStartup(result) {
4276
4361
  // package.json
4277
4362
  var package_default = {
4278
4363
  name: "@hasna/loops",
4279
- version: "0.3.13",
4364
+ version: "0.3.15",
4280
4365
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
4281
4366
  type: "module",
4282
4367
  main: "dist/index.js",
@@ -4340,7 +4425,7 @@ var package_default = {
4340
4425
  bun: ">=1.0.0"
4341
4426
  },
4342
4427
  dependencies: {
4343
- "@hasna/events": "^0.1.8",
4428
+ "@hasna/events": "^0.1.9",
4344
4429
  "@hasna/machines": "0.0.49",
4345
4430
  "@openrouter/ai-sdk-provider": "2.9.1",
4346
4431
  ai: "6.0.204",
package/dist/index.js CHANGED
@@ -480,6 +480,8 @@ function rowToLoop(row) {
480
480
  name: row.name,
481
481
  description: row.description ?? undefined,
482
482
  status: row.status,
483
+ archivedAt: row.archived_at ?? undefined,
484
+ archivedFromStatus: row.archived_from_status ? row.archived_from_status : undefined,
483
485
  schedule: JSON.parse(row.schedule_json),
484
486
  target: JSON.parse(row.target_json),
485
487
  goal: row.goal_json ? JSON.parse(row.goal_json) : undefined,
@@ -689,6 +691,8 @@ class Store {
689
691
  name TEXT NOT NULL,
690
692
  description TEXT,
691
693
  status TEXT NOT NULL,
694
+ archived_at TEXT,
695
+ archived_from_status TEXT,
692
696
  schedule_json TEXT NOT NULL,
693
697
  target_json TEXT NOT NULL,
694
698
  goal_json TEXT,
@@ -889,6 +893,8 @@ class Store {
889
893
  `);
890
894
  this.addColumnIfMissing("loops", "machine_json", "TEXT");
891
895
  this.addColumnIfMissing("loops", "goal_json", "TEXT");
896
+ this.addColumnIfMissing("loops", "archived_at", "TEXT");
897
+ this.addColumnIfMissing("loops", "archived_from_status", "TEXT");
892
898
  this.addColumnIfMissing("loop_runs", "goal_run_id", "TEXT");
893
899
  this.addColumnIfMissing("workflow_specs", "goal_json", "TEXT");
894
900
  this.addColumnIfMissing("workflow_runs", "goal_run_id", "TEXT");
@@ -897,6 +903,7 @@ class Store {
897
903
  this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0001_initial_and_workflows", nowIso());
898
904
  this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0002_loop_machines", nowIso());
899
905
  this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0003_goals", nowIso());
906
+ this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0004_loop_archive_metadata", nowIso());
900
907
  }
901
908
  addColumnIfMissing(table, column, definition) {
902
909
  const columns = this.db.query(`PRAGMA table_info(${table})`).all();
@@ -973,12 +980,26 @@ class Store {
973
980
  }
974
981
  listLoops(opts = {}) {
975
982
  const limit = opts.limit ?? 200;
976
- const rows = opts.status ? this.db.query("SELECT * FROM loops WHERE status = ? ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit) : this.db.query("SELECT * FROM loops ORDER BY status ASC, next_run_at ASC LIMIT ?").all(limit);
983
+ let rows;
984
+ if (opts.status && opts.archived) {
985
+ rows = this.db.query("SELECT * FROM loops WHERE status = ? AND archived_at IS NOT NULL ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit);
986
+ } else if (opts.status && opts.includeArchived) {
987
+ rows = this.db.query("SELECT * FROM loops WHERE status = ? ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit);
988
+ } else if (opts.status) {
989
+ rows = this.db.query("SELECT * FROM loops WHERE status = ? AND archived_at IS NULL ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit);
990
+ } else if (opts.archived) {
991
+ rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NOT NULL ORDER BY archived_at DESC LIMIT ?").all(limit);
992
+ } else if (opts.includeArchived) {
993
+ rows = this.db.query("SELECT * FROM loops ORDER BY status ASC, next_run_at ASC LIMIT ?").all(limit);
994
+ } else {
995
+ rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NULL ORDER BY status ASC, next_run_at ASC LIMIT ?").all(limit);
996
+ }
977
997
  return rows.map(rowToLoop);
978
998
  }
979
999
  dueLoops(now) {
980
1000
  const rows = this.db.query(`SELECT * FROM loops
981
1001
  WHERE status = 'active'
1002
+ AND archived_at IS NULL
982
1003
  AND next_run_at IS NOT NULL
983
1004
  AND next_run_at <= ?
984
1005
  ORDER BY next_run_at ASC`).all(now.toISOString());
@@ -1010,6 +1031,44 @@ class Store {
1010
1031
  throw new Error(`loop not found after update: ${id}`);
1011
1032
  return after;
1012
1033
  }
1034
+ archiveLoop(idOrName) {
1035
+ const loop = this.requireLoop(idOrName);
1036
+ if (loop.archivedAt)
1037
+ return loop;
1038
+ const updated = nowIso();
1039
+ const archivedStatus = loop.status === "active" ? "paused" : loop.status;
1040
+ this.db.query(`UPDATE loops
1041
+ SET status=$status, archived_at=$archivedAt, archived_from_status=$archivedFromStatus, updated_at=$updated
1042
+ WHERE id=$id`).run({
1043
+ $id: loop.id,
1044
+ $status: archivedStatus,
1045
+ $archivedAt: updated,
1046
+ $archivedFromStatus: loop.status,
1047
+ $updated: updated
1048
+ });
1049
+ const archived = this.getLoop(loop.id);
1050
+ if (!archived)
1051
+ throw new Error(`loop not found after archive: ${loop.id}`);
1052
+ return archived;
1053
+ }
1054
+ unarchiveLoop(idOrName) {
1055
+ const loop = this.requireLoop(idOrName);
1056
+ if (!loop.archivedAt)
1057
+ return loop;
1058
+ const updated = nowIso();
1059
+ const restoredStatus = loop.archivedFromStatus ?? loop.status;
1060
+ this.db.query(`UPDATE loops
1061
+ SET status=$status, archived_at=NULL, archived_from_status=NULL, updated_at=$updated
1062
+ WHERE id=$id`).run({
1063
+ $id: loop.id,
1064
+ $status: restoredStatus,
1065
+ $updated: updated
1066
+ });
1067
+ const unarchived = this.getLoop(loop.id);
1068
+ if (!unarchived)
1069
+ throw new Error(`loop not found after unarchive: ${loop.id}`);
1070
+ return unarchived;
1071
+ }
1013
1072
  deleteLoop(idOrName) {
1014
1073
  const loop = this.requireLoop(idOrName);
1015
1074
  const res = this.db.query("DELETE FROM loops WHERE id = ?").run(loop.id);
@@ -1784,10 +1843,16 @@ class Store {
1784
1843
  }
1785
1844
  claimRun(loop, scheduledFor, runnerId, now = new Date, opts = {}) {
1786
1845
  const startedAt = now.toISOString();
1787
- const leaseExpiresAt = new Date(now.getTime() + loop.leaseMs).toISOString();
1788
1846
  this.db.exec("BEGIN IMMEDIATE");
1789
1847
  try {
1790
1848
  this.assertDaemonLeaseFence(opts, startedAt);
1849
+ const currentLoop = this.getLoop(loop.id);
1850
+ if (!currentLoop || currentLoop.archivedAt) {
1851
+ this.db.exec("COMMIT");
1852
+ return;
1853
+ }
1854
+ loop = currentLoop;
1855
+ const leaseExpiresAt = new Date(now.getTime() + loop.leaseMs).toISOString();
1791
1856
  const existing = this.getRunBySlot(loop.id, scheduledFor);
1792
1857
  if (existing) {
1793
1858
  if (existing.status === "running") {
@@ -2005,7 +2070,7 @@ class Store {
2005
2070
  return recovered;
2006
2071
  }
2007
2072
  expireLoops(now = new Date, opts = {}) {
2008
- const rows = this.db.query("SELECT * FROM loops WHERE status = 'active' AND expires_at IS NOT NULL AND expires_at <= ?").all(now.toISOString());
2073
+ const rows = this.db.query("SELECT * FROM loops WHERE status = 'active' AND archived_at IS NULL AND expires_at IS NOT NULL AND expires_at <= ?").all(now.toISOString());
2009
2074
  const expired = [];
2010
2075
  for (const row of rows) {
2011
2076
  const updated = this.updateLoop(row.id, { status: "expired", nextRunAt: undefined }, opts);
@@ -2014,8 +2079,21 @@ class Store {
2014
2079
  }
2015
2080
  return expired;
2016
2081
  }
2017
- countLoops(status) {
2018
- const row = status ? this.db.query("SELECT COUNT(*) AS count FROM loops WHERE status = ?").get(status) : this.db.query("SELECT COUNT(*) AS count FROM loops").get();
2082
+ countLoops(status, opts = {}) {
2083
+ let row;
2084
+ if (status && opts.archived) {
2085
+ row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE status = ? AND archived_at IS NOT NULL").get(status);
2086
+ } else if (status && opts.includeArchived) {
2087
+ row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE status = ?").get(status);
2088
+ } else if (status) {
2089
+ row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE status = ? AND archived_at IS NULL").get(status);
2090
+ } else if (opts.archived) {
2091
+ row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE archived_at IS NOT NULL").get();
2092
+ } else if (opts.includeArchived) {
2093
+ row = this.db.query("SELECT COUNT(*) AS count FROM loops").get();
2094
+ } else {
2095
+ row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE archived_at IS NULL").get();
2096
+ }
2019
2097
  return row?.count ?? 0;
2020
2098
  }
2021
2099
  countRuns(status) {
@@ -3587,12 +3665,16 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
3587
3665
 
3588
3666
  // src/lib/scheduler.ts
3589
3667
  function manualRunScheduledFor(loop, now = new Date) {
3668
+ if (loop.archivedAt)
3669
+ return now.toISOString();
3590
3670
  if (loop.status === "active" && loop.nextRunAt && new Date(loop.nextRunAt).getTime() <= now.getTime()) {
3591
3671
  return loop.retryScheduledFor ?? loop.nextRunAt;
3592
3672
  }
3593
3673
  return now.toISOString();
3594
3674
  }
3595
3675
  function shouldAdvanceManualRun(loop, scheduledFor, now = new Date) {
3676
+ if (loop.archivedAt)
3677
+ return false;
3596
3678
  if (loop.status !== "active")
3597
3679
  return false;
3598
3680
  if (!loop.nextRunAt || new Date(loop.nextRunAt).getTime() > now.getTime())
@@ -3600,6 +3682,8 @@ function shouldAdvanceManualRun(loop, scheduledFor, now = new Date) {
3600
3682
  return scheduledFor === (loop.retryScheduledFor ?? loop.nextRunAt);
3601
3683
  }
3602
3684
  function manualRunSource(loop, scheduledFor, now = new Date) {
3685
+ if (loop.archivedAt)
3686
+ return "ad_hoc";
3603
3687
  if (loop.status !== "active")
3604
3688
  return "ad_hoc";
3605
3689
  if (!loop.nextRunAt || new Date(loop.nextRunAt).getTime() > now.getTime())
@@ -3618,7 +3702,7 @@ function advanceLoop(store, loop, run, finishedAt, succeeded, opts = {}) {
3618
3702
  if (run.status === "running")
3619
3703
  return;
3620
3704
  const current = store.getLoop(loop.id);
3621
- if (!current || current.status !== "active")
3705
+ if (!current || current.status !== "active" || current.archivedAt)
3622
3706
  return;
3623
3707
  if (current.retryScheduledFor && current.retryScheduledFor !== run.scheduledFor)
3624
3708
  return;
@@ -3903,16 +3987,28 @@ class LoopsClient {
3903
3987
  }
3904
3988
  pause(idOrName) {
3905
3989
  const loop = this.get(idOrName);
3990
+ if (loop.archivedAt)
3991
+ throw new Error(`loop is archived; unarchive it before pausing: ${idOrName}`);
3906
3992
  return this.store.updateLoop(loop.id, { status: "paused" });
3907
3993
  }
3908
3994
  resume(idOrName) {
3909
3995
  const loop = this.get(idOrName);
3996
+ if (loop.archivedAt)
3997
+ throw new Error(`loop is archived; unarchive it before resuming: ${idOrName}`);
3910
3998
  return this.store.updateLoop(loop.id, { status: "active" });
3911
3999
  }
3912
4000
  stop(idOrName) {
3913
4001
  const loop = this.get(idOrName);
4002
+ if (loop.archivedAt)
4003
+ throw new Error(`loop is archived; unarchive it before stopping: ${idOrName}`);
3914
4004
  return this.store.updateLoop(loop.id, { status: "stopped", nextRunAt: undefined });
3915
4005
  }
4006
+ archive(idOrName) {
4007
+ return this.store.archiveLoop(idOrName);
4008
+ }
4009
+ unarchive(idOrName) {
4010
+ return this.store.unarchiveLoop(idOrName);
4011
+ }
3916
4012
  delete(idOrName) {
3917
4013
  return this.store.deleteLoop(idOrName);
3918
4014
  }
@@ -3931,6 +4027,8 @@ class LoopsClient {
3931
4027
  }
3932
4028
  async runNow(idOrName) {
3933
4029
  const loop = this.get(idOrName);
4030
+ if (loop.archivedAt)
4031
+ throw new Error(`loop is archived; unarchive it before running: ${idOrName}`);
3934
4032
  const now = new Date;
3935
4033
  let scheduledFor = manualRunScheduledFor(loop, now);
3936
4034
  let shouldAdvance = shouldAdvanceManualRun(loop, scheduledFor, now);
@@ -3974,6 +4072,10 @@ var TEMPLATE_SUMMARIES = [
3974
4072
  { name: "projectPath", required: true, description: "Repository or project working directory." },
3975
4073
  { name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
3976
4074
  { name: "authProfile", description: "Provider-native auth profile, currently Codewith." },
4075
+ { name: "authProfilePool", description: "Comma-separated provider-native auth profiles; worker/verifier are selected deterministically." },
4076
+ { name: "workerAuthProfile", description: "Provider-native auth profile for the worker step." },
4077
+ { name: "verifierAuthProfile", description: "Provider-native auth profile for the verifier step." },
4078
+ { name: "accountPool", description: "Comma-separated OpenAccounts profiles; worker/verifier are selected deterministically." },
3977
4079
  { name: "model", description: "Provider model." },
3978
4080
  { name: "variant", description: "Provider reasoning/model effort variant." },
3979
4081
  { name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
@@ -3993,6 +4095,10 @@ var TEMPLATE_SUMMARIES = [
3993
4095
  { name: "projectPath", required: true, description: "Repository or project working directory." },
3994
4096
  { name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
3995
4097
  { name: "authProfile", description: "Provider-native auth profile, currently Codewith." },
4098
+ { name: "authProfilePool", description: "Comma-separated provider-native auth profiles; worker/verifier are selected deterministically." },
4099
+ { name: "workerAuthProfile", description: "Provider-native auth profile for the worker step." },
4100
+ { name: "verifierAuthProfile", description: "Provider-native auth profile for the verifier step." },
4101
+ { name: "accountPool", description: "Comma-separated OpenAccounts profiles; worker/verifier are selected deterministically." },
3996
4102
  { name: "model", description: "Provider model." },
3997
4103
  { name: "variant", description: "Provider reasoning/model effort variant." },
3998
4104
  { name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
@@ -4007,7 +4113,37 @@ function taskLabel(input) {
4007
4113
  const head = input.taskTitle?.trim() || input.taskId;
4008
4114
  return head.length > 160 ? `${head.slice(0, 157)}...` : head;
4009
4115
  }
4010
- function agentTarget(input, prompt) {
4116
+ function stableIndex(seed, size) {
4117
+ let hash = 2166136261;
4118
+ for (let i = 0;i < seed.length; i += 1) {
4119
+ hash ^= seed.charCodeAt(i);
4120
+ hash = Math.imul(hash, 16777619);
4121
+ }
4122
+ return Math.abs(hash >>> 0) % size;
4123
+ }
4124
+ function rolePoolValue(pool, seed, role) {
4125
+ if (!pool?.length)
4126
+ return;
4127
+ const workerIndex = stableIndex(seed, pool.length);
4128
+ if (role === "worker" || pool.length === 1)
4129
+ return pool[workerIndex];
4130
+ return pool[(workerIndex + 1) % pool.length];
4131
+ }
4132
+ function authProfileForRole(input, role, seed) {
4133
+ if (role === "worker" && input.workerAuthProfile)
4134
+ return input.workerAuthProfile;
4135
+ if (role === "verifier" && input.verifierAuthProfile)
4136
+ return input.verifierAuthProfile;
4137
+ return rolePoolValue(input.authProfilePool, seed, role) ?? input.authProfile;
4138
+ }
4139
+ function accountForRole(input, role, seed) {
4140
+ if (role === "worker" && input.workerAccount)
4141
+ return input.workerAccount;
4142
+ if (role === "verifier" && input.verifierAccount)
4143
+ return input.verifierAccount;
4144
+ return rolePoolValue(input.accountPool, seed, role) ?? input.account;
4145
+ }
4146
+ function agentTarget(input, prompt, role, seed) {
4011
4147
  const provider = input.provider ?? "codewith";
4012
4148
  const sandbox = input.sandbox ?? (provider === "codewith" || provider === "codex" ? "danger-full-access" : provider === "cursor" ? "disabled" : undefined);
4013
4149
  return {
@@ -4018,11 +4154,11 @@ function agentTarget(input, prompt) {
4018
4154
  model: input.model,
4019
4155
  variant: input.variant,
4020
4156
  agent: input.agent,
4021
- authProfile: provider === "codewith" ? input.authProfile : undefined,
4157
+ authProfile: provider === "codewith" ? authProfileForRole(input, role, seed) : undefined,
4022
4158
  configIsolation: "safe",
4023
4159
  permissionMode: input.permissionMode ?? "bypass",
4024
4160
  sandbox,
4025
- account: input.account,
4161
+ account: accountForRole(input, role, seed),
4026
4162
  timeoutMs: 45 * 60000
4027
4163
  };
4028
4164
  }
@@ -4076,7 +4212,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
4076
4212
  id: "worker",
4077
4213
  name: "Worker",
4078
4214
  description: "Implement the todos task and record evidence.",
4079
- target: agentTarget(input, workerPrompt),
4215
+ target: agentTarget(input, workerPrompt, "worker", input.taskId),
4080
4216
  timeoutMs: 45 * 60000
4081
4217
  },
4082
4218
  {
@@ -4084,7 +4220,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
4084
4220
  name: "Verifier",
4085
4221
  description: "Adversarially verify worker output and update todos.",
4086
4222
  dependsOn: ["worker"],
4087
- target: agentTarget(input, verifierPrompt),
4223
+ target: agentTarget(input, verifierPrompt, "verifier", input.taskId),
4088
4224
  timeoutMs: 30 * 60000
4089
4225
  }
4090
4226
  ]
@@ -4140,7 +4276,7 @@ function renderEventWorkerVerifierWorkflow(input) {
4140
4276
  id: "worker",
4141
4277
  name: "Worker",
4142
4278
  description: "Handle the Hasna event and record evidence.",
4143
- target: agentTarget(input, workerPrompt),
4279
+ target: agentTarget(input, workerPrompt, "worker", `${input.eventSource}:${input.eventType}:${input.eventId}`),
4144
4280
  timeoutMs: 45 * 60000
4145
4281
  },
4146
4282
  {
@@ -4148,7 +4284,7 @@ function renderEventWorkerVerifierWorkflow(input) {
4148
4284
  name: "Verifier",
4149
4285
  description: "Adversarially verify event handling.",
4150
4286
  dependsOn: ["worker"],
4151
- target: agentTarget(input, verifierPrompt),
4287
+ target: agentTarget(input, verifierPrompt, "verifier", `${input.eventSource}:${input.eventType}:${input.eventId}`),
4152
4288
  timeoutMs: 30 * 60000
4153
4289
  }
4154
4290
  ]
@@ -4163,7 +4299,11 @@ function renderLoopTemplate(id, values) {
4163
4299
  projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
4164
4300
  provider: values.provider,
4165
4301
  authProfile: values.authProfile,
4302
+ authProfilePool: listVar(values.authProfilePool),
4303
+ workerAuthProfile: values.workerAuthProfile,
4304
+ verifierAuthProfile: values.verifierAuthProfile,
4166
4305
  account: values.account ? { profile: values.account, tool: values.accountTool } : undefined,
4306
+ accountPool: accountPoolVar(values.accountPool, values.accountTool),
4167
4307
  model: values.model,
4168
4308
  variant: values.variant,
4169
4309
  agent: values.agent,
@@ -4184,7 +4324,11 @@ function renderLoopTemplate(id, values) {
4184
4324
  projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
4185
4325
  provider: values.provider,
4186
4326
  authProfile: values.authProfile,
4327
+ authProfilePool: listVar(values.authProfilePool),
4328
+ workerAuthProfile: values.workerAuthProfile,
4329
+ verifierAuthProfile: values.verifierAuthProfile,
4187
4330
  account: values.account ? { profile: values.account, tool: values.accountTool } : undefined,
4331
+ accountPool: accountPoolVar(values.accountPool, values.accountTool),
4188
4332
  model: values.model,
4189
4333
  variant: values.variant,
4190
4334
  agent: values.agent,
@@ -4194,6 +4338,13 @@ function renderLoopTemplate(id, values) {
4194
4338
  }
4195
4339
  throw new Error(`unknown template: ${id}`);
4196
4340
  }
4341
+ function listVar(value) {
4342
+ const values = value?.split(",").map((entry) => entry.trim()).filter(Boolean);
4343
+ return values?.length ? values : undefined;
4344
+ }
4345
+ function accountPoolVar(value, tool) {
4346
+ return listVar(value)?.map((profile) => ({ profile, tool }));
4347
+ }
4197
4348
  // src/lib/doctor.ts
4198
4349
  import { spawnSync as spawnSync3 } from "child_process";
4199
4350
  import { accessSync as accessSync2, constants as constants2 } from "fs";
@@ -4269,7 +4420,8 @@ function daemonStatus(store, path = pidFilePath()) {
4269
4420
  active: store.countLoops("active"),
4270
4421
  paused: store.countLoops("paused"),
4271
4422
  stopped: store.countLoops("stopped"),
4272
- expired: store.countLoops("expired")
4423
+ expired: store.countLoops("expired"),
4424
+ archived: store.countLoops(undefined, { archived: true })
4273
4425
  },
4274
4426
  runs: {
4275
4427
  total: store.countRuns(),
@@ -74,9 +74,13 @@ export declare class Store {
74
74
  listLoops(opts?: {
75
75
  status?: LoopStatus;
76
76
  limit?: number;
77
+ archived?: boolean;
78
+ includeArchived?: boolean;
77
79
  }): Loop[];
78
80
  dueLoops(now: Date): Loop[];
79
81
  updateLoop(id: string, patch: Partial<Pick<Loop, "status" | "nextRunAt" | "retryScheduledFor" | "expiresAt">>, opts?: DaemonLeaseFence): Loop;
82
+ archiveLoop(idOrName: string): Loop;
83
+ unarchiveLoop(idOrName: string): Loop;
80
84
  deleteLoop(idOrName: string): boolean;
81
85
  createWorkflow(input: CreateWorkflowInput): WorkflowSpec;
82
86
  getWorkflow(id: string): WorkflowSpec | undefined;
@@ -159,7 +163,10 @@ export declare class Store {
159
163
  }): LoopRun[];
160
164
  recoverExpiredRunLeases(now?: Date, opts?: DaemonLeaseFence): LoopRun[];
161
165
  expireLoops(now?: Date, opts?: DaemonLeaseFence): Loop[];
162
- countLoops(status?: LoopStatus): number;
166
+ countLoops(status?: LoopStatus, opts?: {
167
+ archived?: boolean;
168
+ includeArchived?: boolean;
169
+ }): number;
163
170
  countRuns(status?: RunStatus): number;
164
171
  acquireDaemonLease(input: {
165
172
  id: string;