@hasna/loops 0.4.4 → 0.4.6
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/CHANGELOG.md +39 -0
- package/README.md +2 -2
- package/dist/api/index.js +19 -10
- package/dist/cli/index.js +368 -193
- package/dist/daemon/index.js +317 -163
- package/dist/index.js +339 -175
- package/dist/lib/executor.d.ts +14 -1
- package/dist/lib/mode.js +1 -1
- package/dist/lib/storage/index.js +56 -5
- package/dist/lib/storage/sqlite.js +56 -5
- package/dist/lib/store.d.ts +7 -0
- package/dist/lib/store.js +56 -5
- package/dist/lib/template-kit.d.ts +6 -0
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.js +321 -166
- package/dist/runner/index.d.ts +3 -1
- package/dist/runner/index.js +130 -11
- package/dist/sdk/index.js +320 -167
- package/docs/USAGE.md +2 -2
- package/package.json +1 -1
package/dist/sdk/index.js
CHANGED
|
@@ -1491,11 +1491,14 @@ function ensurePrivateStorePath(file) {
|
|
|
1491
1491
|
class Store {
|
|
1492
1492
|
db;
|
|
1493
1493
|
rootDir;
|
|
1494
|
+
memoryRootDir;
|
|
1494
1495
|
constructor(path) {
|
|
1495
1496
|
const file = path ?? dbPath();
|
|
1496
1497
|
if (file !== ":memory:")
|
|
1497
1498
|
ensurePrivateStorePath(file);
|
|
1498
1499
|
this.rootDir = file === ":memory:" ? mkdtempSync(join3(tmpdir(), "open-loops-store-")) : dirname2(file);
|
|
1500
|
+
if (file === ":memory:")
|
|
1501
|
+
this.memoryRootDir = this.rootDir;
|
|
1499
1502
|
this.db = new Database(file);
|
|
1500
1503
|
this.db.exec("PRAGMA foreign_keys = ON;");
|
|
1501
1504
|
this.db.exec("PRAGMA busy_timeout = 5000;");
|
|
@@ -1640,7 +1643,6 @@ class Store {
|
|
|
1640
1643
|
CREATE INDEX IF NOT EXISTS idx_runs_status ON loop_runs(status);
|
|
1641
1644
|
CREATE INDEX IF NOT EXISTS idx_runs_status_lease ON loop_runs(status, lease_expires_at);
|
|
1642
1645
|
CREATE INDEX IF NOT EXISTS idx_runs_scheduled ON loop_runs(scheduled_for);
|
|
1643
|
-
CREATE INDEX IF NOT EXISTS idx_runs_claim_token ON loop_runs(claim_token) WHERE claim_token IS NOT NULL;
|
|
1644
1646
|
|
|
1645
1647
|
CREATE TABLE IF NOT EXISTS daemon_lease (
|
|
1646
1648
|
id TEXT PRIMARY KEY,
|
|
@@ -1949,9 +1951,12 @@ class Store {
|
|
|
1949
1951
|
const rows = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 2").all(idOrName);
|
|
1950
1952
|
if (rows.length === 0)
|
|
1951
1953
|
throw new LoopNotFoundError(idOrName);
|
|
1952
|
-
if (rows.length
|
|
1954
|
+
if (rows.length === 1)
|
|
1955
|
+
return rowToLoop(rows[0]);
|
|
1956
|
+
const active = this.db.query("SELECT * FROM loops WHERE name = ? AND archived_at IS NULL ORDER BY created_at DESC LIMIT 2").all(idOrName);
|
|
1957
|
+
if (active.length !== 1)
|
|
1953
1958
|
throw new AmbiguousNameError(idOrName);
|
|
1954
|
-
return rowToLoop(
|
|
1959
|
+
return rowToLoop(active[0]);
|
|
1955
1960
|
}
|
|
1956
1961
|
requireLoop(idOrName) {
|
|
1957
1962
|
return this.getLoop(idOrName) ?? this.findLoopByName(idOrName) ?? (() => {
|
|
@@ -2292,6 +2297,7 @@ class Store {
|
|
|
2292
2297
|
return this.transact(() => {
|
|
2293
2298
|
const loop = this.requireLoop(idOrName);
|
|
2294
2299
|
this.setWorkflowWorkItemsForLoop(loop.id, "cancelled", "loop deleted", nowIso());
|
|
2300
|
+
this.db.query("DELETE FROM loop_runs WHERE loop_id = ?").run(loop.id);
|
|
2295
2301
|
const res = this.db.query("DELETE FROM loops WHERE id = ?").run(loop.id);
|
|
2296
2302
|
return res.changes > 0;
|
|
2297
2303
|
});
|
|
@@ -2776,7 +2782,10 @@ class Store {
|
|
|
2776
2782
|
return rowToGoal(row);
|
|
2777
2783
|
}
|
|
2778
2784
|
if (context.sourceType && context.sourceId) {
|
|
2779
|
-
const
|
|
2785
|
+
const terminalPlaceholders = GOAL_TERMINAL.map(() => "?").join(", ");
|
|
2786
|
+
const row = this.db.query(`SELECT * FROM goals
|
|
2787
|
+
WHERE source_type = ? AND source_id = ? AND status NOT IN (${terminalPlaceholders})
|
|
2788
|
+
ORDER BY created_at DESC LIMIT 1`).get(context.sourceType, context.sourceId, ...GOAL_TERMINAL);
|
|
2780
2789
|
if (row)
|
|
2781
2790
|
return rowToGoal(row);
|
|
2782
2791
|
}
|
|
@@ -3196,6 +3205,41 @@ class Store {
|
|
|
3196
3205
|
throw new Error(`workflow step run not found after pid update: ${workflowRunId}/${stepId}`);
|
|
3197
3206
|
return run;
|
|
3198
3207
|
}
|
|
3208
|
+
recordWorkflowStepProgress(workflowRunId, stepId, progress, opts = {}) {
|
|
3209
|
+
const now = (opts.now ?? new Date).toISOString();
|
|
3210
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
3211
|
+
try {
|
|
3212
|
+
const res = this.db.query(`UPDATE workflow_step_runs
|
|
3213
|
+
SET stdout=COALESCE($stdout, stdout),
|
|
3214
|
+
stderr=COALESCE($stderr, stderr),
|
|
3215
|
+
updated_at=$updated
|
|
3216
|
+
WHERE workflow_run_id=$workflowRunId AND step_id=$stepId AND status='running'
|
|
3217
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
3218
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
3219
|
+
))`).run({
|
|
3220
|
+
$workflowRunId: workflowRunId,
|
|
3221
|
+
$stepId: stepId,
|
|
3222
|
+
$stdout: progress.stdout === undefined ? null : scrubbedOrNull(progress.stdout),
|
|
3223
|
+
$stderr: progress.stderr === undefined ? null : scrubbedOrNull(progress.stderr),
|
|
3224
|
+
$updated: now,
|
|
3225
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
3226
|
+
$now: now
|
|
3227
|
+
});
|
|
3228
|
+
if (res.changes === 1) {
|
|
3229
|
+
this.appendWorkflowEvent(workflowRunId, "step_progress", stepId, progress.payload);
|
|
3230
|
+
}
|
|
3231
|
+
this.db.exec("COMMIT");
|
|
3232
|
+
} catch (error) {
|
|
3233
|
+
try {
|
|
3234
|
+
this.db.exec("ROLLBACK");
|
|
3235
|
+
} catch {}
|
|
3236
|
+
throw error;
|
|
3237
|
+
}
|
|
3238
|
+
const run = this.getWorkflowStepRun(workflowRunId, stepId);
|
|
3239
|
+
if (!run)
|
|
3240
|
+
throw new Error(`workflow step run not found after progress update: ${workflowRunId}/${stepId}`);
|
|
3241
|
+
return run;
|
|
3242
|
+
}
|
|
3199
3243
|
recoverWorkflowRun(workflowRunId, reason = "workflow run recovered for retry") {
|
|
3200
3244
|
return this.transact(() => {
|
|
3201
3245
|
const now = nowIso();
|
|
@@ -3633,7 +3677,8 @@ class Store {
|
|
|
3633
3677
|
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
3634
3678
|
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
3635
3679
|
))`).run(params) : this.db.query(`UPDATE loop_runs SET status=$status, finished_at=$finished, claim_token=NULL, lease_expires_at=NULL, pid=$pid, exit_code=$exitCode,
|
|
3636
|
-
duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated
|
|
3680
|
+
duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated
|
|
3681
|
+
WHERE id=$id AND status='running'`).run(params);
|
|
3637
3682
|
const run = this.getRun(id);
|
|
3638
3683
|
if (!run)
|
|
3639
3684
|
throw new Error(`run not found after finalize: ${id}`);
|
|
@@ -4161,12 +4206,18 @@ class Store {
|
|
|
4161
4206
|
}
|
|
4162
4207
|
close() {
|
|
4163
4208
|
this.db.close();
|
|
4209
|
+
if (this.memoryRootDir) {
|
|
4210
|
+
try {
|
|
4211
|
+
rmSync2(this.memoryRootDir, { recursive: true, force: true });
|
|
4212
|
+
} catch {}
|
|
4213
|
+
this.memoryRootDir = undefined;
|
|
4214
|
+
}
|
|
4164
4215
|
}
|
|
4165
4216
|
}
|
|
4166
4217
|
// package.json
|
|
4167
4218
|
var package_default = {
|
|
4168
4219
|
name: "@hasna/loops",
|
|
4169
|
-
version: "0.4.
|
|
4220
|
+
version: "0.4.6",
|
|
4170
4221
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4171
4222
|
type: "module",
|
|
4172
4223
|
main: "dist/index.js",
|
|
@@ -4624,10 +4675,7 @@ async function stopDaemon(opts = {}) {
|
|
|
4624
4675
|
const store = new Store(join4(dirname3(path), "loops.db"));
|
|
4625
4676
|
try {
|
|
4626
4677
|
const lease = store.getDaemonLease();
|
|
4627
|
-
|
|
4628
|
-
removePid(path);
|
|
4629
|
-
return { wasRunning: false, stopped: false, forced: false, pid: state.pid };
|
|
4630
|
-
}
|
|
4678
|
+
const leaseVerified = Boolean(lease && lease.pid === state.pid && new Date(lease.expiresAt).getTime() > Date.now());
|
|
4631
4679
|
try {
|
|
4632
4680
|
process.kill(state.pid, "SIGTERM");
|
|
4633
4681
|
} catch {
|
|
@@ -4647,11 +4695,14 @@ async function stopDaemon(opts = {}) {
|
|
|
4647
4695
|
} catch {}
|
|
4648
4696
|
await sleep(150);
|
|
4649
4697
|
removePid(path);
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4698
|
+
let reapedPgids = [];
|
|
4699
|
+
if (leaseVerified && lease) {
|
|
4700
|
+
const ownedRuns = store.listRuns({ status: "running", limit: 1000 }).filter((run) => run.claimedBy !== undefined && run.claimedBy.endsWith(`:${lease.id}`));
|
|
4701
|
+
reapedPgids = await reapProcessGroups(ownedRuns.map(toReapableProcess), {
|
|
4702
|
+
sleep,
|
|
4703
|
+
graceMs: opts.reapGraceMs
|
|
4704
|
+
});
|
|
4705
|
+
}
|
|
4655
4706
|
return { wasRunning: true, stopped: !isAlive(state.pid, record?.startedAt), forced: true, pid: state.pid, reapedPgids };
|
|
4656
4707
|
} finally {
|
|
4657
4708
|
store.close();
|
|
@@ -4985,7 +5036,12 @@ function notifySpawn(pid, opts) {
|
|
|
4985
5036
|
if (!pid)
|
|
4986
5037
|
return;
|
|
4987
5038
|
opts.onSpawn?.(pid);
|
|
4988
|
-
|
|
5039
|
+
const startedMs = processStartTimeMs(pid);
|
|
5040
|
+
opts.onSpawnProcess?.({
|
|
5041
|
+
pid,
|
|
5042
|
+
pgid: pid,
|
|
5043
|
+
processStartedAt: startedMs !== undefined ? new Date(startedMs).toISOString() : nowIso()
|
|
5044
|
+
});
|
|
4989
5045
|
}
|
|
4990
5046
|
function shellQuote(value) {
|
|
4991
5047
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -5340,6 +5396,13 @@ function numberField(value, key) {
|
|
|
5340
5396
|
function codewithAgentStatus(readJson) {
|
|
5341
5397
|
return stringField(recordField(readJson, "agent"), "status") ?? stringField(recordField(readJson, "statusSnapshot"), "status");
|
|
5342
5398
|
}
|
|
5399
|
+
function codewithAgentLastEventSeq(readJson) {
|
|
5400
|
+
const agentSeq = numberField(recordField(readJson, "agent"), "lastEventSeq");
|
|
5401
|
+
const snapshotSeq = numberField(recordField(readJson, "statusSnapshot"), "lastEventSeq");
|
|
5402
|
+
if (agentSeq !== undefined && snapshotSeq !== undefined)
|
|
5403
|
+
return Math.max(agentSeq, snapshotSeq);
|
|
5404
|
+
return agentSeq ?? snapshotSeq;
|
|
5405
|
+
}
|
|
5343
5406
|
function codewithAgentEvidence(startJson, readJson, logsJson) {
|
|
5344
5407
|
const agent = recordField(readJson, "agent") ?? recordField(startJson, "agent");
|
|
5345
5408
|
const statusSnapshot = recordField(readJson, "statusSnapshot");
|
|
@@ -5370,6 +5433,21 @@ function codewithAgentEvidence(startJson, readJson, logsJson) {
|
|
|
5370
5433
|
events
|
|
5371
5434
|
}, null, 2);
|
|
5372
5435
|
}
|
|
5436
|
+
function codewithAgentProgress(readJson) {
|
|
5437
|
+
const agent = recordField(readJson, "agent");
|
|
5438
|
+
const statusSnapshot = recordField(readJson, "statusSnapshot");
|
|
5439
|
+
return {
|
|
5440
|
+
provider: "codewith",
|
|
5441
|
+
agentId: stringField(agent, "agentId"),
|
|
5442
|
+
status: stringField(agent, "status") ?? stringField(statusSnapshot, "status"),
|
|
5443
|
+
summary: stringField(statusSnapshot, "summary"),
|
|
5444
|
+
statusReason: stringField(agent, "statusReason"),
|
|
5445
|
+
threadId: stringField(agent, "threadId"),
|
|
5446
|
+
rolloutPath: stringField(agent, "rolloutPath"),
|
|
5447
|
+
pid: numberField(agent, "pid"),
|
|
5448
|
+
lastEventSeq: codewithAgentLastEventSeq(readJson)
|
|
5449
|
+
};
|
|
5450
|
+
}
|
|
5373
5451
|
function sleep(ms) {
|
|
5374
5452
|
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
5375
5453
|
}
|
|
@@ -5404,6 +5482,7 @@ async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt)
|
|
|
5404
5482
|
if (!agentId) {
|
|
5405
5483
|
return failureResult(startedAt, "codewith agent start did not return agent.agentId", { stdout: start.stdout, stderr: stderr.value() });
|
|
5406
5484
|
}
|
|
5485
|
+
opts.onAgentProgress?.(codewithAgentProgress(startJson));
|
|
5407
5486
|
const stopAgent = async () => {
|
|
5408
5487
|
await spawnCapture(spec.command, codewithAgentControlArgs(target, "stop", agentId), {
|
|
5409
5488
|
cwd: spec.cwd,
|
|
@@ -5446,10 +5525,15 @@ async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt)
|
|
|
5446
5525
|
});
|
|
5447
5526
|
}
|
|
5448
5527
|
const status = codewithAgentStatus(lastReadJson);
|
|
5449
|
-
const fingerprint = JSON.stringify({
|
|
5528
|
+
const fingerprint = JSON.stringify({
|
|
5529
|
+
status,
|
|
5530
|
+
agentLastEventSeq: numberField(recordField(lastReadJson, "agent"), "lastEventSeq"),
|
|
5531
|
+
snapshot: recordField(lastReadJson, "statusSnapshot") ?? null
|
|
5532
|
+
});
|
|
5450
5533
|
if (fingerprint !== lastFingerprint) {
|
|
5451
5534
|
lastFingerprint = fingerprint;
|
|
5452
5535
|
lastProgressAt = Date.now();
|
|
5536
|
+
opts.onAgentProgress?.(codewithAgentProgress(lastReadJson));
|
|
5453
5537
|
}
|
|
5454
5538
|
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
5455
5539
|
const logs = await spawnCapture(spec.command, codewithAgentControlArgs(target, "logs", agentId), {
|
|
@@ -6377,7 +6461,7 @@ ${failure.evidence.stderr}` : undefined
|
|
|
6377
6461
|
|
|
6378
6462
|
`);
|
|
6379
6463
|
const dedupeKey = `openloops:${loop.id}:${failure.fingerprint}`;
|
|
6380
|
-
const tags = ["bug", "openloops", "loop-health", failure.classification];
|
|
6464
|
+
const tags = ["bug", "openloops", "loops", "loop-health", failure.classification];
|
|
6381
6465
|
const priority = failure.classification === "auth" || failure.classification === "rate_limit" ? "high" : "medium";
|
|
6382
6466
|
return {
|
|
6383
6467
|
title,
|
|
@@ -6392,7 +6476,7 @@ ${failure.evidence.stderr}` : undefined
|
|
|
6392
6476
|
comment: ["todos", "comment", "<task-id>", description]
|
|
6393
6477
|
},
|
|
6394
6478
|
futureNativeUpsert: {
|
|
6395
|
-
command: "todos upsert",
|
|
6479
|
+
command: "todos task upsert",
|
|
6396
6480
|
fields: {
|
|
6397
6481
|
title,
|
|
6398
6482
|
description,
|
|
@@ -7596,173 +7680,213 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
7596
7680
|
if (run.status === "succeeded" || run.status === "failed" || run.status === "timed_out" || run.status === "cancelled") {
|
|
7597
7681
|
return workflowResult(run, run.status, startedAt, run.finishedAt ?? nowIso(), store.listWorkflowStepRuns(run.id), run.error);
|
|
7598
7682
|
}
|
|
7683
|
+
const resumedRunningSteps = store.listWorkflowStepRuns(run.id).filter((step) => step.status === "running");
|
|
7684
|
+
if (resumedRunningSteps.length > 0 && resumedRunningSteps.every((step) => step.pid !== undefined)) {
|
|
7685
|
+
try {
|
|
7686
|
+
store.recoverWorkflowRun(run.id, "workflow run resumed after lease takeover");
|
|
7687
|
+
} catch {}
|
|
7688
|
+
}
|
|
7599
7689
|
const ordered = workflowExecutionOrder(workflow);
|
|
7600
7690
|
const byId = new Map(workflow.steps.map((step) => [step.id, step]));
|
|
7601
7691
|
let blockingError;
|
|
7602
7692
|
let terminalStatus = "succeeded";
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
|
|
7618
|
-
|
|
7619
|
-
const
|
|
7620
|
-
|
|
7621
|
-
|
|
7622
|
-
|
|
7623
|
-
|
|
7624
|
-
|
|
7625
|
-
|
|
7626
|
-
|
|
7627
|
-
|
|
7628
|
-
store.
|
|
7629
|
-
|
|
7630
|
-
|
|
7693
|
+
try {
|
|
7694
|
+
for (const step of ordered) {
|
|
7695
|
+
if (store.isWorkflowRunTerminal(run.id)) {
|
|
7696
|
+
terminalStatus = store.requireWorkflowRun(run.id).status;
|
|
7697
|
+
blockingError = "workflow run was cancelled";
|
|
7698
|
+
break;
|
|
7699
|
+
}
|
|
7700
|
+
const pendingTimeout = opts.signal?.aborted ? opts.signalTimeoutMessage?.() : undefined;
|
|
7701
|
+
if (pendingTimeout) {
|
|
7702
|
+
terminalStatus = "timed_out";
|
|
7703
|
+
blockingError = pendingTimeout;
|
|
7704
|
+
break;
|
|
7705
|
+
}
|
|
7706
|
+
const existing = store.getWorkflowStepRun(run.id, step.id);
|
|
7707
|
+
if (existing?.status === "succeeded" || existing?.status === "skipped" || existing?.status === "cancelled")
|
|
7708
|
+
continue;
|
|
7709
|
+
const blockedBy = (step.dependsOn ?? []).find((dependencyId) => {
|
|
7710
|
+
const dependencyRun = store.getWorkflowStepRun(run.id, dependencyId);
|
|
7711
|
+
const dependencyStep = byId.get(dependencyId);
|
|
7712
|
+
if (dependencyRun?.status === "succeeded")
|
|
7713
|
+
return false;
|
|
7714
|
+
return !dependencyStep?.continueOnFailure;
|
|
7715
|
+
});
|
|
7716
|
+
if (blockedBy) {
|
|
7717
|
+
opts.beforePersist?.();
|
|
7718
|
+
if (isBlockedStepRun(store.getWorkflowStepRun(run.id, blockedBy))) {
|
|
7719
|
+
store.skipWorkflowStepRun(run.id, step.id, `${BLOCKED_STEP_ERROR_PREFIX} upstream step ${blockedBy} was blocked`, {
|
|
7720
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
7721
|
+
});
|
|
7722
|
+
continue;
|
|
7723
|
+
}
|
|
7724
|
+
store.skipWorkflowStepRun(run.id, step.id, `dependency did not succeed: ${blockedBy}`, { daemonLeaseId: opts.daemonLeaseId });
|
|
7725
|
+
blockingError ??= `step ${step.id} blocked by dependency ${blockedBy}`;
|
|
7726
|
+
terminalStatus = "failed";
|
|
7631
7727
|
continue;
|
|
7632
7728
|
}
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
});
|
|
7653
|
-
let result;
|
|
7654
|
-
const controller = new AbortController;
|
|
7655
|
-
const externalAbort = () => controller.abort();
|
|
7656
|
-
if (opts.signal?.aborted)
|
|
7657
|
-
controller.abort();
|
|
7658
|
-
opts.signal?.addEventListener("abort", externalAbort, { once: true });
|
|
7659
|
-
const cancelTimer = setInterval(() => {
|
|
7660
|
-
if (store.getWorkflowRun(run.id)?.status === "cancelled")
|
|
7729
|
+
opts.beforePersist?.();
|
|
7730
|
+
const startedStep = store.startWorkflowStepRun(run.id, step.id, { daemonLeaseId: opts.daemonLeaseId });
|
|
7731
|
+
if (startedStep.status !== "running") {
|
|
7732
|
+
terminalStatus = "failed";
|
|
7733
|
+
blockingError = `step ${step.id} could not start because workflow is no longer running`;
|
|
7734
|
+
break;
|
|
7735
|
+
}
|
|
7736
|
+
const stepContext = goalExecutionContext({
|
|
7737
|
+
loop: opts.loop,
|
|
7738
|
+
loopRun: opts.loopRun,
|
|
7739
|
+
scheduledFor: opts.scheduledFor,
|
|
7740
|
+
workflow,
|
|
7741
|
+
workflowRunId: run.id,
|
|
7742
|
+
workflowStepId: step.id
|
|
7743
|
+
});
|
|
7744
|
+
let result;
|
|
7745
|
+
const controller = new AbortController;
|
|
7746
|
+
const externalAbort = () => controller.abort();
|
|
7747
|
+
if (opts.signal?.aborted)
|
|
7661
7748
|
controller.abort();
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
opts
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7749
|
+
opts.signal?.addEventListener("abort", externalAbort, { once: true });
|
|
7750
|
+
const cancelTimer = setInterval(() => {
|
|
7751
|
+
if (store.getWorkflowRun(run.id)?.status === "cancelled")
|
|
7752
|
+
controller.abort();
|
|
7753
|
+
}, opts.cancelPollMs ?? 500);
|
|
7754
|
+
cancelTimer.unref();
|
|
7755
|
+
try {
|
|
7756
|
+
if (step.goal) {
|
|
7757
|
+
result = await runGoal(store, step.goal, {
|
|
7758
|
+
...opts,
|
|
7759
|
+
model: opts.goalModel,
|
|
7760
|
+
target: targetWithStepAccount(step),
|
|
7761
|
+
signal: controller.signal,
|
|
7762
|
+
context: stepContext
|
|
7763
|
+
});
|
|
7764
|
+
} else {
|
|
7765
|
+
result = await executeTarget(targetWithStepAccount(step, opts.goalNodePrompt), executionMetadata(stepContext), {
|
|
7766
|
+
...opts,
|
|
7767
|
+
machine: opts.machine ?? opts.loop?.machine,
|
|
7768
|
+
signal: controller.signal,
|
|
7769
|
+
onAgentProgress: (progress) => {
|
|
7770
|
+
const stdout = JSON.stringify({ agentProgress: progress }, null, 2);
|
|
7771
|
+
opts.beforePersist?.();
|
|
7772
|
+
store.recordWorkflowStepProgress(run.id, step.id, {
|
|
7773
|
+
stdout,
|
|
7774
|
+
payload: progress
|
|
7775
|
+
}, {
|
|
7776
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
7777
|
+
});
|
|
7778
|
+
opts.onAgentProgress?.(progress);
|
|
7779
|
+
},
|
|
7780
|
+
onSpawn: (pid) => {
|
|
7781
|
+
opts.beforePersist?.();
|
|
7782
|
+
store.markWorkflowStepPid(run.id, step.id, pid, { daemonLeaseId: opts.daemonLeaseId });
|
|
7783
|
+
opts.onSpawn?.(pid);
|
|
7784
|
+
}
|
|
7785
|
+
});
|
|
7786
|
+
}
|
|
7787
|
+
} catch (error) {
|
|
7788
|
+
const finishedAt2 = nowIso();
|
|
7789
|
+
result = {
|
|
7790
|
+
status: "failed",
|
|
7791
|
+
stdout: "",
|
|
7792
|
+
stderr: "",
|
|
7793
|
+
error: error instanceof Error ? error.message : String(error),
|
|
7794
|
+
startedAt: startedStep.startedAt ?? finishedAt2,
|
|
7795
|
+
finishedAt: finishedAt2,
|
|
7796
|
+
durationMs: new Date(finishedAt2).getTime() - new Date(startedStep.startedAt ?? finishedAt2).getTime()
|
|
7797
|
+
};
|
|
7798
|
+
} finally {
|
|
7799
|
+
clearInterval(cancelTimer);
|
|
7800
|
+
opts.signal?.removeEventListener("abort", externalAbort);
|
|
7801
|
+
}
|
|
7802
|
+
const timeoutMessage = opts.signal?.aborted ? opts.signalTimeoutMessage?.() : undefined;
|
|
7803
|
+
if (timeoutMessage && result.status === "failed") {
|
|
7804
|
+
result = { ...result, status: "timed_out", error: timeoutMessage };
|
|
7805
|
+
}
|
|
7806
|
+
if (store.isWorkflowRunTerminal(run.id)) {
|
|
7807
|
+
terminalStatus = store.requireWorkflowRun(run.id).status;
|
|
7808
|
+
blockingError = "workflow run was cancelled";
|
|
7809
|
+
break;
|
|
7810
|
+
}
|
|
7811
|
+
opts.beforePersist?.();
|
|
7812
|
+
const blockedExit = result.status === "failed" && result.exitCode !== undefined && blockedExitCodesForStep(step).includes(result.exitCode);
|
|
7813
|
+
if (blockedExit) {
|
|
7814
|
+
store.finalizeWorkflowStepRun(run.id, step.id, {
|
|
7815
|
+
status: "skipped",
|
|
7816
|
+
finishedAt: result.finishedAt,
|
|
7817
|
+
durationMs: result.durationMs,
|
|
7818
|
+
stdout: result.stdout,
|
|
7819
|
+
stderr: result.stderr,
|
|
7820
|
+
exitCode: result.exitCode,
|
|
7821
|
+
error: `${BLOCKED_STEP_ERROR_PREFIX} step ${step.id} exited with blocked exit code ${result.exitCode}`
|
|
7822
|
+
}, {
|
|
7823
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
7683
7824
|
});
|
|
7825
|
+
continue;
|
|
7684
7826
|
}
|
|
7685
|
-
} catch (error) {
|
|
7686
|
-
const finishedAt2 = nowIso();
|
|
7687
|
-
result = {
|
|
7688
|
-
status: "failed",
|
|
7689
|
-
stdout: "",
|
|
7690
|
-
stderr: "",
|
|
7691
|
-
error: error instanceof Error ? error.message : String(error),
|
|
7692
|
-
startedAt: startedStep.startedAt ?? finishedAt2,
|
|
7693
|
-
finishedAt: finishedAt2,
|
|
7694
|
-
durationMs: new Date(finishedAt2).getTime() - new Date(startedStep.startedAt ?? finishedAt2).getTime()
|
|
7695
|
-
};
|
|
7696
|
-
} finally {
|
|
7697
|
-
clearInterval(cancelTimer);
|
|
7698
|
-
opts.signal?.removeEventListener("abort", externalAbort);
|
|
7699
|
-
}
|
|
7700
|
-
const timeoutMessage = opts.signal?.aborted ? opts.signalTimeoutMessage?.() : undefined;
|
|
7701
|
-
if (timeoutMessage && result.status === "failed") {
|
|
7702
|
-
result = { ...result, status: "timed_out", error: timeoutMessage };
|
|
7703
|
-
}
|
|
7704
|
-
if (store.isWorkflowRunTerminal(run.id)) {
|
|
7705
|
-
terminalStatus = store.requireWorkflowRun(run.id).status;
|
|
7706
|
-
blockingError = "workflow run was cancelled";
|
|
7707
|
-
break;
|
|
7708
|
-
}
|
|
7709
|
-
opts.beforePersist?.();
|
|
7710
|
-
const blockedExit = result.status === "failed" && result.exitCode !== undefined && blockedExitCodesForStep(step).includes(result.exitCode);
|
|
7711
|
-
if (blockedExit) {
|
|
7712
7827
|
store.finalizeWorkflowStepRun(run.id, step.id, {
|
|
7713
|
-
status:
|
|
7828
|
+
status: result.status,
|
|
7714
7829
|
finishedAt: result.finishedAt,
|
|
7715
7830
|
durationMs: result.durationMs,
|
|
7716
7831
|
stdout: result.stdout,
|
|
7717
7832
|
stderr: result.stderr,
|
|
7718
7833
|
exitCode: result.exitCode,
|
|
7719
|
-
error:
|
|
7834
|
+
error: result.error
|
|
7720
7835
|
}, {
|
|
7721
7836
|
daemonLeaseId: opts.daemonLeaseId
|
|
7722
7837
|
});
|
|
7723
|
-
|
|
7838
|
+
if (result.status !== "succeeded" && !step.continueOnFailure) {
|
|
7839
|
+
terminalStatus = result.status;
|
|
7840
|
+
blockingError = `step ${step.id} ${result.status}${result.error ? `: ${result.error}` : ""}`;
|
|
7841
|
+
break;
|
|
7842
|
+
}
|
|
7724
7843
|
}
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7844
|
+
if (terminalStatus !== "succeeded") {
|
|
7845
|
+
for (const step of ordered) {
|
|
7846
|
+
const existing = store.getWorkflowStepRun(run.id, step.id);
|
|
7847
|
+
if (existing?.status === "pending" || existing?.status === "running") {
|
|
7848
|
+
store.skipWorkflowStepRun(run.id, step.id, blockingError ?? "workflow stopped before step could run", {
|
|
7849
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
7850
|
+
});
|
|
7851
|
+
}
|
|
7852
|
+
}
|
|
7853
|
+
}
|
|
7854
|
+
const finishedAt = nowIso();
|
|
7855
|
+
if (store.isWorkflowRunTerminal(run.id)) {
|
|
7856
|
+
const terminalRun = store.requireWorkflowRun(run.id);
|
|
7857
|
+
return workflowResult(terminalRun, terminalRun.status, startedAt, terminalRun.finishedAt ?? finishedAt, store.listWorkflowStepRuns(run.id), terminalRun.error ?? blockingError);
|
|
7858
|
+
}
|
|
7859
|
+
opts.beforePersist?.();
|
|
7860
|
+
const finalRun = store.finalizeWorkflowRun(run.id, terminalStatus, {
|
|
7861
|
+
finishedAt,
|
|
7862
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime(),
|
|
7863
|
+
error: blockingError
|
|
7733
7864
|
}, {
|
|
7734
7865
|
daemonLeaseId: opts.daemonLeaseId
|
|
7735
7866
|
});
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
if (
|
|
7746
|
-
store.
|
|
7867
|
+
return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, store.listWorkflowStepRuns(run.id), blockingError);
|
|
7868
|
+
} catch (error) {
|
|
7869
|
+
if (opts.daemonLeaseId && error instanceof Error && error.message === "daemon lease lost")
|
|
7870
|
+
throw error;
|
|
7871
|
+
if (error instanceof Error && error.message.includes("workflow step is not claimable"))
|
|
7872
|
+
throw error;
|
|
7873
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7874
|
+
const finishedAt = nowIso();
|
|
7875
|
+
try {
|
|
7876
|
+
if (!store.isWorkflowRunTerminal(run.id)) {
|
|
7877
|
+
store.finalizeWorkflowRun(run.id, "failed", {
|
|
7878
|
+
finishedAt,
|
|
7879
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime(),
|
|
7880
|
+
error: message
|
|
7881
|
+
}, {
|
|
7747
7882
|
daemonLeaseId: opts.daemonLeaseId
|
|
7748
7883
|
});
|
|
7749
7884
|
}
|
|
7750
|
-
}
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
const terminalRun = store.requireWorkflowRun(run.id);
|
|
7755
|
-
return workflowResult(terminalRun, terminalRun.status, startedAt, terminalRun.finishedAt ?? finishedAt, store.listWorkflowStepRuns(run.id), terminalRun.error ?? blockingError);
|
|
7885
|
+
} catch {}
|
|
7886
|
+
const current = store.getWorkflowRun(run.id) ?? run;
|
|
7887
|
+
const resultStatus = current.status === "running" ? "failed" : current.status;
|
|
7888
|
+
return workflowResult(current, resultStatus, startedAt, finishedAt, store.listWorkflowStepRuns(run.id), current.error ?? message);
|
|
7756
7889
|
}
|
|
7757
|
-
opts.beforePersist?.();
|
|
7758
|
-
const finalRun = store.finalizeWorkflowRun(run.id, terminalStatus, {
|
|
7759
|
-
finishedAt,
|
|
7760
|
-
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime(),
|
|
7761
|
-
error: blockingError
|
|
7762
|
-
}, {
|
|
7763
|
-
daemonLeaseId: opts.daemonLeaseId
|
|
7764
|
-
});
|
|
7765
|
-
return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, store.listWorkflowStepRuns(run.id), blockingError);
|
|
7766
7890
|
}
|
|
7767
7891
|
function preflightWorkflow(workflow, opts = {}) {
|
|
7768
7892
|
return workflowExecutionOrder(workflow).map((step) => {
|
|
@@ -8133,6 +8257,25 @@ function advanceOptions(deps) {
|
|
|
8133
8257
|
onRun: deps.onRun
|
|
8134
8258
|
};
|
|
8135
8259
|
}
|
|
8260
|
+
var TERMINAL_RUN_STATUSES2 = new Set([
|
|
8261
|
+
"succeeded",
|
|
8262
|
+
"failed",
|
|
8263
|
+
"timed_out",
|
|
8264
|
+
"abandoned",
|
|
8265
|
+
"skipped"
|
|
8266
|
+
]);
|
|
8267
|
+
function repairWedgedTerminalSlot(deps, loop, scheduledFor, now) {
|
|
8268
|
+
const existing = deps.store.getRunBySlot(loop.id, scheduledFor);
|
|
8269
|
+
if (!existing || !TERMINAL_RUN_STATUSES2.has(existing.status))
|
|
8270
|
+
return;
|
|
8271
|
+
try {
|
|
8272
|
+
advanceLoop(deps.store, loop, existing, new Date(existing.finishedAt ?? now), existing.status === "succeeded", advanceOptions(deps));
|
|
8273
|
+
} catch (error) {
|
|
8274
|
+
if (deps.daemonLeaseId && isDaemonLeaseLost(error))
|
|
8275
|
+
return;
|
|
8276
|
+
throw error;
|
|
8277
|
+
}
|
|
8278
|
+
}
|
|
8136
8279
|
async function runSlot(deps, loop, scheduledFor) {
|
|
8137
8280
|
const now = deps.now?.() ?? new Date;
|
|
8138
8281
|
deps.beforeRun?.(loop, scheduledFor);
|
|
@@ -8159,8 +8302,10 @@ async function runSlot(deps, loop, scheduledFor) {
|
|
|
8159
8302
|
return;
|
|
8160
8303
|
throw error;
|
|
8161
8304
|
}
|
|
8162
|
-
if (!claim)
|
|
8305
|
+
if (!claim) {
|
|
8306
|
+
repairWedgedTerminalSlot(deps, loop, scheduledFor, now);
|
|
8163
8307
|
return;
|
|
8308
|
+
}
|
|
8164
8309
|
deps.beforeRun?.(claim.loop, claim.run.scheduledFor);
|
|
8165
8310
|
deps.onRun?.(claim.run);
|
|
8166
8311
|
const finalRun = await executeClaimedRun({
|
|
@@ -8206,8 +8351,10 @@ function claimSlot(deps, loop, scheduledFor) {
|
|
|
8206
8351
|
return;
|
|
8207
8352
|
throw error;
|
|
8208
8353
|
}
|
|
8209
|
-
if (!claim)
|
|
8354
|
+
if (!claim) {
|
|
8355
|
+
repairWedgedTerminalSlot(deps, loop, scheduledFor, now);
|
|
8210
8356
|
return;
|
|
8357
|
+
}
|
|
8211
8358
|
deps.beforeRun?.(claim.loop, claim.run.scheduledFor);
|
|
8212
8359
|
deps.onRun?.(claim.run);
|
|
8213
8360
|
return claim;
|
|
@@ -8324,13 +8471,19 @@ class LoopsClient {
|
|
|
8324
8471
|
return this.store.requireLoop(idOrName);
|
|
8325
8472
|
}
|
|
8326
8473
|
pause(idOrName) {
|
|
8327
|
-
return this.store.updateLoop(this.
|
|
8474
|
+
return this.store.updateLoop(this.store.requireUniqueLoop(idOrName).id, { status: "paused" });
|
|
8328
8475
|
}
|
|
8329
8476
|
resume(idOrName) {
|
|
8330
|
-
|
|
8477
|
+
const loop = this.store.requireUniqueLoop(idOrName);
|
|
8478
|
+
let nextRunAt = loop.nextRunAt;
|
|
8479
|
+
if (!nextRunAt) {
|
|
8480
|
+
const now = new Date;
|
|
8481
|
+
nextRunAt = computeNextAfter(loop.schedule, now, now);
|
|
8482
|
+
}
|
|
8483
|
+
return this.store.updateLoop(loop.id, { status: "active", nextRunAt });
|
|
8331
8484
|
}
|
|
8332
8485
|
stop(idOrName) {
|
|
8333
|
-
return this.store.updateLoop(this.
|
|
8486
|
+
return this.store.updateLoop(this.store.requireUniqueLoop(idOrName).id, { status: "stopped", nextRunAt: undefined });
|
|
8334
8487
|
}
|
|
8335
8488
|
archive(idOrName) {
|
|
8336
8489
|
return this.store.archiveLoop(idOrName);
|
|
@@ -8339,7 +8492,7 @@ class LoopsClient {
|
|
|
8339
8492
|
return this.store.unarchiveLoop(idOrName);
|
|
8340
8493
|
}
|
|
8341
8494
|
delete(idOrName) {
|
|
8342
|
-
return this.store.deleteLoop(idOrName);
|
|
8495
|
+
return this.store.deleteLoop(this.store.requireUniqueLoop(idOrName).id);
|
|
8343
8496
|
}
|
|
8344
8497
|
runs(idOrName, filters = {}) {
|
|
8345
8498
|
let loopId;
|
|
@@ -8371,7 +8524,7 @@ class LoopsClient {
|
|
|
8371
8524
|
return tick({ store: this.store, runnerId: this.runnerId });
|
|
8372
8525
|
}
|
|
8373
8526
|
async runNow(idOrName) {
|
|
8374
|
-
const result = await runLoopNow({ store: this.store, idOrName, runnerId: this.runnerId });
|
|
8527
|
+
const result = await runLoopNow({ store: this.store, idOrName: this.store.requireUniqueLoop(idOrName).id, runnerId: this.runnerId });
|
|
8375
8528
|
return result.run;
|
|
8376
8529
|
}
|
|
8377
8530
|
exportBundle(opts = {}) {
|