@hasna/loops 0.3.54 → 0.3.56
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 +378 -66
- package/dist/daemon/index.js +165 -18
- package/dist/index.js +247 -56
- 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) {
|
|
@@ -5383,6 +5557,21 @@ function gitRootFor(path) {
|
|
|
5383
5557
|
return;
|
|
5384
5558
|
}
|
|
5385
5559
|
}
|
|
5560
|
+
function gitCommonDirFor(path) {
|
|
5561
|
+
if (!existsSync3(path))
|
|
5562
|
+
return;
|
|
5563
|
+
try {
|
|
5564
|
+
const raw = execFileSync("git", ["-C", path, "rev-parse", "--git-common-dir"], {
|
|
5565
|
+
encoding: "utf8",
|
|
5566
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
5567
|
+
}).trim();
|
|
5568
|
+
if (!raw)
|
|
5569
|
+
return;
|
|
5570
|
+
return isAbsolute2(raw) ? raw : resolve2(path, raw);
|
|
5571
|
+
} catch {
|
|
5572
|
+
return;
|
|
5573
|
+
}
|
|
5574
|
+
}
|
|
5386
5575
|
function prepareWorktreeCommand(plan) {
|
|
5387
5576
|
const repo = shellQuote2(plan.repoRoot);
|
|
5388
5577
|
const path = shellQuote2(plan.path);
|
|
@@ -5499,6 +5688,7 @@ function worktreePlan(input, seed) {
|
|
|
5499
5688
|
root,
|
|
5500
5689
|
path: worktreePath,
|
|
5501
5690
|
branch,
|
|
5691
|
+
gitMetadataDir: gitCommonDirFor(repoRoot),
|
|
5502
5692
|
prepareStep
|
|
5503
5693
|
};
|
|
5504
5694
|
}
|
|
@@ -5546,6 +5736,10 @@ function agentTarget(input, prompt, role, seed, plan) {
|
|
|
5546
5736
|
assertNativeAuthProfileSupport(input, provider);
|
|
5547
5737
|
const sandbox = input.sandbox ?? (provider === "codewith" || provider === "codex" ? "workspace-write" : provider === "cursor" ? "enabled" : undefined);
|
|
5548
5738
|
failClosedSandbox(input, provider, sandbox);
|
|
5739
|
+
const addDirs = [...input.addDirs ?? []];
|
|
5740
|
+
if (plan.enabled && plan.gitMetadataDir && (provider === "codewith" || provider === "codex") && sandbox === "workspace-write") {
|
|
5741
|
+
addDirs.push(plan.gitMetadataDir);
|
|
5742
|
+
}
|
|
5549
5743
|
return {
|
|
5550
5744
|
type: "agent",
|
|
5551
5745
|
provider,
|
|
@@ -5554,7 +5748,7 @@ function agentTarget(input, prompt, role, seed, plan) {
|
|
|
5554
5748
|
model: input.model,
|
|
5555
5749
|
variant: input.variant,
|
|
5556
5750
|
agent: input.agent,
|
|
5557
|
-
addDirs:
|
|
5751
|
+
addDirs: addDirs.length ? [...new Set(addDirs)] : undefined,
|
|
5558
5752
|
authProfile: provider === "codewith" ? authProfileForRole(input, role, seed) : undefined,
|
|
5559
5753
|
configIsolation: "safe",
|
|
5560
5754
|
permissionMode: input.permissionMode ?? "bypass",
|
|
@@ -5576,7 +5770,7 @@ function agentTarget(input, prompt, role, seed, plan) {
|
|
|
5576
5770
|
...input.projectGroup ? { projectGroup: input.projectGroup } : {}
|
|
5577
5771
|
},
|
|
5578
5772
|
account: accountForRole(input, role, seed),
|
|
5579
|
-
timeoutMs:
|
|
5773
|
+
timeoutMs: agentTimeoutMs(input)
|
|
5580
5774
|
};
|
|
5581
5775
|
}
|
|
5582
5776
|
function workflowStepsWithWorktree(plan, steps) {
|
|
@@ -6019,18 +6213,15 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
6019
6213
|
name: "Worker",
|
|
6020
6214
|
description: "Implement the todos task and record evidence.",
|
|
6021
6215
|
target: agentTarget(input, workerPrompt, "worker", input.taskId, plan),
|
|
6022
|
-
timeoutMs:
|
|
6216
|
+
timeoutMs: agentTimeoutMs(input)
|
|
6023
6217
|
},
|
|
6024
6218
|
{
|
|
6025
6219
|
id: "verifier",
|
|
6026
6220
|
name: "Verifier",
|
|
6027
6221
|
description: "Adversarially verify worker output and update todos.",
|
|
6028
6222
|
dependsOn: ["worker"],
|
|
6029
|
-
target:
|
|
6030
|
-
|
|
6031
|
-
idleTimeoutMs: 10 * 60000
|
|
6032
|
-
},
|
|
6033
|
-
timeoutMs: 30 * 60000
|
|
6223
|
+
target: agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
|
|
6224
|
+
timeoutMs: agentTimeoutMs(input)
|
|
6034
6225
|
}
|
|
6035
6226
|
])
|
|
6036
6227
|
};
|
|
@@ -6191,7 +6382,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
6191
6382
|
name: "Triage",
|
|
6192
6383
|
description: "Check task eligibility, duplicates, dependencies, and automation gates.",
|
|
6193
6384
|
target: agentTarget(input, triagePrompt, "triage", input.taskId, plan),
|
|
6194
|
-
timeoutMs:
|
|
6385
|
+
timeoutMs: agentTimeoutMs(input)
|
|
6195
6386
|
},
|
|
6196
6387
|
{
|
|
6197
6388
|
id: "triage-gate",
|
|
@@ -6213,7 +6404,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
6213
6404
|
description: "Create a concise implementation plan and split unsafe scope before work starts.",
|
|
6214
6405
|
dependsOn: ["triage-gate"],
|
|
6215
6406
|
target: agentTarget(input, plannerPrompt, "planner", input.taskId, plan),
|
|
6216
|
-
timeoutMs:
|
|
6407
|
+
timeoutMs: agentTimeoutMs(input)
|
|
6217
6408
|
},
|
|
6218
6409
|
{
|
|
6219
6410
|
id: "planner-gate",
|
|
@@ -6235,18 +6426,15 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
6235
6426
|
description: "Implement the todos task according to triage and planner evidence.",
|
|
6236
6427
|
dependsOn: ["planner-gate"],
|
|
6237
6428
|
target: agentTarget(input, workerPrompt, "worker", input.taskId, plan),
|
|
6238
|
-
timeoutMs:
|
|
6429
|
+
timeoutMs: agentTimeoutMs(input)
|
|
6239
6430
|
},
|
|
6240
6431
|
{
|
|
6241
6432
|
id: "verifier",
|
|
6242
6433
|
name: "Verifier",
|
|
6243
6434
|
description: "Adversarially verify worker output and update todos.",
|
|
6244
6435
|
dependsOn: ["worker"],
|
|
6245
|
-
target:
|
|
6246
|
-
|
|
6247
|
-
idleTimeoutMs: 10 * 60000
|
|
6248
|
-
},
|
|
6249
|
-
timeoutMs: 30 * 60000
|
|
6436
|
+
target: agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
|
|
6437
|
+
timeoutMs: agentTimeoutMs(input)
|
|
6250
6438
|
}
|
|
6251
6439
|
])
|
|
6252
6440
|
};
|
|
@@ -6316,18 +6504,15 @@ function renderEventWorkerVerifierWorkflow(input) {
|
|
|
6316
6504
|
name: "Worker",
|
|
6317
6505
|
description: "Handle the Hasna event and record evidence.",
|
|
6318
6506
|
target: agentTarget(input, workerPrompt, "worker", seed, plan),
|
|
6319
|
-
timeoutMs:
|
|
6507
|
+
timeoutMs: agentTimeoutMs(input)
|
|
6320
6508
|
},
|
|
6321
6509
|
{
|
|
6322
6510
|
id: "verifier",
|
|
6323
6511
|
name: "Verifier",
|
|
6324
6512
|
description: "Adversarially verify event handling.",
|
|
6325
6513
|
dependsOn: ["worker"],
|
|
6326
|
-
target:
|
|
6327
|
-
|
|
6328
|
-
idleTimeoutMs: 10 * 60000
|
|
6329
|
-
},
|
|
6330
|
-
timeoutMs: 30 * 60000
|
|
6514
|
+
target: agentTarget(input, verifierPrompt, "verifier", seed, plan),
|
|
6515
|
+
timeoutMs: agentTimeoutMs(input)
|
|
6331
6516
|
}
|
|
6332
6517
|
])
|
|
6333
6518
|
};
|
|
@@ -6339,7 +6524,7 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
|
|
|
6339
6524
|
throw new Error("projectPath is required");
|
|
6340
6525
|
const seed = `${input.projectPath}:${input.objective}`;
|
|
6341
6526
|
const plan = worktreePlan(input, seed);
|
|
6342
|
-
const timeoutMs =
|
|
6527
|
+
const timeoutMs = agentTimeoutMs(input);
|
|
6343
6528
|
const workerPrompt = [
|
|
6344
6529
|
`/goal ${input.objective}`,
|
|
6345
6530
|
"",
|
|
@@ -6377,11 +6562,8 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
|
|
|
6377
6562
|
name: "Verifier",
|
|
6378
6563
|
description: "Adversarially verify the bounded objective result.",
|
|
6379
6564
|
dependsOn: ["worker"],
|
|
6380
|
-
target:
|
|
6381
|
-
|
|
6382
|
-
idleTimeoutMs: 10 * 60000
|
|
6383
|
-
},
|
|
6384
|
-
timeoutMs: Math.min(timeoutMs, 30 * 60000)
|
|
6565
|
+
target: agentTarget(input, verifierPrompt, "verifier", seed, plan),
|
|
6566
|
+
timeoutMs
|
|
6385
6567
|
}
|
|
6386
6568
|
])
|
|
6387
6569
|
};
|
|
@@ -6410,7 +6592,7 @@ function renderLifecycleBoundedTemplate(id, values) {
|
|
|
6410
6592
|
worktreeMode: values.worktreeMode ?? (id === REPORT_ONLY_TEMPLATE_ID ? "main" : "required"),
|
|
6411
6593
|
worktreeRoot: values.worktreeRoot,
|
|
6412
6594
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
6413
|
-
timeoutMs:
|
|
6595
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
|
|
6414
6596
|
};
|
|
6415
6597
|
if (id === TASK_LIFECYCLE_TEMPLATE_ID) {
|
|
6416
6598
|
const taskId = values.taskId ?? "";
|
|
@@ -6447,6 +6629,7 @@ function renderLifecycleBoundedTemplate(id, values) {
|
|
|
6447
6629
|
worktreeMode: values.worktreeMode ?? "required",
|
|
6448
6630
|
worktreeRoot: values.worktreeRoot,
|
|
6449
6631
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
6632
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
|
|
6450
6633
|
eventId: values.eventId,
|
|
6451
6634
|
eventType: values.eventType
|
|
6452
6635
|
});
|
|
@@ -6513,6 +6696,8 @@ function renderDeterministicCheckCreateTaskWorkflow(values) {
|
|
|
6513
6696
|
if (!checkCommand.trim())
|
|
6514
6697
|
throw new Error("checkCommand is required");
|
|
6515
6698
|
const seed = `${projectPath}:${checkCommand}`;
|
|
6699
|
+
const timeoutMs = parseDeterministicTimeoutMs(values.timeoutMs, 5 * 60000);
|
|
6700
|
+
const idleTimeoutMs = parseDeterministicTimeoutMs(values.idleTimeoutMs, 60000, "idleTimeoutMs");
|
|
6516
6701
|
return {
|
|
6517
6702
|
name: values.name ?? `deterministic-check-${stableIndex(seed, 4294967295).toString(16).padStart(8, "0")}`,
|
|
6518
6703
|
description: values.description ?? "Deterministic check that writes compact evidence and upserts one deduped todos task when the expectation is not met.",
|
|
@@ -6527,10 +6712,10 @@ function renderDeterministicCheckCreateTaskWorkflow(values) {
|
|
|
6527
6712
|
command: "bash",
|
|
6528
6713
|
args: ["-lc", checkCommand],
|
|
6529
6714
|
cwd: projectPath,
|
|
6530
|
-
timeoutMs
|
|
6531
|
-
idleTimeoutMs
|
|
6715
|
+
timeoutMs,
|
|
6716
|
+
idleTimeoutMs
|
|
6532
6717
|
},
|
|
6533
|
-
timeoutMs
|
|
6718
|
+
timeoutMs
|
|
6534
6719
|
}
|
|
6535
6720
|
]
|
|
6536
6721
|
};
|
|
@@ -6568,6 +6753,7 @@ function renderBuiltinLoopTemplate(id, values) {
|
|
|
6568
6753
|
worktreeMode: values.worktreeMode,
|
|
6569
6754
|
worktreeRoot: values.worktreeRoot,
|
|
6570
6755
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
6756
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
|
|
6571
6757
|
eventId: values.eventId,
|
|
6572
6758
|
eventType: values.eventType
|
|
6573
6759
|
});
|
|
@@ -6599,7 +6785,8 @@ function renderBuiltinLoopTemplate(id, values) {
|
|
|
6599
6785
|
manualBreakGlass: booleanVar(values.manualBreakGlass),
|
|
6600
6786
|
worktreeMode: values.worktreeMode,
|
|
6601
6787
|
worktreeRoot: values.worktreeRoot,
|
|
6602
|
-
worktreeBranchPrefix: values.worktreeBranchPrefix
|
|
6788
|
+
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
6789
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
|
|
6603
6790
|
});
|
|
6604
6791
|
}
|
|
6605
6792
|
if (id === BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID) {
|
|
@@ -6627,7 +6814,7 @@ function renderBuiltinLoopTemplate(id, values) {
|
|
|
6627
6814
|
worktreeMode: values.worktreeMode,
|
|
6628
6815
|
worktreeRoot: values.worktreeRoot,
|
|
6629
6816
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
6630
|
-
timeoutMs:
|
|
6817
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
|
|
6631
6818
|
});
|
|
6632
6819
|
}
|
|
6633
6820
|
throw new Error(`unknown template: ${id}`);
|
|
@@ -6862,7 +7049,11 @@ function runDoctor(store) {
|
|
|
6862
7049
|
if (loop.target.type === "workflow") {
|
|
6863
7050
|
const workflow = store.requireWorkflow(loop.target.workflowId);
|
|
6864
7051
|
for (const step of workflowExecutionOrder(workflow)) {
|
|
6865
|
-
preflightTarget({
|
|
7052
|
+
preflightTarget({
|
|
7053
|
+
...step.target,
|
|
7054
|
+
account: step.account ?? step.target.account,
|
|
7055
|
+
timeoutMs: step.timeoutMs !== undefined ? step.timeoutMs : step.target.timeoutMs
|
|
7056
|
+
}, { loopId: loop.id, loopName: loop.name, workflowId: workflow.id, workflowName: workflow.name, workflowStepId: step.id }, { machine: loop.machine });
|
|
6866
7057
|
}
|
|
6867
7058
|
} else {
|
|
6868
7059
|
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;
|