@hasna/loops 0.3.14 → 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/README.md +20 -6
- package/dist/cli/index.js +208 -22
- package/dist/daemon/control.d.ts +1 -0
- package/dist/daemon/index.js +93 -8
- package/dist/index.js +166 -14
- package/dist/lib/store.d.ts +8 -1
- package/dist/lib/store.js +83 -5
- package/dist/lib/templates.d.ts +12 -0
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.js +104 -6
- package/dist/types.d.ts +2 -0
- package/docs/USAGE.md +20 -6
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/lib/templates.d.ts
CHANGED
|
@@ -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;
|
package/dist/sdk/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
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
|
|
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
|
-
|
|
220
|
-
|
|
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