@hasna/loops 0.3.54 → 0.3.55
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 +32 -4
- package/dist/cli/index.js +357 -65
- package/dist/daemon/index.js +165 -18
- package/dist/index.js +226 -55
- package/dist/lib/store.d.ts +16 -1
- package/dist/lib/store.js +147 -3
- package/dist/lib/templates.d.ts +4 -2
- package/dist/sdk/index.js +164 -17
- package/dist/types.d.ts +5 -4
- package/docs/USAGE.md +32 -4
- package/docs/workflows/transcript-feedback-to-loops.json +2 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -329,6 +329,15 @@ function optionalPositiveInteger(value, label) {
|
|
|
329
329
|
throw new Error(`${label} must be a positive integer`);
|
|
330
330
|
return value;
|
|
331
331
|
}
|
|
332
|
+
function optionalTimeoutMs(value, label) {
|
|
333
|
+
if (value === undefined)
|
|
334
|
+
return;
|
|
335
|
+
if (value === null)
|
|
336
|
+
return null;
|
|
337
|
+
if (!Number.isInteger(value) || value <= 0)
|
|
338
|
+
throw new Error(`${label} must be a positive integer or null for unlimited`);
|
|
339
|
+
return value;
|
|
340
|
+
}
|
|
332
341
|
function optionalBoolean(value, label) {
|
|
333
342
|
if (value === undefined)
|
|
334
343
|
return;
|
|
@@ -417,7 +426,7 @@ function validateTarget(value, label, opts) {
|
|
|
417
426
|
assertObject(value, label);
|
|
418
427
|
if (value.type === "command") {
|
|
419
428
|
assertString(value.command, `${label}.command`);
|
|
420
|
-
|
|
429
|
+
optionalTimeoutMs(value.timeoutMs, `${label}.timeoutMs`);
|
|
421
430
|
optionalPositiveInteger(value.idleTimeoutMs, `${label}.idleTimeoutMs`);
|
|
422
431
|
if (value.shell !== true && /\s/.test(value.command.trim())) {
|
|
423
432
|
throw new Error(`${label}.command must be an executable without spaces when shell is false; put flags in args or set shell true`);
|
|
@@ -436,7 +445,7 @@ function validateTarget(value, label, opts) {
|
|
|
436
445
|
const providers = ["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"];
|
|
437
446
|
if (!providers.includes(value.provider))
|
|
438
447
|
throw new Error(`${label}.provider must be one of ${providers.join(", ")}`);
|
|
439
|
-
|
|
448
|
+
optionalTimeoutMs(value.timeoutMs, `${label}.timeoutMs`);
|
|
440
449
|
optionalPositiveInteger(value.idleTimeoutMs, `${label}.idleTimeoutMs`);
|
|
441
450
|
if (value.authProfile !== undefined) {
|
|
442
451
|
assertString(value.authProfile, `${label}.authProfile`);
|
|
@@ -559,7 +568,7 @@ function normalizeCreateWorkflowInput(input, opts = {}) {
|
|
|
559
568
|
target: validateTarget(step.target, `workflow.steps[${index}].target`, opts),
|
|
560
569
|
dependsOn: optionalStringArray(step.dependsOn, `workflow.steps[${index}].dependsOn`) ?? [],
|
|
561
570
|
continueOnFailure: optionalBoolean(step.continueOnFailure, `workflow.steps[${index}].continueOnFailure`) ?? false,
|
|
562
|
-
timeoutMs:
|
|
571
|
+
timeoutMs: optionalTimeoutMs(step.timeoutMs, `workflow.steps[${index}].timeoutMs`),
|
|
563
572
|
account: optionalAccountRef(step.account, `workflow.steps[${index}].account`)
|
|
564
573
|
};
|
|
565
574
|
});
|
|
@@ -1305,6 +1314,17 @@ class Store {
|
|
|
1305
1314
|
const row = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 1").get(name);
|
|
1306
1315
|
return row ? rowToLoop(row) : undefined;
|
|
1307
1316
|
}
|
|
1317
|
+
requireUniqueLoop(idOrName) {
|
|
1318
|
+
const byId = this.getLoop(idOrName);
|
|
1319
|
+
if (byId)
|
|
1320
|
+
return byId;
|
|
1321
|
+
const rows = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 2").all(idOrName);
|
|
1322
|
+
if (rows.length === 0)
|
|
1323
|
+
throw new Error(`loop not found: ${idOrName}`);
|
|
1324
|
+
if (rows.length > 1)
|
|
1325
|
+
throw new Error(`ambiguous loop name: ${idOrName}; use a loop id`);
|
|
1326
|
+
return rowToLoop(rows[0]);
|
|
1327
|
+
}
|
|
1308
1328
|
requireLoop(idOrName) {
|
|
1309
1329
|
return this.getLoop(idOrName) ?? this.findLoopByName(idOrName) ?? (() => {
|
|
1310
1330
|
throw new Error(`loop not found: ${idOrName}`);
|
|
@@ -1379,6 +1399,130 @@ class Store {
|
|
|
1379
1399
|
throw new Error(`loop not found after update: ${id}`);
|
|
1380
1400
|
return after;
|
|
1381
1401
|
}
|
|
1402
|
+
activeLoopReferenceCount(workflowId) {
|
|
1403
|
+
const rows = this.db.query("SELECT target_json FROM loops WHERE archived_at IS NULL").all();
|
|
1404
|
+
let count = 0;
|
|
1405
|
+
for (const row of rows) {
|
|
1406
|
+
try {
|
|
1407
|
+
const target = JSON.parse(row.target_json);
|
|
1408
|
+
if (target.type === "workflow" && target.workflowId === workflowId)
|
|
1409
|
+
count += 1;
|
|
1410
|
+
} catch {}
|
|
1411
|
+
}
|
|
1412
|
+
return count;
|
|
1413
|
+
}
|
|
1414
|
+
archiveWorkflowIfUnreferenced(workflowId, updated) {
|
|
1415
|
+
if (this.activeLoopReferenceCount(workflowId) > 0)
|
|
1416
|
+
return;
|
|
1417
|
+
const workflow = this.getWorkflow(workflowId);
|
|
1418
|
+
if (!workflow || workflow.status !== "active")
|
|
1419
|
+
return;
|
|
1420
|
+
const res = this.db.query("UPDATE workflow_specs SET status='archived', updated_at=? WHERE id=? AND status='active'").run(updated, workflowId);
|
|
1421
|
+
if (res.changes !== 1)
|
|
1422
|
+
return;
|
|
1423
|
+
return this.getWorkflow(workflowId);
|
|
1424
|
+
}
|
|
1425
|
+
retargetWorkflowLoop(idOrName, workflowId, opts = {}) {
|
|
1426
|
+
const updated = (opts.now ?? new Date).toISOString();
|
|
1427
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1428
|
+
try {
|
|
1429
|
+
const current = this.requireLoop(idOrName);
|
|
1430
|
+
if (current.target.type !== "workflow")
|
|
1431
|
+
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1432
|
+
if (this.hasRunningRun(current.id))
|
|
1433
|
+
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1434
|
+
const workflow = this.requireWorkflow(workflowId);
|
|
1435
|
+
const target = { ...current.target, workflowId: workflow.id };
|
|
1436
|
+
if (opts.workflowTimeoutMs !== undefined)
|
|
1437
|
+
target.timeoutMs = opts.workflowTimeoutMs;
|
|
1438
|
+
const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
|
|
1439
|
+
WHERE id=$id
|
|
1440
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1441
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1442
|
+
))`).run({
|
|
1443
|
+
$id: current.id,
|
|
1444
|
+
$target: JSON.stringify(target),
|
|
1445
|
+
$updated: updated,
|
|
1446
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1447
|
+
$now: updated
|
|
1448
|
+
});
|
|
1449
|
+
if (res.changes !== 1)
|
|
1450
|
+
throw new Error("daemon lease lost");
|
|
1451
|
+
this.db.exec("COMMIT");
|
|
1452
|
+
const after = this.getLoop(current.id);
|
|
1453
|
+
if (!after)
|
|
1454
|
+
throw new Error(`loop not found after retarget: ${current.id}`);
|
|
1455
|
+
return after;
|
|
1456
|
+
} catch (error) {
|
|
1457
|
+
try {
|
|
1458
|
+
this.db.exec("ROLLBACK");
|
|
1459
|
+
} catch {}
|
|
1460
|
+
throw error;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
createAndRetargetWorkflowLoop(idOrName, workflowInput, opts = {}) {
|
|
1464
|
+
const normalized = normalizeCreateWorkflowInput(workflowInput);
|
|
1465
|
+
const updated = (opts.now ?? new Date).toISOString();
|
|
1466
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1467
|
+
try {
|
|
1468
|
+
const current = this.requireUniqueLoop(idOrName);
|
|
1469
|
+
if (current.target.type !== "workflow")
|
|
1470
|
+
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1471
|
+
if (this.hasRunningRun(current.id))
|
|
1472
|
+
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1473
|
+
const previousWorkflow = this.requireWorkflow(current.target.workflowId);
|
|
1474
|
+
const workflow = {
|
|
1475
|
+
id: genId(),
|
|
1476
|
+
name: normalized.name,
|
|
1477
|
+
description: normalized.description,
|
|
1478
|
+
version: normalized.version ?? 1,
|
|
1479
|
+
status: "active",
|
|
1480
|
+
goal: normalized.goal,
|
|
1481
|
+
steps: normalized.steps,
|
|
1482
|
+
createdAt: updated,
|
|
1483
|
+
updatedAt: updated
|
|
1484
|
+
};
|
|
1485
|
+
this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
|
|
1486
|
+
VALUES ($id, $name, $description, $version, $status, $goal, $steps, $created, $updated)`).run({
|
|
1487
|
+
$id: workflow.id,
|
|
1488
|
+
$name: workflow.name,
|
|
1489
|
+
$description: workflow.description ?? null,
|
|
1490
|
+
$version: workflow.version,
|
|
1491
|
+
$status: workflow.status,
|
|
1492
|
+
$goal: workflow.goal ? JSON.stringify(workflow.goal) : null,
|
|
1493
|
+
$steps: JSON.stringify(workflow.steps),
|
|
1494
|
+
$created: workflow.createdAt,
|
|
1495
|
+
$updated: workflow.updatedAt
|
|
1496
|
+
});
|
|
1497
|
+
const target = { ...current.target, workflowId: workflow.id };
|
|
1498
|
+
if (opts.workflowTimeoutMs !== undefined)
|
|
1499
|
+
target.timeoutMs = opts.workflowTimeoutMs;
|
|
1500
|
+
const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
|
|
1501
|
+
WHERE id=$id
|
|
1502
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1503
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1504
|
+
))`).run({
|
|
1505
|
+
$id: current.id,
|
|
1506
|
+
$target: JSON.stringify(target),
|
|
1507
|
+
$updated: updated,
|
|
1508
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1509
|
+
$now: updated
|
|
1510
|
+
});
|
|
1511
|
+
if (res.changes !== 1)
|
|
1512
|
+
throw new Error("daemon lease lost");
|
|
1513
|
+
const archivedOld = opts.archiveOld ? this.archiveWorkflowIfUnreferenced(previousWorkflow.id, updated) : undefined;
|
|
1514
|
+
this.db.exec("COMMIT");
|
|
1515
|
+
const loop = this.getLoop(current.id);
|
|
1516
|
+
if (!loop)
|
|
1517
|
+
throw new Error(`loop not found after retarget: ${current.id}`);
|
|
1518
|
+
return { loop, workflow, previousWorkflow, archivedOld };
|
|
1519
|
+
} catch (error) {
|
|
1520
|
+
try {
|
|
1521
|
+
this.db.exec("ROLLBACK");
|
|
1522
|
+
} catch {}
|
|
1523
|
+
throw error;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1382
1526
|
renameLoop(id, name, opts = {}) {
|
|
1383
1527
|
const current = this.getLoop(id);
|
|
1384
1528
|
if (!current)
|
|
@@ -3480,7 +3624,7 @@ function commandSpec(target) {
|
|
|
3480
3624
|
cwd: commandTarget.cwd,
|
|
3481
3625
|
shell: commandTarget.shell,
|
|
3482
3626
|
env: commandTarget.env,
|
|
3483
|
-
timeoutMs: commandTarget.timeoutMs
|
|
3627
|
+
timeoutMs: commandTarget.timeoutMs === undefined ? DEFAULT_TIMEOUT_MS : commandTarget.timeoutMs,
|
|
3484
3628
|
idleTimeoutMs: commandTarget.idleTimeoutMs,
|
|
3485
3629
|
account: commandTarget.account,
|
|
3486
3630
|
accountTool: commandTarget.account?.tool
|
|
@@ -3491,7 +3635,7 @@ function commandSpec(target) {
|
|
|
3491
3635
|
command: providerCommand(agentTarget.provider),
|
|
3492
3636
|
args: agentArgs(agentTarget),
|
|
3493
3637
|
cwd: agentTarget.cwd,
|
|
3494
|
-
timeoutMs: agentTarget.timeoutMs ??
|
|
3638
|
+
timeoutMs: agentTarget.timeoutMs ?? null,
|
|
3495
3639
|
idleTimeoutMs: agentTarget.idleTimeoutMs,
|
|
3496
3640
|
account: agentTarget.account,
|
|
3497
3641
|
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
|
|
@@ -3679,12 +3823,12 @@ async function executeRemoteSpec(spec, machine, metadata, opts) {
|
|
|
3679
3823
|
if (opts.signal?.aborted)
|
|
3680
3824
|
abortHandler();
|
|
3681
3825
|
opts.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
3682
|
-
const timer = setTimeout(() => {
|
|
3826
|
+
const timer = typeof spec.timeoutMs === "number" ? setTimeout(() => {
|
|
3683
3827
|
timedOut = true;
|
|
3684
3828
|
if (child.pid)
|
|
3685
3829
|
killProcessGroup(child.pid);
|
|
3686
|
-
}, spec.timeoutMs);
|
|
3687
|
-
timer
|
|
3830
|
+
}, spec.timeoutMs) : undefined;
|
|
3831
|
+
timer?.unref();
|
|
3688
3832
|
let idleTimer;
|
|
3689
3833
|
const resetIdleTimer = () => {
|
|
3690
3834
|
if (!spec.idleTimeoutMs)
|
|
@@ -3716,7 +3860,8 @@ async function executeRemoteSpec(spec, machine, metadata, opts) {
|
|
|
3716
3860
|
} catch (err) {
|
|
3717
3861
|
error = err instanceof Error ? err.message : String(err);
|
|
3718
3862
|
} finally {
|
|
3719
|
-
|
|
3863
|
+
if (timer)
|
|
3864
|
+
clearTimeout(timer);
|
|
3720
3865
|
if (idleTimer)
|
|
3721
3866
|
clearTimeout(idleTimer);
|
|
3722
3867
|
opts.signal?.removeEventListener("abort", abortHandler);
|
|
@@ -3858,12 +4003,12 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
3858
4003
|
if (opts.signal?.aborted)
|
|
3859
4004
|
abortHandler();
|
|
3860
4005
|
opts.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
3861
|
-
const timer = setTimeout(() => {
|
|
4006
|
+
const timer = typeof spec.timeoutMs === "number" ? setTimeout(() => {
|
|
3862
4007
|
timedOut = true;
|
|
3863
4008
|
if (child.pid)
|
|
3864
4009
|
killProcessGroup(child.pid);
|
|
3865
|
-
}, spec.timeoutMs);
|
|
3866
|
-
timer
|
|
4010
|
+
}, spec.timeoutMs) : undefined;
|
|
4011
|
+
timer?.unref();
|
|
3867
4012
|
let idleTimer;
|
|
3868
4013
|
const resetIdleTimer = () => {
|
|
3869
4014
|
if (!spec.idleTimeoutMs)
|
|
@@ -3895,7 +4040,8 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
3895
4040
|
} catch (err) {
|
|
3896
4041
|
error = err instanceof Error ? err.message : String(err);
|
|
3897
4042
|
} finally {
|
|
3898
|
-
|
|
4043
|
+
if (timer)
|
|
4044
|
+
clearTimeout(timer);
|
|
3899
4045
|
if (idleTimer)
|
|
3900
4046
|
clearTimeout(idleTimer);
|
|
3901
4047
|
opts.signal?.removeEventListener("abort", abortHandler);
|
|
@@ -4320,7 +4466,7 @@ ${result.stderr}`);
|
|
|
4320
4466
|
// src/lib/workflow-runner.ts
|
|
4321
4467
|
function targetWithStepAccount(step) {
|
|
4322
4468
|
const account = step.account ?? step.target.account;
|
|
4323
|
-
const timeoutMs = step.timeoutMs
|
|
4469
|
+
const timeoutMs = step.timeoutMs !== undefined ? step.timeoutMs : step.target.timeoutMs;
|
|
4324
4470
|
if (!account && timeoutMs === step.target.timeoutMs)
|
|
4325
4471
|
return step.target;
|
|
4326
4472
|
return { ...step.target, account, timeoutMs };
|
|
@@ -4616,7 +4762,8 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4616
4762
|
})
|
|
4617
4763
|
});
|
|
4618
4764
|
}
|
|
4619
|
-
const
|
|
4765
|
+
const workflowTimeoutMs = typeof loop.target.timeoutMs === "number" ? loop.target.timeoutMs : undefined;
|
|
4766
|
+
const controller = workflowTimeoutMs !== undefined ? new AbortController : undefined;
|
|
4620
4767
|
let workflowTimedOut = false;
|
|
4621
4768
|
const externalAbort = () => controller?.abort();
|
|
4622
4769
|
if (controller && opts.signal?.aborted)
|
|
@@ -4626,13 +4773,13 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4626
4773
|
const timer = controller ? setTimeout(() => {
|
|
4627
4774
|
workflowTimedOut = true;
|
|
4628
4775
|
controller.abort();
|
|
4629
|
-
},
|
|
4776
|
+
}, workflowTimeoutMs) : undefined;
|
|
4630
4777
|
timer?.unref();
|
|
4631
4778
|
try {
|
|
4632
4779
|
return await executeWorkflow(store, workflow, {
|
|
4633
4780
|
...opts,
|
|
4634
4781
|
signal: controller?.signal ?? opts.signal,
|
|
4635
|
-
signalTimeoutMessage: () => workflowTimedOut && loop.target.type === "workflow" ? `workflow timed out after ${
|
|
4782
|
+
signalTimeoutMessage: () => workflowTimedOut && loop.target.type === "workflow" ? `workflow timed out after ${workflowTimeoutMs}ms` : undefined,
|
|
4636
4783
|
loop,
|
|
4637
4784
|
loopRun: run,
|
|
4638
4785
|
scheduledFor: run.scheduledFor,
|
|
@@ -5123,7 +5270,8 @@ var TEMPLATE_SUMMARIES = [
|
|
|
5123
5270
|
{ name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
|
|
5124
5271
|
{ name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
|
|
5125
5272
|
{ name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
|
|
5126
|
-
{ name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated task/event worktree branches." }
|
|
5273
|
+
{ name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated task/event worktree branches." },
|
|
5274
|
+
{ name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." }
|
|
5127
5275
|
]
|
|
5128
5276
|
},
|
|
5129
5277
|
{
|
|
@@ -5153,7 +5301,8 @@ var TEMPLATE_SUMMARIES = [
|
|
|
5153
5301
|
{ name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
|
|
5154
5302
|
{ name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
|
|
5155
5303
|
{ name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
|
|
5156
|
-
{ name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated event worktree branches." }
|
|
5304
|
+
{ name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated event worktree branches." },
|
|
5305
|
+
{ name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." }
|
|
5157
5306
|
]
|
|
5158
5307
|
},
|
|
5159
5308
|
{
|
|
@@ -5181,7 +5330,7 @@ var TEMPLATE_SUMMARIES = [
|
|
|
5181
5330
|
{ name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
|
|
5182
5331
|
{ name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
|
|
5183
5332
|
{ name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated bounded-agent worktree branches." },
|
|
5184
|
-
{ name: "timeoutMs", default: "
|
|
5333
|
+
{ name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." }
|
|
5185
5334
|
]
|
|
5186
5335
|
},
|
|
5187
5336
|
{
|
|
@@ -5200,7 +5349,8 @@ var TEMPLATE_SUMMARIES = [
|
|
|
5200
5349
|
{ name: "accountPool", description: "Comma-separated OpenAccounts profiles for non-Codewith providers." },
|
|
5201
5350
|
{ name: "provider", default: "codewith", description: "Agent provider." },
|
|
5202
5351
|
{ name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
|
|
5203
|
-
{ name: "worktreeMode", default: "required", description: "Worktree isolation mode." }
|
|
5352
|
+
{ name: "worktreeMode", default: "required", description: "Worktree isolation mode." },
|
|
5353
|
+
{ name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." }
|
|
5204
5354
|
]
|
|
5205
5355
|
},
|
|
5206
5356
|
{
|
|
@@ -5305,6 +5455,30 @@ function taskLabel(input) {
|
|
|
5305
5455
|
const head = input.taskTitle?.trim() || input.taskId;
|
|
5306
5456
|
return head.length > 160 ? `${head.slice(0, 157)}...` : head;
|
|
5307
5457
|
}
|
|
5458
|
+
var UNLIMITED_AGENT_TIMEOUT_MS = null;
|
|
5459
|
+
function agentTimeoutMs(input) {
|
|
5460
|
+
return input.timeoutMs === undefined ? UNLIMITED_AGENT_TIMEOUT_MS : input.timeoutMs;
|
|
5461
|
+
}
|
|
5462
|
+
function parseTemplateTimeoutMs(raw) {
|
|
5463
|
+
if (raw === undefined || raw.trim() === "")
|
|
5464
|
+
return;
|
|
5465
|
+
const normalized = raw.trim().toLowerCase();
|
|
5466
|
+
if (["unlimited", "none", "null", "never", "off", "false"].includes(normalized))
|
|
5467
|
+
return null;
|
|
5468
|
+
const value = Number(raw);
|
|
5469
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
5470
|
+
throw new Error("timeoutMs must be a positive integer number of milliseconds, or unlimited/none/null");
|
|
5471
|
+
}
|
|
5472
|
+
return value;
|
|
5473
|
+
}
|
|
5474
|
+
function parseDeterministicTimeoutMs(raw, fallbackMs, label = "timeoutMs") {
|
|
5475
|
+
if (raw === undefined || raw.trim() === "")
|
|
5476
|
+
return fallbackMs;
|
|
5477
|
+
const value = Number(raw);
|
|
5478
|
+
if (!Number.isInteger(value) || value <= 0)
|
|
5479
|
+
throw new Error(`${label} must be a positive integer number of milliseconds`);
|
|
5480
|
+
return value;
|
|
5481
|
+
}
|
|
5308
5482
|
function stableIndex(seed, size) {
|
|
5309
5483
|
let hash = 2166136261;
|
|
5310
5484
|
for (let i = 0;i < seed.length; i += 1) {
|
|
@@ -5576,7 +5750,7 @@ function agentTarget(input, prompt, role, seed, plan) {
|
|
|
5576
5750
|
...input.projectGroup ? { projectGroup: input.projectGroup } : {}
|
|
5577
5751
|
},
|
|
5578
5752
|
account: accountForRole(input, role, seed),
|
|
5579
|
-
timeoutMs:
|
|
5753
|
+
timeoutMs: agentTimeoutMs(input)
|
|
5580
5754
|
};
|
|
5581
5755
|
}
|
|
5582
5756
|
function workflowStepsWithWorktree(plan, steps) {
|
|
@@ -6019,18 +6193,15 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
6019
6193
|
name: "Worker",
|
|
6020
6194
|
description: "Implement the todos task and record evidence.",
|
|
6021
6195
|
target: agentTarget(input, workerPrompt, "worker", input.taskId, plan),
|
|
6022
|
-
timeoutMs:
|
|
6196
|
+
timeoutMs: agentTimeoutMs(input)
|
|
6023
6197
|
},
|
|
6024
6198
|
{
|
|
6025
6199
|
id: "verifier",
|
|
6026
6200
|
name: "Verifier",
|
|
6027
6201
|
description: "Adversarially verify worker output and update todos.",
|
|
6028
6202
|
dependsOn: ["worker"],
|
|
6029
|
-
target:
|
|
6030
|
-
|
|
6031
|
-
idleTimeoutMs: 10 * 60000
|
|
6032
|
-
},
|
|
6033
|
-
timeoutMs: 30 * 60000
|
|
6203
|
+
target: agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
|
|
6204
|
+
timeoutMs: agentTimeoutMs(input)
|
|
6034
6205
|
}
|
|
6035
6206
|
])
|
|
6036
6207
|
};
|
|
@@ -6191,7 +6362,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
6191
6362
|
name: "Triage",
|
|
6192
6363
|
description: "Check task eligibility, duplicates, dependencies, and automation gates.",
|
|
6193
6364
|
target: agentTarget(input, triagePrompt, "triage", input.taskId, plan),
|
|
6194
|
-
timeoutMs:
|
|
6365
|
+
timeoutMs: agentTimeoutMs(input)
|
|
6195
6366
|
},
|
|
6196
6367
|
{
|
|
6197
6368
|
id: "triage-gate",
|
|
@@ -6213,7 +6384,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
6213
6384
|
description: "Create a concise implementation plan and split unsafe scope before work starts.",
|
|
6214
6385
|
dependsOn: ["triage-gate"],
|
|
6215
6386
|
target: agentTarget(input, plannerPrompt, "planner", input.taskId, plan),
|
|
6216
|
-
timeoutMs:
|
|
6387
|
+
timeoutMs: agentTimeoutMs(input)
|
|
6217
6388
|
},
|
|
6218
6389
|
{
|
|
6219
6390
|
id: "planner-gate",
|
|
@@ -6235,18 +6406,15 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
6235
6406
|
description: "Implement the todos task according to triage and planner evidence.",
|
|
6236
6407
|
dependsOn: ["planner-gate"],
|
|
6237
6408
|
target: agentTarget(input, workerPrompt, "worker", input.taskId, plan),
|
|
6238
|
-
timeoutMs:
|
|
6409
|
+
timeoutMs: agentTimeoutMs(input)
|
|
6239
6410
|
},
|
|
6240
6411
|
{
|
|
6241
6412
|
id: "verifier",
|
|
6242
6413
|
name: "Verifier",
|
|
6243
6414
|
description: "Adversarially verify worker output and update todos.",
|
|
6244
6415
|
dependsOn: ["worker"],
|
|
6245
|
-
target:
|
|
6246
|
-
|
|
6247
|
-
idleTimeoutMs: 10 * 60000
|
|
6248
|
-
},
|
|
6249
|
-
timeoutMs: 30 * 60000
|
|
6416
|
+
target: agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
|
|
6417
|
+
timeoutMs: agentTimeoutMs(input)
|
|
6250
6418
|
}
|
|
6251
6419
|
])
|
|
6252
6420
|
};
|
|
@@ -6316,18 +6484,15 @@ function renderEventWorkerVerifierWorkflow(input) {
|
|
|
6316
6484
|
name: "Worker",
|
|
6317
6485
|
description: "Handle the Hasna event and record evidence.",
|
|
6318
6486
|
target: agentTarget(input, workerPrompt, "worker", seed, plan),
|
|
6319
|
-
timeoutMs:
|
|
6487
|
+
timeoutMs: agentTimeoutMs(input)
|
|
6320
6488
|
},
|
|
6321
6489
|
{
|
|
6322
6490
|
id: "verifier",
|
|
6323
6491
|
name: "Verifier",
|
|
6324
6492
|
description: "Adversarially verify event handling.",
|
|
6325
6493
|
dependsOn: ["worker"],
|
|
6326
|
-
target:
|
|
6327
|
-
|
|
6328
|
-
idleTimeoutMs: 10 * 60000
|
|
6329
|
-
},
|
|
6330
|
-
timeoutMs: 30 * 60000
|
|
6494
|
+
target: agentTarget(input, verifierPrompt, "verifier", seed, plan),
|
|
6495
|
+
timeoutMs: agentTimeoutMs(input)
|
|
6331
6496
|
}
|
|
6332
6497
|
])
|
|
6333
6498
|
};
|
|
@@ -6339,7 +6504,7 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
|
|
|
6339
6504
|
throw new Error("projectPath is required");
|
|
6340
6505
|
const seed = `${input.projectPath}:${input.objective}`;
|
|
6341
6506
|
const plan = worktreePlan(input, seed);
|
|
6342
|
-
const timeoutMs =
|
|
6507
|
+
const timeoutMs = agentTimeoutMs(input);
|
|
6343
6508
|
const workerPrompt = [
|
|
6344
6509
|
`/goal ${input.objective}`,
|
|
6345
6510
|
"",
|
|
@@ -6377,11 +6542,8 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
|
|
|
6377
6542
|
name: "Verifier",
|
|
6378
6543
|
description: "Adversarially verify the bounded objective result.",
|
|
6379
6544
|
dependsOn: ["worker"],
|
|
6380
|
-
target:
|
|
6381
|
-
|
|
6382
|
-
idleTimeoutMs: 10 * 60000
|
|
6383
|
-
},
|
|
6384
|
-
timeoutMs: Math.min(timeoutMs, 30 * 60000)
|
|
6545
|
+
target: agentTarget(input, verifierPrompt, "verifier", seed, plan),
|
|
6546
|
+
timeoutMs
|
|
6385
6547
|
}
|
|
6386
6548
|
])
|
|
6387
6549
|
};
|
|
@@ -6410,7 +6572,7 @@ function renderLifecycleBoundedTemplate(id, values) {
|
|
|
6410
6572
|
worktreeMode: values.worktreeMode ?? (id === REPORT_ONLY_TEMPLATE_ID ? "main" : "required"),
|
|
6411
6573
|
worktreeRoot: values.worktreeRoot,
|
|
6412
6574
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
6413
|
-
timeoutMs:
|
|
6575
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
|
|
6414
6576
|
};
|
|
6415
6577
|
if (id === TASK_LIFECYCLE_TEMPLATE_ID) {
|
|
6416
6578
|
const taskId = values.taskId ?? "";
|
|
@@ -6447,6 +6609,7 @@ function renderLifecycleBoundedTemplate(id, values) {
|
|
|
6447
6609
|
worktreeMode: values.worktreeMode ?? "required",
|
|
6448
6610
|
worktreeRoot: values.worktreeRoot,
|
|
6449
6611
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
6612
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
|
|
6450
6613
|
eventId: values.eventId,
|
|
6451
6614
|
eventType: values.eventType
|
|
6452
6615
|
});
|
|
@@ -6513,6 +6676,8 @@ function renderDeterministicCheckCreateTaskWorkflow(values) {
|
|
|
6513
6676
|
if (!checkCommand.trim())
|
|
6514
6677
|
throw new Error("checkCommand is required");
|
|
6515
6678
|
const seed = `${projectPath}:${checkCommand}`;
|
|
6679
|
+
const timeoutMs = parseDeterministicTimeoutMs(values.timeoutMs, 5 * 60000);
|
|
6680
|
+
const idleTimeoutMs = parseDeterministicTimeoutMs(values.idleTimeoutMs, 60000, "idleTimeoutMs");
|
|
6516
6681
|
return {
|
|
6517
6682
|
name: values.name ?? `deterministic-check-${stableIndex(seed, 4294967295).toString(16).padStart(8, "0")}`,
|
|
6518
6683
|
description: values.description ?? "Deterministic check that writes compact evidence and upserts one deduped todos task when the expectation is not met.",
|
|
@@ -6527,10 +6692,10 @@ function renderDeterministicCheckCreateTaskWorkflow(values) {
|
|
|
6527
6692
|
command: "bash",
|
|
6528
6693
|
args: ["-lc", checkCommand],
|
|
6529
6694
|
cwd: projectPath,
|
|
6530
|
-
timeoutMs
|
|
6531
|
-
idleTimeoutMs
|
|
6695
|
+
timeoutMs,
|
|
6696
|
+
idleTimeoutMs
|
|
6532
6697
|
},
|
|
6533
|
-
timeoutMs
|
|
6698
|
+
timeoutMs
|
|
6534
6699
|
}
|
|
6535
6700
|
]
|
|
6536
6701
|
};
|
|
@@ -6568,6 +6733,7 @@ function renderBuiltinLoopTemplate(id, values) {
|
|
|
6568
6733
|
worktreeMode: values.worktreeMode,
|
|
6569
6734
|
worktreeRoot: values.worktreeRoot,
|
|
6570
6735
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
6736
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
|
|
6571
6737
|
eventId: values.eventId,
|
|
6572
6738
|
eventType: values.eventType
|
|
6573
6739
|
});
|
|
@@ -6599,7 +6765,8 @@ function renderBuiltinLoopTemplate(id, values) {
|
|
|
6599
6765
|
manualBreakGlass: booleanVar(values.manualBreakGlass),
|
|
6600
6766
|
worktreeMode: values.worktreeMode,
|
|
6601
6767
|
worktreeRoot: values.worktreeRoot,
|
|
6602
|
-
worktreeBranchPrefix: values.worktreeBranchPrefix
|
|
6768
|
+
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
6769
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
|
|
6603
6770
|
});
|
|
6604
6771
|
}
|
|
6605
6772
|
if (id === BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID) {
|
|
@@ -6627,7 +6794,7 @@ function renderBuiltinLoopTemplate(id, values) {
|
|
|
6627
6794
|
worktreeMode: values.worktreeMode,
|
|
6628
6795
|
worktreeRoot: values.worktreeRoot,
|
|
6629
6796
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
6630
|
-
timeoutMs:
|
|
6797
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
|
|
6631
6798
|
});
|
|
6632
6799
|
}
|
|
6633
6800
|
throw new Error(`unknown template: ${id}`);
|
|
@@ -6862,7 +7029,11 @@ function runDoctor(store) {
|
|
|
6862
7029
|
if (loop.target.type === "workflow") {
|
|
6863
7030
|
const workflow = store.requireWorkflow(loop.target.workflowId);
|
|
6864
7031
|
for (const step of workflowExecutionOrder(workflow)) {
|
|
6865
|
-
preflightTarget({
|
|
7032
|
+
preflightTarget({
|
|
7033
|
+
...step.target,
|
|
7034
|
+
account: step.account ?? step.target.account,
|
|
7035
|
+
timeoutMs: step.timeoutMs !== undefined ? step.timeoutMs : step.target.timeoutMs
|
|
7036
|
+
}, { loopId: loop.id, loopName: loop.name, workflowId: workflow.id, workflowName: workflow.name, workflowStepId: step.id }, { machine: loop.machine });
|
|
6866
7037
|
}
|
|
6867
7038
|
} else {
|
|
6868
7039
|
preflightTarget(loop.target, { loopId: loop.id, loopName: loop.name }, { machine: loop.machine });
|
package/dist/lib/store.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CreateLoopInput, CreateWorkflowInvocationInput, CreateWorkflowInput, Goal, GoalAutoExecute, GoalPlanNode, GoalRun, GoalStatus, Loop, LoopRun, LoopStatus, RunStatus, WorkflowEvent, WorkflowInvocation, WorkflowRun, WorkflowRunStatus, WorkflowSpec, WorkflowStepRun, WorkflowWorkItem, WorkflowWorkItemStatus, UpsertWorkflowWorkItemInput } from "../types.js";
|
|
1
|
+
import type { CreateLoopInput, CreateWorkflowInvocationInput, CreateWorkflowInput, Goal, GoalAutoExecute, GoalPlanNode, GoalRun, GoalStatus, Loop, LoopRun, LoopStatus, RunStatus, TimeoutMs, WorkflowEvent, WorkflowInvocation, WorkflowRun, WorkflowRunStatus, WorkflowSpec, WorkflowStepRun, WorkflowWorkItem, WorkflowWorkItemStatus, UpsertWorkflowWorkItemInput } from "../types.js";
|
|
2
2
|
interface DaemonLeaseFence {
|
|
3
3
|
daemonLeaseId?: string;
|
|
4
4
|
now?: Date;
|
|
@@ -74,6 +74,7 @@ export declare class Store {
|
|
|
74
74
|
createLoop(input: CreateLoopInput, from?: Date): Loop;
|
|
75
75
|
getLoop(id: string): Loop | undefined;
|
|
76
76
|
findLoopByName(name: string): Loop | undefined;
|
|
77
|
+
requireUniqueLoop(idOrName: string): Loop;
|
|
77
78
|
requireLoop(idOrName: string): Loop;
|
|
78
79
|
listLoops(opts?: {
|
|
79
80
|
status?: LoopStatus;
|
|
@@ -83,6 +84,20 @@ export declare class Store {
|
|
|
83
84
|
}): Loop[];
|
|
84
85
|
dueLoops(now: Date, limit?: number): Loop[];
|
|
85
86
|
updateLoop(id: string, patch: Partial<Pick<Loop, "status" | "nextRunAt" | "retryScheduledFor" | "expiresAt">>, opts?: DaemonLeaseFence): Loop;
|
|
87
|
+
private activeLoopReferenceCount;
|
|
88
|
+
private archiveWorkflowIfUnreferenced;
|
|
89
|
+
retargetWorkflowLoop(idOrName: string, workflowId: string, opts?: DaemonLeaseFence & {
|
|
90
|
+
workflowTimeoutMs?: TimeoutMs;
|
|
91
|
+
}): Loop;
|
|
92
|
+
createAndRetargetWorkflowLoop(idOrName: string, workflowInput: CreateWorkflowInput, opts?: DaemonLeaseFence & {
|
|
93
|
+
workflowTimeoutMs?: TimeoutMs;
|
|
94
|
+
archiveOld?: boolean;
|
|
95
|
+
}): {
|
|
96
|
+
loop: Loop;
|
|
97
|
+
workflow: WorkflowSpec;
|
|
98
|
+
previousWorkflow: WorkflowSpec;
|
|
99
|
+
archivedOld?: WorkflowSpec;
|
|
100
|
+
};
|
|
86
101
|
renameLoop(id: string, name: string, opts?: DaemonLeaseFence): Loop;
|
|
87
102
|
archiveLoop(idOrName: string): Loop;
|
|
88
103
|
unarchiveLoop(idOrName: string): Loop;
|