@hasna/loops 0.3.14 → 0.3.16
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 +590 -26
- package/dist/daemon/control.d.ts +1 -0
- package/dist/daemon/index.js +128 -9
- package/dist/index.d.ts +1 -0
- package/dist/index.js +413 -15
- package/dist/lib/health.d.ts +70 -0
- package/dist/lib/store.d.ts +8 -1
- package/dist/lib/store.js +102 -5
- package/dist/lib/templates.d.ts +12 -0
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.js +139 -7
- package/dist/types.d.ts +8 -0
- package/docs/USAGE.md +66 -6
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -326,6 +326,17 @@ function optionalPositiveInteger(value, label) {
|
|
|
326
326
|
throw new Error(`${label} must be a positive integer`);
|
|
327
327
|
return value;
|
|
328
328
|
}
|
|
329
|
+
function optionalStringArray(value, label) {
|
|
330
|
+
if (value === undefined)
|
|
331
|
+
return;
|
|
332
|
+
if (!Array.isArray(value))
|
|
333
|
+
throw new Error(`${label} must be an array`);
|
|
334
|
+
const values = value.map((entry, index) => {
|
|
335
|
+
assertString(entry, `${label}[${index}]`);
|
|
336
|
+
return entry.trim();
|
|
337
|
+
}).filter(Boolean);
|
|
338
|
+
return values.length ? values : undefined;
|
|
339
|
+
}
|
|
329
340
|
function normalizeGoalSpec(value, label = "goal") {
|
|
330
341
|
if (value === undefined)
|
|
331
342
|
return;
|
|
@@ -397,6 +408,14 @@ function validateTarget(value, label) {
|
|
|
397
408
|
throw new Error(`${label}.sandbox is currently supported only for provider codewith, codex, or cursor`);
|
|
398
409
|
}
|
|
399
410
|
}
|
|
411
|
+
if (value.allowlist !== undefined) {
|
|
412
|
+
assertObject(value.allowlist, `${label}.allowlist`);
|
|
413
|
+
optionalStringArray(value.allowlist.tools, `${label}.allowlist.tools`);
|
|
414
|
+
optionalStringArray(value.allowlist.commands, `${label}.allowlist.commands`);
|
|
415
|
+
if (value.allowlist.enforcement !== undefined && value.allowlist.enforcement !== "metadata_only") {
|
|
416
|
+
throw new Error(`${label}.allowlist.enforcement must be metadata_only`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
400
419
|
return value;
|
|
401
420
|
}
|
|
402
421
|
throw new Error(`${label}.type must be command or agent`);
|
|
@@ -480,6 +499,8 @@ function rowToLoop(row) {
|
|
|
480
499
|
name: row.name,
|
|
481
500
|
description: row.description ?? undefined,
|
|
482
501
|
status: row.status,
|
|
502
|
+
archivedAt: row.archived_at ?? undefined,
|
|
503
|
+
archivedFromStatus: row.archived_from_status ? row.archived_from_status : undefined,
|
|
483
504
|
schedule: JSON.parse(row.schedule_json),
|
|
484
505
|
target: JSON.parse(row.target_json),
|
|
485
506
|
goal: row.goal_json ? JSON.parse(row.goal_json) : undefined,
|
|
@@ -689,6 +710,8 @@ class Store {
|
|
|
689
710
|
name TEXT NOT NULL,
|
|
690
711
|
description TEXT,
|
|
691
712
|
status TEXT NOT NULL,
|
|
713
|
+
archived_at TEXT,
|
|
714
|
+
archived_from_status TEXT,
|
|
692
715
|
schedule_json TEXT NOT NULL,
|
|
693
716
|
target_json TEXT NOT NULL,
|
|
694
717
|
goal_json TEXT,
|
|
@@ -889,6 +912,8 @@ class Store {
|
|
|
889
912
|
`);
|
|
890
913
|
this.addColumnIfMissing("loops", "machine_json", "TEXT");
|
|
891
914
|
this.addColumnIfMissing("loops", "goal_json", "TEXT");
|
|
915
|
+
this.addColumnIfMissing("loops", "archived_at", "TEXT");
|
|
916
|
+
this.addColumnIfMissing("loops", "archived_from_status", "TEXT");
|
|
892
917
|
this.addColumnIfMissing("loop_runs", "goal_run_id", "TEXT");
|
|
893
918
|
this.addColumnIfMissing("workflow_specs", "goal_json", "TEXT");
|
|
894
919
|
this.addColumnIfMissing("workflow_runs", "goal_run_id", "TEXT");
|
|
@@ -897,6 +922,7 @@ class Store {
|
|
|
897
922
|
this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0001_initial_and_workflows", nowIso());
|
|
898
923
|
this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0002_loop_machines", nowIso());
|
|
899
924
|
this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0003_goals", nowIso());
|
|
925
|
+
this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0004_loop_archive_metadata", nowIso());
|
|
900
926
|
}
|
|
901
927
|
addColumnIfMissing(table, column, definition) {
|
|
902
928
|
const columns = this.db.query(`PRAGMA table_info(${table})`).all();
|
|
@@ -973,12 +999,26 @@ class Store {
|
|
|
973
999
|
}
|
|
974
1000
|
listLoops(opts = {}) {
|
|
975
1001
|
const limit = opts.limit ?? 200;
|
|
976
|
-
|
|
1002
|
+
let rows;
|
|
1003
|
+
if (opts.status && opts.archived) {
|
|
1004
|
+
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);
|
|
1005
|
+
} else if (opts.status && opts.includeArchived) {
|
|
1006
|
+
rows = this.db.query("SELECT * FROM loops WHERE status = ? ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit);
|
|
1007
|
+
} else if (opts.status) {
|
|
1008
|
+
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);
|
|
1009
|
+
} else if (opts.archived) {
|
|
1010
|
+
rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NOT NULL ORDER BY archived_at DESC LIMIT ?").all(limit);
|
|
1011
|
+
} else if (opts.includeArchived) {
|
|
1012
|
+
rows = this.db.query("SELECT * FROM loops ORDER BY status ASC, next_run_at ASC LIMIT ?").all(limit);
|
|
1013
|
+
} else {
|
|
1014
|
+
rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NULL ORDER BY status ASC, next_run_at ASC LIMIT ?").all(limit);
|
|
1015
|
+
}
|
|
977
1016
|
return rows.map(rowToLoop);
|
|
978
1017
|
}
|
|
979
1018
|
dueLoops(now) {
|
|
980
1019
|
const rows = this.db.query(`SELECT * FROM loops
|
|
981
1020
|
WHERE status = 'active'
|
|
1021
|
+
AND archived_at IS NULL
|
|
982
1022
|
AND next_run_at IS NOT NULL
|
|
983
1023
|
AND next_run_at <= ?
|
|
984
1024
|
ORDER BY next_run_at ASC`).all(now.toISOString());
|
|
@@ -1010,6 +1050,44 @@ class Store {
|
|
|
1010
1050
|
throw new Error(`loop not found after update: ${id}`);
|
|
1011
1051
|
return after;
|
|
1012
1052
|
}
|
|
1053
|
+
archiveLoop(idOrName) {
|
|
1054
|
+
const loop = this.requireLoop(idOrName);
|
|
1055
|
+
if (loop.archivedAt)
|
|
1056
|
+
return loop;
|
|
1057
|
+
const updated = nowIso();
|
|
1058
|
+
const archivedStatus = loop.status === "active" ? "paused" : loop.status;
|
|
1059
|
+
this.db.query(`UPDATE loops
|
|
1060
|
+
SET status=$status, archived_at=$archivedAt, archived_from_status=$archivedFromStatus, updated_at=$updated
|
|
1061
|
+
WHERE id=$id`).run({
|
|
1062
|
+
$id: loop.id,
|
|
1063
|
+
$status: archivedStatus,
|
|
1064
|
+
$archivedAt: updated,
|
|
1065
|
+
$archivedFromStatus: loop.status,
|
|
1066
|
+
$updated: updated
|
|
1067
|
+
});
|
|
1068
|
+
const archived = this.getLoop(loop.id);
|
|
1069
|
+
if (!archived)
|
|
1070
|
+
throw new Error(`loop not found after archive: ${loop.id}`);
|
|
1071
|
+
return archived;
|
|
1072
|
+
}
|
|
1073
|
+
unarchiveLoop(idOrName) {
|
|
1074
|
+
const loop = this.requireLoop(idOrName);
|
|
1075
|
+
if (!loop.archivedAt)
|
|
1076
|
+
return loop;
|
|
1077
|
+
const updated = nowIso();
|
|
1078
|
+
const restoredStatus = loop.archivedFromStatus ?? loop.status;
|
|
1079
|
+
this.db.query(`UPDATE loops
|
|
1080
|
+
SET status=$status, archived_at=NULL, archived_from_status=NULL, updated_at=$updated
|
|
1081
|
+
WHERE id=$id`).run({
|
|
1082
|
+
$id: loop.id,
|
|
1083
|
+
$status: restoredStatus,
|
|
1084
|
+
$updated: updated
|
|
1085
|
+
});
|
|
1086
|
+
const unarchived = this.getLoop(loop.id);
|
|
1087
|
+
if (!unarchived)
|
|
1088
|
+
throw new Error(`loop not found after unarchive: ${loop.id}`);
|
|
1089
|
+
return unarchived;
|
|
1090
|
+
}
|
|
1013
1091
|
deleteLoop(idOrName) {
|
|
1014
1092
|
const loop = this.requireLoop(idOrName);
|
|
1015
1093
|
const res = this.db.query("DELETE FROM loops WHERE id = ?").run(loop.id);
|
|
@@ -1784,10 +1862,16 @@ class Store {
|
|
|
1784
1862
|
}
|
|
1785
1863
|
claimRun(loop, scheduledFor, runnerId, now = new Date, opts = {}) {
|
|
1786
1864
|
const startedAt = now.toISOString();
|
|
1787
|
-
const leaseExpiresAt = new Date(now.getTime() + loop.leaseMs).toISOString();
|
|
1788
1865
|
this.db.exec("BEGIN IMMEDIATE");
|
|
1789
1866
|
try {
|
|
1790
1867
|
this.assertDaemonLeaseFence(opts, startedAt);
|
|
1868
|
+
const currentLoop = this.getLoop(loop.id);
|
|
1869
|
+
if (!currentLoop || currentLoop.archivedAt) {
|
|
1870
|
+
this.db.exec("COMMIT");
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
loop = currentLoop;
|
|
1874
|
+
const leaseExpiresAt = new Date(now.getTime() + loop.leaseMs).toISOString();
|
|
1791
1875
|
const existing = this.getRunBySlot(loop.id, scheduledFor);
|
|
1792
1876
|
if (existing) {
|
|
1793
1877
|
if (existing.status === "running") {
|
|
@@ -2005,7 +2089,7 @@ class Store {
|
|
|
2005
2089
|
return recovered;
|
|
2006
2090
|
}
|
|
2007
2091
|
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());
|
|
2092
|
+
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
2093
|
const expired = [];
|
|
2010
2094
|
for (const row of rows) {
|
|
2011
2095
|
const updated = this.updateLoop(row.id, { status: "expired", nextRunAt: undefined }, opts);
|
|
@@ -2014,8 +2098,21 @@ class Store {
|
|
|
2014
2098
|
}
|
|
2015
2099
|
return expired;
|
|
2016
2100
|
}
|
|
2017
|
-
countLoops(status) {
|
|
2018
|
-
|
|
2101
|
+
countLoops(status, opts = {}) {
|
|
2102
|
+
let row;
|
|
2103
|
+
if (status && opts.archived) {
|
|
2104
|
+
row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE status = ? AND archived_at IS NOT NULL").get(status);
|
|
2105
|
+
} else if (status && opts.includeArchived) {
|
|
2106
|
+
row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE status = ?").get(status);
|
|
2107
|
+
} else if (status) {
|
|
2108
|
+
row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE status = ? AND archived_at IS NULL").get(status);
|
|
2109
|
+
} else if (opts.archived) {
|
|
2110
|
+
row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE archived_at IS NOT NULL").get();
|
|
2111
|
+
} else if (opts.includeArchived) {
|
|
2112
|
+
row = this.db.query("SELECT COUNT(*) AS count FROM loops").get();
|
|
2113
|
+
} else {
|
|
2114
|
+
row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE archived_at IS NULL").get();
|
|
2115
|
+
}
|
|
2019
2116
|
return row?.count ?? 0;
|
|
2020
2117
|
}
|
|
2021
2118
|
countRuns(status) {
|
|
@@ -2377,6 +2474,16 @@ function metadataEnv(metadata) {
|
|
|
2377
2474
|
env.LOOPS_GOAL_NODE_KEY = metadata.goalNodeKey;
|
|
2378
2475
|
return env;
|
|
2379
2476
|
}
|
|
2477
|
+
function allowlistEnv(allowlist) {
|
|
2478
|
+
const env = {};
|
|
2479
|
+
if (allowlist?.tools?.length)
|
|
2480
|
+
env.LOOPS_AGENT_ALLOWED_TOOLS = allowlist.tools.join(",");
|
|
2481
|
+
if (allowlist?.commands?.length)
|
|
2482
|
+
env.LOOPS_AGENT_ALLOWED_COMMANDS = allowlist.commands.join(",");
|
|
2483
|
+
if (allowlist?.tools?.length || allowlist?.commands?.length)
|
|
2484
|
+
env.LOOPS_AGENT_ALLOWLIST_ENFORCEMENT = "metadata_only";
|
|
2485
|
+
return env;
|
|
2486
|
+
}
|
|
2380
2487
|
function providerCommand(provider) {
|
|
2381
2488
|
switch (provider) {
|
|
2382
2489
|
case "claude":
|
|
@@ -2584,7 +2691,8 @@ function commandSpec(target) {
|
|
|
2584
2691
|
account: agentTarget.account,
|
|
2585
2692
|
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
|
|
2586
2693
|
preflightAnyOf: agentTarget.provider === "cursor" ? ["cursor", "agent"] : undefined,
|
|
2587
|
-
stdin: agentTarget.prompt
|
|
2694
|
+
stdin: agentTarget.prompt,
|
|
2695
|
+
allowlist: agentTarget.allowlist
|
|
2588
2696
|
};
|
|
2589
2697
|
}
|
|
2590
2698
|
function executionEnv(spec, metadata, opts) {
|
|
@@ -2596,6 +2704,7 @@ function executionEnv(spec, metadata, opts) {
|
|
|
2596
2704
|
Object.assign(env, accountEnv);
|
|
2597
2705
|
}
|
|
2598
2706
|
Object.assign(env, spec.env ?? {});
|
|
2707
|
+
Object.assign(env, allowlistEnv(spec.allowlist));
|
|
2599
2708
|
env.PATH = normalizeExecutionPath(env);
|
|
2600
2709
|
Object.assign(env, metadataEnv(metadata));
|
|
2601
2710
|
return env;
|
|
@@ -2634,6 +2743,9 @@ function remoteBootstrapLines(spec, metadata) {
|
|
|
2634
2743
|
continue;
|
|
2635
2744
|
lines.push(`export ${key}=${shellQuote(value)}`);
|
|
2636
2745
|
}
|
|
2746
|
+
for (const [key, value] of Object.entries(allowlistEnv(spec.allowlist))) {
|
|
2747
|
+
lines.push(`export ${key}=${shellQuote(value)}`);
|
|
2748
|
+
}
|
|
2637
2749
|
return lines;
|
|
2638
2750
|
}
|
|
2639
2751
|
function remoteScript(spec, metadata) {
|
|
@@ -3587,12 +3699,16 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
3587
3699
|
|
|
3588
3700
|
// src/lib/scheduler.ts
|
|
3589
3701
|
function manualRunScheduledFor(loop, now = new Date) {
|
|
3702
|
+
if (loop.archivedAt)
|
|
3703
|
+
return now.toISOString();
|
|
3590
3704
|
if (loop.status === "active" && loop.nextRunAt && new Date(loop.nextRunAt).getTime() <= now.getTime()) {
|
|
3591
3705
|
return loop.retryScheduledFor ?? loop.nextRunAt;
|
|
3592
3706
|
}
|
|
3593
3707
|
return now.toISOString();
|
|
3594
3708
|
}
|
|
3595
3709
|
function shouldAdvanceManualRun(loop, scheduledFor, now = new Date) {
|
|
3710
|
+
if (loop.archivedAt)
|
|
3711
|
+
return false;
|
|
3596
3712
|
if (loop.status !== "active")
|
|
3597
3713
|
return false;
|
|
3598
3714
|
if (!loop.nextRunAt || new Date(loop.nextRunAt).getTime() > now.getTime())
|
|
@@ -3600,6 +3716,8 @@ function shouldAdvanceManualRun(loop, scheduledFor, now = new Date) {
|
|
|
3600
3716
|
return scheduledFor === (loop.retryScheduledFor ?? loop.nextRunAt);
|
|
3601
3717
|
}
|
|
3602
3718
|
function manualRunSource(loop, scheduledFor, now = new Date) {
|
|
3719
|
+
if (loop.archivedAt)
|
|
3720
|
+
return "ad_hoc";
|
|
3603
3721
|
if (loop.status !== "active")
|
|
3604
3722
|
return "ad_hoc";
|
|
3605
3723
|
if (!loop.nextRunAt || new Date(loop.nextRunAt).getTime() > now.getTime())
|
|
@@ -3618,7 +3736,7 @@ function advanceLoop(store, loop, run, finishedAt, succeeded, opts = {}) {
|
|
|
3618
3736
|
if (run.status === "running")
|
|
3619
3737
|
return;
|
|
3620
3738
|
const current = store.getLoop(loop.id);
|
|
3621
|
-
if (!current || current.status !== "active")
|
|
3739
|
+
if (!current || current.status !== "active" || current.archivedAt)
|
|
3622
3740
|
return;
|
|
3623
3741
|
if (current.retryScheduledFor && current.retryScheduledFor !== run.scheduledFor)
|
|
3624
3742
|
return;
|
|
@@ -3903,16 +4021,28 @@ class LoopsClient {
|
|
|
3903
4021
|
}
|
|
3904
4022
|
pause(idOrName) {
|
|
3905
4023
|
const loop = this.get(idOrName);
|
|
4024
|
+
if (loop.archivedAt)
|
|
4025
|
+
throw new Error(`loop is archived; unarchive it before pausing: ${idOrName}`);
|
|
3906
4026
|
return this.store.updateLoop(loop.id, { status: "paused" });
|
|
3907
4027
|
}
|
|
3908
4028
|
resume(idOrName) {
|
|
3909
4029
|
const loop = this.get(idOrName);
|
|
4030
|
+
if (loop.archivedAt)
|
|
4031
|
+
throw new Error(`loop is archived; unarchive it before resuming: ${idOrName}`);
|
|
3910
4032
|
return this.store.updateLoop(loop.id, { status: "active" });
|
|
3911
4033
|
}
|
|
3912
4034
|
stop(idOrName) {
|
|
3913
4035
|
const loop = this.get(idOrName);
|
|
4036
|
+
if (loop.archivedAt)
|
|
4037
|
+
throw new Error(`loop is archived; unarchive it before stopping: ${idOrName}`);
|
|
3914
4038
|
return this.store.updateLoop(loop.id, { status: "stopped", nextRunAt: undefined });
|
|
3915
4039
|
}
|
|
4040
|
+
archive(idOrName) {
|
|
4041
|
+
return this.store.archiveLoop(idOrName);
|
|
4042
|
+
}
|
|
4043
|
+
unarchive(idOrName) {
|
|
4044
|
+
return this.store.unarchiveLoop(idOrName);
|
|
4045
|
+
}
|
|
3916
4046
|
delete(idOrName) {
|
|
3917
4047
|
return this.store.deleteLoop(idOrName);
|
|
3918
4048
|
}
|
|
@@ -3931,6 +4061,8 @@ class LoopsClient {
|
|
|
3931
4061
|
}
|
|
3932
4062
|
async runNow(idOrName) {
|
|
3933
4063
|
const loop = this.get(idOrName);
|
|
4064
|
+
if (loop.archivedAt)
|
|
4065
|
+
throw new Error(`loop is archived; unarchive it before running: ${idOrName}`);
|
|
3934
4066
|
const now = new Date;
|
|
3935
4067
|
let scheduledFor = manualRunScheduledFor(loop, now);
|
|
3936
4068
|
let shouldAdvance = shouldAdvanceManualRun(loop, scheduledFor, now);
|
|
@@ -3974,6 +4106,10 @@ var TEMPLATE_SUMMARIES = [
|
|
|
3974
4106
|
{ name: "projectPath", required: true, description: "Repository or project working directory." },
|
|
3975
4107
|
{ name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
|
|
3976
4108
|
{ name: "authProfile", description: "Provider-native auth profile, currently Codewith." },
|
|
4109
|
+
{ name: "authProfilePool", description: "Comma-separated provider-native auth profiles; worker/verifier are selected deterministically." },
|
|
4110
|
+
{ name: "workerAuthProfile", description: "Provider-native auth profile for the worker step." },
|
|
4111
|
+
{ name: "verifierAuthProfile", description: "Provider-native auth profile for the verifier step." },
|
|
4112
|
+
{ name: "accountPool", description: "Comma-separated OpenAccounts profiles; worker/verifier are selected deterministically." },
|
|
3977
4113
|
{ name: "model", description: "Provider model." },
|
|
3978
4114
|
{ name: "variant", description: "Provider reasoning/model effort variant." },
|
|
3979
4115
|
{ name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
|
|
@@ -3993,6 +4129,10 @@ var TEMPLATE_SUMMARIES = [
|
|
|
3993
4129
|
{ name: "projectPath", required: true, description: "Repository or project working directory." },
|
|
3994
4130
|
{ name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
|
|
3995
4131
|
{ name: "authProfile", description: "Provider-native auth profile, currently Codewith." },
|
|
4132
|
+
{ name: "authProfilePool", description: "Comma-separated provider-native auth profiles; worker/verifier are selected deterministically." },
|
|
4133
|
+
{ name: "workerAuthProfile", description: "Provider-native auth profile for the worker step." },
|
|
4134
|
+
{ name: "verifierAuthProfile", description: "Provider-native auth profile for the verifier step." },
|
|
4135
|
+
{ name: "accountPool", description: "Comma-separated OpenAccounts profiles; worker/verifier are selected deterministically." },
|
|
3996
4136
|
{ name: "model", description: "Provider model." },
|
|
3997
4137
|
{ name: "variant", description: "Provider reasoning/model effort variant." },
|
|
3998
4138
|
{ name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
|
|
@@ -4007,7 +4147,37 @@ function taskLabel(input) {
|
|
|
4007
4147
|
const head = input.taskTitle?.trim() || input.taskId;
|
|
4008
4148
|
return head.length > 160 ? `${head.slice(0, 157)}...` : head;
|
|
4009
4149
|
}
|
|
4010
|
-
function
|
|
4150
|
+
function stableIndex(seed, size) {
|
|
4151
|
+
let hash = 2166136261;
|
|
4152
|
+
for (let i = 0;i < seed.length; i += 1) {
|
|
4153
|
+
hash ^= seed.charCodeAt(i);
|
|
4154
|
+
hash = Math.imul(hash, 16777619);
|
|
4155
|
+
}
|
|
4156
|
+
return Math.abs(hash >>> 0) % size;
|
|
4157
|
+
}
|
|
4158
|
+
function rolePoolValue(pool, seed, role) {
|
|
4159
|
+
if (!pool?.length)
|
|
4160
|
+
return;
|
|
4161
|
+
const workerIndex = stableIndex(seed, pool.length);
|
|
4162
|
+
if (role === "worker" || pool.length === 1)
|
|
4163
|
+
return pool[workerIndex];
|
|
4164
|
+
return pool[(workerIndex + 1) % pool.length];
|
|
4165
|
+
}
|
|
4166
|
+
function authProfileForRole(input, role, seed) {
|
|
4167
|
+
if (role === "worker" && input.workerAuthProfile)
|
|
4168
|
+
return input.workerAuthProfile;
|
|
4169
|
+
if (role === "verifier" && input.verifierAuthProfile)
|
|
4170
|
+
return input.verifierAuthProfile;
|
|
4171
|
+
return rolePoolValue(input.authProfilePool, seed, role) ?? input.authProfile;
|
|
4172
|
+
}
|
|
4173
|
+
function accountForRole(input, role, seed) {
|
|
4174
|
+
if (role === "worker" && input.workerAccount)
|
|
4175
|
+
return input.workerAccount;
|
|
4176
|
+
if (role === "verifier" && input.verifierAccount)
|
|
4177
|
+
return input.verifierAccount;
|
|
4178
|
+
return rolePoolValue(input.accountPool, seed, role) ?? input.account;
|
|
4179
|
+
}
|
|
4180
|
+
function agentTarget(input, prompt, role, seed) {
|
|
4011
4181
|
const provider = input.provider ?? "codewith";
|
|
4012
4182
|
const sandbox = input.sandbox ?? (provider === "codewith" || provider === "codex" ? "danger-full-access" : provider === "cursor" ? "disabled" : undefined);
|
|
4013
4183
|
return {
|
|
@@ -4018,11 +4188,11 @@ function agentTarget(input, prompt) {
|
|
|
4018
4188
|
model: input.model,
|
|
4019
4189
|
variant: input.variant,
|
|
4020
4190
|
agent: input.agent,
|
|
4021
|
-
authProfile: provider === "codewith" ? input
|
|
4191
|
+
authProfile: provider === "codewith" ? authProfileForRole(input, role, seed) : undefined,
|
|
4022
4192
|
configIsolation: "safe",
|
|
4023
4193
|
permissionMode: input.permissionMode ?? "bypass",
|
|
4024
4194
|
sandbox,
|
|
4025
|
-
account: input
|
|
4195
|
+
account: accountForRole(input, role, seed),
|
|
4026
4196
|
timeoutMs: 45 * 60000
|
|
4027
4197
|
};
|
|
4028
4198
|
}
|
|
@@ -4076,7 +4246,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
4076
4246
|
id: "worker",
|
|
4077
4247
|
name: "Worker",
|
|
4078
4248
|
description: "Implement the todos task and record evidence.",
|
|
4079
|
-
target: agentTarget(input, workerPrompt),
|
|
4249
|
+
target: agentTarget(input, workerPrompt, "worker", input.taskId),
|
|
4080
4250
|
timeoutMs: 45 * 60000
|
|
4081
4251
|
},
|
|
4082
4252
|
{
|
|
@@ -4084,7 +4254,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
4084
4254
|
name: "Verifier",
|
|
4085
4255
|
description: "Adversarially verify worker output and update todos.",
|
|
4086
4256
|
dependsOn: ["worker"],
|
|
4087
|
-
target: agentTarget(input, verifierPrompt),
|
|
4257
|
+
target: agentTarget(input, verifierPrompt, "verifier", input.taskId),
|
|
4088
4258
|
timeoutMs: 30 * 60000
|
|
4089
4259
|
}
|
|
4090
4260
|
]
|
|
@@ -4140,7 +4310,7 @@ function renderEventWorkerVerifierWorkflow(input) {
|
|
|
4140
4310
|
id: "worker",
|
|
4141
4311
|
name: "Worker",
|
|
4142
4312
|
description: "Handle the Hasna event and record evidence.",
|
|
4143
|
-
target: agentTarget(input, workerPrompt),
|
|
4313
|
+
target: agentTarget(input, workerPrompt, "worker", `${input.eventSource}:${input.eventType}:${input.eventId}`),
|
|
4144
4314
|
timeoutMs: 45 * 60000
|
|
4145
4315
|
},
|
|
4146
4316
|
{
|
|
@@ -4148,7 +4318,7 @@ function renderEventWorkerVerifierWorkflow(input) {
|
|
|
4148
4318
|
name: "Verifier",
|
|
4149
4319
|
description: "Adversarially verify event handling.",
|
|
4150
4320
|
dependsOn: ["worker"],
|
|
4151
|
-
target: agentTarget(input, verifierPrompt),
|
|
4321
|
+
target: agentTarget(input, verifierPrompt, "verifier", `${input.eventSource}:${input.eventType}:${input.eventId}`),
|
|
4152
4322
|
timeoutMs: 30 * 60000
|
|
4153
4323
|
}
|
|
4154
4324
|
]
|
|
@@ -4163,7 +4333,11 @@ function renderLoopTemplate(id, values) {
|
|
|
4163
4333
|
projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
|
|
4164
4334
|
provider: values.provider,
|
|
4165
4335
|
authProfile: values.authProfile,
|
|
4336
|
+
authProfilePool: listVar(values.authProfilePool),
|
|
4337
|
+
workerAuthProfile: values.workerAuthProfile,
|
|
4338
|
+
verifierAuthProfile: values.verifierAuthProfile,
|
|
4166
4339
|
account: values.account ? { profile: values.account, tool: values.accountTool } : undefined,
|
|
4340
|
+
accountPool: accountPoolVar(values.accountPool, values.accountTool),
|
|
4167
4341
|
model: values.model,
|
|
4168
4342
|
variant: values.variant,
|
|
4169
4343
|
agent: values.agent,
|
|
@@ -4184,7 +4358,11 @@ function renderLoopTemplate(id, values) {
|
|
|
4184
4358
|
projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
|
|
4185
4359
|
provider: values.provider,
|
|
4186
4360
|
authProfile: values.authProfile,
|
|
4361
|
+
authProfilePool: listVar(values.authProfilePool),
|
|
4362
|
+
workerAuthProfile: values.workerAuthProfile,
|
|
4363
|
+
verifierAuthProfile: values.verifierAuthProfile,
|
|
4187
4364
|
account: values.account ? { profile: values.account, tool: values.accountTool } : undefined,
|
|
4365
|
+
accountPool: accountPoolVar(values.accountPool, values.accountTool),
|
|
4188
4366
|
model: values.model,
|
|
4189
4367
|
variant: values.variant,
|
|
4190
4368
|
agent: values.agent,
|
|
@@ -4194,6 +4372,13 @@ function renderLoopTemplate(id, values) {
|
|
|
4194
4372
|
}
|
|
4195
4373
|
throw new Error(`unknown template: ${id}`);
|
|
4196
4374
|
}
|
|
4375
|
+
function listVar(value) {
|
|
4376
|
+
const values = value?.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
4377
|
+
return values?.length ? values : undefined;
|
|
4378
|
+
}
|
|
4379
|
+
function accountPoolVar(value, tool) {
|
|
4380
|
+
return listVar(value)?.map((profile) => ({ profile, tool }));
|
|
4381
|
+
}
|
|
4197
4382
|
// src/lib/doctor.ts
|
|
4198
4383
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
4199
4384
|
import { accessSync as accessSync2, constants as constants2 } from "fs";
|
|
@@ -4269,7 +4454,8 @@ function daemonStatus(store, path = pidFilePath()) {
|
|
|
4269
4454
|
active: store.countLoops("active"),
|
|
4270
4455
|
paused: store.countLoops("paused"),
|
|
4271
4456
|
stopped: store.countLoops("stopped"),
|
|
4272
|
-
expired: store.countLoops("expired")
|
|
4457
|
+
expired: store.countLoops("expired"),
|
|
4458
|
+
archived: store.countLoops(undefined, { archived: true })
|
|
4273
4459
|
},
|
|
4274
4460
|
runs: {
|
|
4275
4461
|
total: store.countRuns(),
|
|
@@ -4430,6 +4616,215 @@ function runDoctor(store) {
|
|
|
4430
4616
|
checks
|
|
4431
4617
|
};
|
|
4432
4618
|
}
|
|
4619
|
+
// src/lib/health.ts
|
|
4620
|
+
import { createHash } from "crypto";
|
|
4621
|
+
var EVIDENCE_CHARS = 2000;
|
|
4622
|
+
var CLASSIFICATIONS = [
|
|
4623
|
+
"rate_limit",
|
|
4624
|
+
"auth",
|
|
4625
|
+
"model_not_found",
|
|
4626
|
+
"context_length",
|
|
4627
|
+
"schema_response_format",
|
|
4628
|
+
"node_init",
|
|
4629
|
+
"timeout",
|
|
4630
|
+
"sigsegv",
|
|
4631
|
+
"skipped_previous_active",
|
|
4632
|
+
"unknown"
|
|
4633
|
+
];
|
|
4634
|
+
function bounded(value, limit = EVIDENCE_CHARS) {
|
|
4635
|
+
if (!value)
|
|
4636
|
+
return;
|
|
4637
|
+
if (value.length <= limit)
|
|
4638
|
+
return value;
|
|
4639
|
+
return `${value.slice(0, limit)}
|
|
4640
|
+
[truncated ${value.length - limit} chars]`;
|
|
4641
|
+
}
|
|
4642
|
+
function searchableText(run) {
|
|
4643
|
+
return [run.error, run.stderr, run.stdout].filter(Boolean).join(`
|
|
4644
|
+
`).toLowerCase();
|
|
4645
|
+
}
|
|
4646
|
+
function stableFingerprint(parts) {
|
|
4647
|
+
return createHash("sha256").update(parts.join(`
|
|
4648
|
+
`)).digest("hex").slice(0, 16);
|
|
4649
|
+
}
|
|
4650
|
+
function healthRun(run) {
|
|
4651
|
+
return {
|
|
4652
|
+
...run,
|
|
4653
|
+
error: bounded(run.error),
|
|
4654
|
+
stdout: bounded(run.stdout),
|
|
4655
|
+
stderr: bounded(run.stderr)
|
|
4656
|
+
};
|
|
4657
|
+
}
|
|
4658
|
+
function classifyRunFailure(run) {
|
|
4659
|
+
if (run.status === "succeeded" || run.status === "running")
|
|
4660
|
+
return;
|
|
4661
|
+
const text = searchableText(run);
|
|
4662
|
+
let classification = "unknown";
|
|
4663
|
+
if (run.status === "timed_out")
|
|
4664
|
+
classification = "timeout";
|
|
4665
|
+
else if (run.status === "skipped" && /previous run still active/.test(text))
|
|
4666
|
+
classification = "skipped_previous_active";
|
|
4667
|
+
else if (/rate limit|too many requests|429\b|quota exceeded/.test(text))
|
|
4668
|
+
classification = "rate_limit";
|
|
4669
|
+
else if (/unauthorized|authentication|auth\b|api key|invalid token|permission denied|401\b|403\b/.test(text))
|
|
4670
|
+
classification = "auth";
|
|
4671
|
+
else if (/model .*not found|model_not_found|unknown model|invalid model|404.*model/.test(text))
|
|
4672
|
+
classification = "model_not_found";
|
|
4673
|
+
else if (/context length|context_length|context window|maximum context|token limit|too many tokens/.test(text))
|
|
4674
|
+
classification = "context_length";
|
|
4675
|
+
else if (/response_format|json schema|schema validation|invalid schema|structured output/.test(text))
|
|
4676
|
+
classification = "schema_response_format";
|
|
4677
|
+
else if (/cannot find module|module not found|node:internal|bun: command not found|node: command not found|npm err!|err_module_not_found/.test(text))
|
|
4678
|
+
classification = "node_init";
|
|
4679
|
+
else if (/sigsegv|segmentation fault|signal 11/.test(text))
|
|
4680
|
+
classification = "sigsegv";
|
|
4681
|
+
return {
|
|
4682
|
+
classification,
|
|
4683
|
+
fingerprint: stableFingerprint([
|
|
4684
|
+
run.loopId,
|
|
4685
|
+
run.loopName,
|
|
4686
|
+
run.status,
|
|
4687
|
+
classification,
|
|
4688
|
+
String(run.exitCode ?? ""),
|
|
4689
|
+
(run.error ?? run.stderr ?? run.stdout ?? "").slice(0, 500)
|
|
4690
|
+
]),
|
|
4691
|
+
evidence: {
|
|
4692
|
+
error: bounded(run.error),
|
|
4693
|
+
stdout: bounded(run.stdout),
|
|
4694
|
+
stderr: bounded(run.stderr),
|
|
4695
|
+
exitCode: run.exitCode
|
|
4696
|
+
}
|
|
4697
|
+
};
|
|
4698
|
+
}
|
|
4699
|
+
function targetRoute(loop) {
|
|
4700
|
+
if (loop.target.type === "agent") {
|
|
4701
|
+
return {
|
|
4702
|
+
source: "openloops",
|
|
4703
|
+
kind: "loop_expectation",
|
|
4704
|
+
loopId: loop.id,
|
|
4705
|
+
loopName: loop.name,
|
|
4706
|
+
cwd: loop.target.cwd,
|
|
4707
|
+
provider: loop.target.provider
|
|
4708
|
+
};
|
|
4709
|
+
}
|
|
4710
|
+
if (loop.target.type === "command") {
|
|
4711
|
+
return {
|
|
4712
|
+
source: "openloops",
|
|
4713
|
+
kind: "loop_expectation",
|
|
4714
|
+
loopId: loop.id,
|
|
4715
|
+
loopName: loop.name,
|
|
4716
|
+
cwd: loop.target.cwd
|
|
4717
|
+
};
|
|
4718
|
+
}
|
|
4719
|
+
return {
|
|
4720
|
+
source: "openloops",
|
|
4721
|
+
kind: "loop_expectation",
|
|
4722
|
+
loopId: loop.id,
|
|
4723
|
+
loopName: loop.name
|
|
4724
|
+
};
|
|
4725
|
+
}
|
|
4726
|
+
function recommendedTask(loop, run, failure, route) {
|
|
4727
|
+
const title = `BUG: open-loops loop failure - ${loop.name}`;
|
|
4728
|
+
const description = [
|
|
4729
|
+
`OpenLoops expectation failed for loop ${loop.name} (${loop.id}).`,
|
|
4730
|
+
`Run: ${run.id}`,
|
|
4731
|
+
`Status: ${run.status}`,
|
|
4732
|
+
`Classification: ${failure.classification}`,
|
|
4733
|
+
`Fingerprint: ${failure.fingerprint}`,
|
|
4734
|
+
route.cwd ? `Route cwd: ${route.cwd}` : undefined,
|
|
4735
|
+
route.provider ? `Provider: ${route.provider}` : undefined,
|
|
4736
|
+
failure.evidence.error ? `Error:
|
|
4737
|
+
${failure.evidence.error}` : undefined,
|
|
4738
|
+
failure.evidence.stderr ? `Stderr:
|
|
4739
|
+
${failure.evidence.stderr}` : undefined
|
|
4740
|
+
].filter(Boolean).join(`
|
|
4741
|
+
|
|
4742
|
+
`);
|
|
4743
|
+
const dedupeKey = `openloops:${loop.id}:${failure.fingerprint}`;
|
|
4744
|
+
const tags = ["bug", "openloops", "loop-health", failure.classification];
|
|
4745
|
+
const priority = failure.classification === "auth" || failure.classification === "rate_limit" ? "high" : "medium";
|
|
4746
|
+
return {
|
|
4747
|
+
title,
|
|
4748
|
+
description,
|
|
4749
|
+
priority,
|
|
4750
|
+
tags,
|
|
4751
|
+
dedupeKey,
|
|
4752
|
+
search: { query: dedupeKey },
|
|
4753
|
+
compatibilityFallback: {
|
|
4754
|
+
search: ["todos", "search", dedupeKey, "--json"],
|
|
4755
|
+
add: ["todos", "add", title, "--description", description, "--tag", tags.join(","), "--priority", priority],
|
|
4756
|
+
comment: ["todos", "comment", "<task-id>", description]
|
|
4757
|
+
},
|
|
4758
|
+
futureNativeUpsert: {
|
|
4759
|
+
command: "todos upsert",
|
|
4760
|
+
fields: {
|
|
4761
|
+
title,
|
|
4762
|
+
description,
|
|
4763
|
+
priority,
|
|
4764
|
+
tags,
|
|
4765
|
+
dedupeKey,
|
|
4766
|
+
routeSource: route.source,
|
|
4767
|
+
routeKind: route.kind,
|
|
4768
|
+
routeLoopId: route.loopId,
|
|
4769
|
+
routeLoopName: route.loopName
|
|
4770
|
+
}
|
|
4771
|
+
}
|
|
4772
|
+
};
|
|
4773
|
+
}
|
|
4774
|
+
function expectationForLoop(store, loop) {
|
|
4775
|
+
const latestRun = store.listRuns({ loopId: loop.id, limit: 1 })[0];
|
|
4776
|
+
const route = targetRoute(loop);
|
|
4777
|
+
if (!latestRun) {
|
|
4778
|
+
return {
|
|
4779
|
+
loop: { id: loop.id, name: loop.name, status: loop.status, nextRunAt: loop.nextRunAt },
|
|
4780
|
+
ok: true,
|
|
4781
|
+
check: { id: "latest-run-succeeded", status: "warn", message: "loop has no recorded runs yet" },
|
|
4782
|
+
route
|
|
4783
|
+
};
|
|
4784
|
+
}
|
|
4785
|
+
if (latestRun.status === "succeeded") {
|
|
4786
|
+
return {
|
|
4787
|
+
loop: { id: loop.id, name: loop.name, status: loop.status, nextRunAt: loop.nextRunAt },
|
|
4788
|
+
ok: true,
|
|
4789
|
+
check: { id: "latest-run-succeeded", status: "pass", message: "latest run succeeded" },
|
|
4790
|
+
latestRun: healthRun(latestRun),
|
|
4791
|
+
route
|
|
4792
|
+
};
|
|
4793
|
+
}
|
|
4794
|
+
const failure = classifyRunFailure(latestRun);
|
|
4795
|
+
return {
|
|
4796
|
+
loop: { id: loop.id, name: loop.name, status: loop.status, nextRunAt: loop.nextRunAt },
|
|
4797
|
+
ok: false,
|
|
4798
|
+
check: { id: "latest-run-succeeded", status: "fail", message: `latest run is ${latestRun.status}` },
|
|
4799
|
+
latestRun: healthRun(latestRun),
|
|
4800
|
+
failure,
|
|
4801
|
+
route,
|
|
4802
|
+
recommendedTask: failure ? recommendedTask(loop, latestRun, failure, route) : undefined
|
|
4803
|
+
};
|
|
4804
|
+
}
|
|
4805
|
+
function buildHealthReport(store, opts = {}) {
|
|
4806
|
+
const loops2 = store.listLoops({ includeArchived: opts.includeArchived, limit: opts.limit ?? 200 });
|
|
4807
|
+
const expectations = loops2.map((loop) => expectationForLoop(store, loop));
|
|
4808
|
+
const classifications = Object.fromEntries(CLASSIFICATIONS.map((key) => [key, 0]));
|
|
4809
|
+
for (const expectation of expectations) {
|
|
4810
|
+
if (expectation.failure)
|
|
4811
|
+
classifications[expectation.failure.classification] += 1;
|
|
4812
|
+
}
|
|
4813
|
+
const unhealthy = expectations.filter((expectation) => !expectation.ok).length;
|
|
4814
|
+
const warnings = expectations.filter((expectation) => expectation.check.status === "warn").length;
|
|
4815
|
+
return {
|
|
4816
|
+
ok: unhealthy === 0,
|
|
4817
|
+
generatedAt: new Date().toISOString(),
|
|
4818
|
+
summary: {
|
|
4819
|
+
loops: expectations.length,
|
|
4820
|
+
healthy: expectations.length - unhealthy,
|
|
4821
|
+
unhealthy,
|
|
4822
|
+
warnings
|
|
4823
|
+
},
|
|
4824
|
+
classifications,
|
|
4825
|
+
expectations
|
|
4826
|
+
};
|
|
4827
|
+
}
|
|
4433
4828
|
export {
|
|
4434
4829
|
workflowExecutionOrder,
|
|
4435
4830
|
workflowBodyFromJson,
|
|
@@ -4455,11 +4850,14 @@ export {
|
|
|
4455
4850
|
isTerminal as isGoalTerminal,
|
|
4456
4851
|
initialNextRun,
|
|
4457
4852
|
getLoopTemplate,
|
|
4853
|
+
expectationForLoop,
|
|
4458
4854
|
executeWorkflow,
|
|
4459
4855
|
executeTarget,
|
|
4460
4856
|
executeLoopTarget,
|
|
4461
4857
|
executeLoop,
|
|
4462
4858
|
computeNextAfter,
|
|
4859
|
+
classifyRunFailure,
|
|
4860
|
+
buildHealthReport,
|
|
4463
4861
|
TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID,
|
|
4464
4862
|
Store,
|
|
4465
4863
|
LoopsClient,
|