@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/daemon/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|
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
|
-
|
|
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);
|
|
@@ -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
|
|
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
|
|
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
|
|
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(),
|
package/dist/lib/store.d.ts
CHANGED
|
@@ -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
|
|
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;
|