@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.
package/dist/lib/store.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) {
@@ -8,7 +8,13 @@ export interface TodosTaskWorkflowTemplateInput {
8
8
  projectPath: string;
9
9
  provider?: AgentProvider;
10
10
  authProfile?: string;
11
+ authProfilePool?: string[];
12
+ workerAuthProfile?: string;
13
+ verifierAuthProfile?: string;
11
14
  account?: AccountRef;
15
+ accountPool?: AccountRef[];
16
+ workerAccount?: AccountRef;
17
+ verifierAccount?: AccountRef;
12
18
  model?: string;
13
19
  variant?: string;
14
20
  agent?: string;
@@ -27,7 +33,13 @@ export interface EventWorkflowTemplateInput {
27
33
  projectPath: string;
28
34
  provider?: AgentProvider;
29
35
  authProfile?: string;
36
+ authProfilePool?: string[];
37
+ workerAuthProfile?: string;
38
+ verifierAuthProfile?: string;
30
39
  account?: AccountRef;
40
+ accountPool?: AccountRef[];
41
+ workerAccount?: AccountRef;
42
+ verifierAccount?: AccountRef;
31
43
  model?: string;
32
44
  variant?: string;
33
45
  agent?: string;
@@ -17,6 +17,8 @@ export declare class LoopsClient {
17
17
  pause(idOrName: string): Loop;
18
18
  resume(idOrName: string): Loop;
19
19
  stop(idOrName: string): Loop;
20
+ archive(idOrName: string): Loop;
21
+ unarchive(idOrName: string): Loop;
20
22
  delete(idOrName: string): boolean;
21
23
  runs(loopId?: string): LoopRun[];
22
24
  goal(idOrName: string): {
package/dist/sdk/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);
package/dist/types.d.ts CHANGED
@@ -175,6 +175,8 @@ export interface Loop {
175
175
  name: string;
176
176
  description?: string;
177
177
  status: LoopStatus;
178
+ archivedAt?: string;
179
+ archivedFromStatus?: LoopStatus;
178
180
  schedule: ScheduleSpec;
179
181
  target: LoopTarget;
180
182
  goal?: GoalSpec;
package/docs/USAGE.md CHANGED
@@ -176,7 +176,7 @@ loops templates render todos-task-worker-verifier \
176
176
  --var taskTitle="Fix parser" \
177
177
  --var projectPath=/path/to/repo \
178
178
  --var provider=codewith \
179
- --var authProfile=account005 \
179
+ --var authProfilePool=account004,account005,account006 \
180
180
  --var sandbox=danger-full-access
181
181
  loops templates create-workflow todos-task-worker-verifier \
182
182
  --var taskId=<task-id> \
@@ -196,7 +196,7 @@ schedules a deduped one-shot workflow loop:
196
196
  ```bash
197
197
  cat task-created-event.json | loops events handle todos-task \
198
198
  --provider codewith \
199
- --auth-profile account005 \
199
+ --auth-profile-pool account004,account005,account006 \
200
200
  --permission-mode bypass \
201
201
  --sandbox danger-full-access
202
202
  ```
@@ -207,7 +207,7 @@ handler:
207
207
  ```bash
208
208
  cat event.json | loops events handle generic \
209
209
  --provider codewith \
210
- --auth-profile account005 \
210
+ --auth-profile-pool account004,account005,account006 \
211
211
  --permission-mode bypass \
212
212
  --sandbox danger-full-access \
213
213
  --project-path /path/to/repo
@@ -215,9 +215,11 @@ cat event.json | loops events handle generic \
215
215
 
216
216
  This is the intended deterministic-to-agentic path: a producer creates a todos
217
217
  task, `@hasna/events` delivers `task.created`, OpenLoops creates a worker and a
218
- verifier workflow, and the workflow updates todos with evidence. Use
219
- `--dry-run` to inspect the rendered workflow and loop input without storing
220
- anything.
218
+ verifier workflow, and the workflow updates todos with evidence. Use account
219
+ pools so worker and verifier steps do not burn the same profile; OpenLoops picks
220
+ deterministically and uses a different verifier profile when the pool has at
221
+ least two entries. Use `--dry-run` to inspect the rendered workflow and loop
222
+ input without storing anything.
221
223
 
222
224
  ## Transcript-Driven Loops
223
225
 
@@ -240,12 +242,24 @@ loops runs <id-or-name>
240
242
  loops pause <id-or-name>
241
243
  loops resume <id-or-name>
242
244
  loops stop <id-or-name>
245
+ loops archive <id-or-name>
246
+ loops unarchive <id-or-name>
243
247
  loops remove <id-or-name>
244
248
  loops run-now <id-or-name>
245
249
  ```
246
250
 
247
251
  Use `--json` for machine-readable output. Prompt bodies and run stdout/stderr are redacted by default in status output. `loops run-now` exits non-zero when the recorded run fails or times out.
248
252
 
253
+ Archive loops when retiring old automation but preserving history:
254
+
255
+ ```bash
256
+ loops archive <id-or-name>
257
+ loops list --archived
258
+ loops list --all
259
+ ```
260
+
261
+ Archived loops are hidden from the default `loops list`, excluded from daemon scheduling and doctor preflight, and cannot be run manually until restored with `loops unarchive`. `loops remove` deletes the loop record; prefer `archive` for superseded loops that may need audit history.
262
+
249
263
  `loops run-now` reports the manual run source:
250
264
 
251
265
  - `source=ad_hoc`: the loop was not due yet, so OpenLoops created a one-off manual slot. This is a single immediate attempt and does not schedule retries or consume the future scheduled slot.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/loops",
3
- "version": "0.3.13",
3
+ "version": "0.3.15",
4
4
  "description": "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -64,7 +64,7 @@
64
64
  "bun": ">=1.0.0"
65
65
  },
66
66
  "dependencies": {
67
- "@hasna/events": "^0.1.8",
67
+ "@hasna/events": "^0.1.9",
68
68
  "@hasna/machines": "0.0.49",
69
69
  "@openrouter/ai-sdk-provider": "2.9.1",
70
70
  "ai": "6.0.204",