@hasna/loops 0.4.5 → 0.4.7
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 +40 -0
- package/README.md +2 -2
- package/dist/api/index.js +19 -10
- package/dist/cli/index.js +613 -230
- package/dist/daemon/index.js +367 -170
- package/dist/index.js +413 -182
- package/dist/lib/executor.d.ts +14 -1
- package/dist/lib/mode.js +1 -1
- package/dist/lib/route/types.d.ts +6 -0
- package/dist/lib/storage/index.js +57 -5
- package/dist/lib/storage/sqlite.js +57 -5
- package/dist/lib/store.d.ts +7 -0
- package/dist/lib/store.js +57 -5
- package/dist/lib/template-kit.d.ts +6 -0
- package/dist/lib/templates.d.ts +10 -0
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.js +371 -173
- package/dist/runner/index.d.ts +3 -1
- package/dist/runner/index.js +180 -19
- package/dist/sdk/index.js +370 -174
- package/docs/USAGE.md +2 -2
- package/package.json +1 -1
package/dist/sdk/index.js
CHANGED
|
@@ -724,7 +724,7 @@ function buildAgentInvocation(target) {
|
|
|
724
724
|
if (target.model)
|
|
725
725
|
args.push("--model", target.model);
|
|
726
726
|
args.push(...target.extraArgs ?? []);
|
|
727
|
-
args.push("agent", "start");
|
|
727
|
+
args.push("agent", "start", "--json");
|
|
728
728
|
if (target.cwd)
|
|
729
729
|
args.push("--cwd", target.cwd);
|
|
730
730
|
args.push(target.prompt);
|
|
@@ -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;");
|
|
@@ -1948,9 +1951,12 @@ class Store {
|
|
|
1948
1951
|
const rows = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 2").all(idOrName);
|
|
1949
1952
|
if (rows.length === 0)
|
|
1950
1953
|
throw new LoopNotFoundError(idOrName);
|
|
1951
|
-
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)
|
|
1952
1958
|
throw new AmbiguousNameError(idOrName);
|
|
1953
|
-
return rowToLoop(
|
|
1959
|
+
return rowToLoop(active[0]);
|
|
1954
1960
|
}
|
|
1955
1961
|
requireLoop(idOrName) {
|
|
1956
1962
|
return this.getLoop(idOrName) ?? this.findLoopByName(idOrName) ?? (() => {
|
|
@@ -2291,6 +2297,7 @@ class Store {
|
|
|
2291
2297
|
return this.transact(() => {
|
|
2292
2298
|
const loop = this.requireLoop(idOrName);
|
|
2293
2299
|
this.setWorkflowWorkItemsForLoop(loop.id, "cancelled", "loop deleted", nowIso());
|
|
2300
|
+
this.db.query("DELETE FROM loop_runs WHERE loop_id = ?").run(loop.id);
|
|
2294
2301
|
const res = this.db.query("DELETE FROM loops WHERE id = ?").run(loop.id);
|
|
2295
2302
|
return res.changes > 0;
|
|
2296
2303
|
});
|
|
@@ -2775,7 +2782,10 @@ class Store {
|
|
|
2775
2782
|
return rowToGoal(row);
|
|
2776
2783
|
}
|
|
2777
2784
|
if (context.sourceType && context.sourceId) {
|
|
2778
|
-
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);
|
|
2779
2789
|
if (row)
|
|
2780
2790
|
return rowToGoal(row);
|
|
2781
2791
|
}
|
|
@@ -3195,6 +3205,41 @@ class Store {
|
|
|
3195
3205
|
throw new Error(`workflow step run not found after pid update: ${workflowRunId}/${stepId}`);
|
|
3196
3206
|
return run;
|
|
3197
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
|
+
}
|
|
3198
3243
|
recoverWorkflowRun(workflowRunId, reason = "workflow run recovered for retry") {
|
|
3199
3244
|
return this.transact(() => {
|
|
3200
3245
|
const now = nowIso();
|
|
@@ -3632,7 +3677,8 @@ class Store {
|
|
|
3632
3677
|
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
3633
3678
|
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
3634
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,
|
|
3635
|
-
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);
|
|
3636
3682
|
const run = this.getRun(id);
|
|
3637
3683
|
if (!run)
|
|
3638
3684
|
throw new Error(`run not found after finalize: ${id}`);
|
|
@@ -4160,12 +4206,18 @@ class Store {
|
|
|
4160
4206
|
}
|
|
4161
4207
|
close() {
|
|
4162
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
|
+
}
|
|
4163
4215
|
}
|
|
4164
4216
|
}
|
|
4165
4217
|
// package.json
|
|
4166
4218
|
var package_default = {
|
|
4167
4219
|
name: "@hasna/loops",
|
|
4168
|
-
version: "0.4.
|
|
4220
|
+
version: "0.4.7",
|
|
4169
4221
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4170
4222
|
type: "module",
|
|
4171
4223
|
main: "dist/index.js",
|
|
@@ -4623,10 +4675,7 @@ async function stopDaemon(opts = {}) {
|
|
|
4623
4675
|
const store = new Store(join4(dirname3(path), "loops.db"));
|
|
4624
4676
|
try {
|
|
4625
4677
|
const lease = store.getDaemonLease();
|
|
4626
|
-
|
|
4627
|
-
removePid(path);
|
|
4628
|
-
return { wasRunning: false, stopped: false, forced: false, pid: state.pid };
|
|
4629
|
-
}
|
|
4678
|
+
const leaseVerified = Boolean(lease && lease.pid === state.pid && new Date(lease.expiresAt).getTime() > Date.now());
|
|
4630
4679
|
try {
|
|
4631
4680
|
process.kill(state.pid, "SIGTERM");
|
|
4632
4681
|
} catch {
|
|
@@ -4646,11 +4695,14 @@ async function stopDaemon(opts = {}) {
|
|
|
4646
4695
|
} catch {}
|
|
4647
4696
|
await sleep(150);
|
|
4648
4697
|
removePid(path);
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
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
|
+
}
|
|
4654
4706
|
return { wasRunning: true, stopped: !isAlive(state.pid, record?.startedAt), forced: true, pid: state.pid, reapedPgids };
|
|
4655
4707
|
} finally {
|
|
4656
4708
|
store.close();
|
|
@@ -4984,7 +5036,12 @@ function notifySpawn(pid, opts) {
|
|
|
4984
5036
|
if (!pid)
|
|
4985
5037
|
return;
|
|
4986
5038
|
opts.onSpawn?.(pid);
|
|
4987
|
-
|
|
5039
|
+
const startedMs = processStartTimeMs(pid);
|
|
5040
|
+
opts.onSpawnProcess?.({
|
|
5041
|
+
pid,
|
|
5042
|
+
pgid: pid,
|
|
5043
|
+
processStartedAt: startedMs !== undefined ? new Date(startedMs).toISOString() : nowIso()
|
|
5044
|
+
});
|
|
4988
5045
|
}
|
|
4989
5046
|
function shellQuote(value) {
|
|
4990
5047
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -5309,18 +5366,60 @@ function codewithAgentControlArgs(target, command, agentId) {
|
|
|
5309
5366
|
"agent",
|
|
5310
5367
|
command,
|
|
5311
5368
|
...command === "logs" ? ["--limit", "20"] : [],
|
|
5369
|
+
"--json",
|
|
5312
5370
|
agentId
|
|
5313
5371
|
];
|
|
5314
5372
|
}
|
|
5373
|
+
function extractFirstJsonObject(text) {
|
|
5374
|
+
let depth = 0;
|
|
5375
|
+
let start = -1;
|
|
5376
|
+
let inString = false;
|
|
5377
|
+
let escaped = false;
|
|
5378
|
+
for (let i = 0;i < text.length; i += 1) {
|
|
5379
|
+
const ch = text[i];
|
|
5380
|
+
if (inString) {
|
|
5381
|
+
if (escaped)
|
|
5382
|
+
escaped = false;
|
|
5383
|
+
else if (ch === "\\")
|
|
5384
|
+
escaped = true;
|
|
5385
|
+
else if (ch === '"')
|
|
5386
|
+
inString = false;
|
|
5387
|
+
continue;
|
|
5388
|
+
}
|
|
5389
|
+
if (ch === '"') {
|
|
5390
|
+
inString = true;
|
|
5391
|
+
} else if (ch === "{") {
|
|
5392
|
+
if (depth === 0)
|
|
5393
|
+
start = i;
|
|
5394
|
+
depth += 1;
|
|
5395
|
+
} else if (ch === "}") {
|
|
5396
|
+
if (depth > 0) {
|
|
5397
|
+
depth -= 1;
|
|
5398
|
+
if (depth === 0 && start !== -1)
|
|
5399
|
+
return text.slice(start, i + 1);
|
|
5400
|
+
}
|
|
5401
|
+
}
|
|
5402
|
+
}
|
|
5403
|
+
return;
|
|
5404
|
+
}
|
|
5315
5405
|
function parseJsonOutput(stdout, label) {
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5406
|
+
const raw = stdout || "{}";
|
|
5407
|
+
const candidates = [raw.trim()];
|
|
5408
|
+
const scanned = extractFirstJsonObject(raw);
|
|
5409
|
+
if (scanned && scanned !== raw.trim())
|
|
5410
|
+
candidates.push(scanned);
|
|
5411
|
+
let lastError;
|
|
5412
|
+
for (const candidate of candidates) {
|
|
5413
|
+
try {
|
|
5414
|
+
const value = JSON.parse(candidate);
|
|
5415
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5416
|
+
throw new Error("not an object");
|
|
5417
|
+
return value;
|
|
5418
|
+
} catch (error) {
|
|
5419
|
+
lastError = error;
|
|
5420
|
+
}
|
|
5323
5421
|
}
|
|
5422
|
+
throw new Error(`${label} did not return JSON${lastError instanceof Error ? `: ${lastError.message}` : ""}`);
|
|
5324
5423
|
}
|
|
5325
5424
|
function recordField(value, key) {
|
|
5326
5425
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -5339,6 +5438,13 @@ function numberField(value, key) {
|
|
|
5339
5438
|
function codewithAgentStatus(readJson) {
|
|
5340
5439
|
return stringField(recordField(readJson, "agent"), "status") ?? stringField(recordField(readJson, "statusSnapshot"), "status");
|
|
5341
5440
|
}
|
|
5441
|
+
function codewithAgentLastEventSeq(readJson) {
|
|
5442
|
+
const agentSeq = numberField(recordField(readJson, "agent"), "lastEventSeq");
|
|
5443
|
+
const snapshotSeq = numberField(recordField(readJson, "statusSnapshot"), "lastEventSeq");
|
|
5444
|
+
if (agentSeq !== undefined && snapshotSeq !== undefined)
|
|
5445
|
+
return Math.max(agentSeq, snapshotSeq);
|
|
5446
|
+
return agentSeq ?? snapshotSeq;
|
|
5447
|
+
}
|
|
5342
5448
|
function codewithAgentEvidence(startJson, readJson, logsJson) {
|
|
5343
5449
|
const agent = recordField(readJson, "agent") ?? recordField(startJson, "agent");
|
|
5344
5450
|
const statusSnapshot = recordField(readJson, "statusSnapshot");
|
|
@@ -5369,6 +5475,21 @@ function codewithAgentEvidence(startJson, readJson, logsJson) {
|
|
|
5369
5475
|
events
|
|
5370
5476
|
}, null, 2);
|
|
5371
5477
|
}
|
|
5478
|
+
function codewithAgentProgress(readJson) {
|
|
5479
|
+
const agent = recordField(readJson, "agent");
|
|
5480
|
+
const statusSnapshot = recordField(readJson, "statusSnapshot");
|
|
5481
|
+
return {
|
|
5482
|
+
provider: "codewith",
|
|
5483
|
+
agentId: stringField(agent, "agentId"),
|
|
5484
|
+
status: stringField(agent, "status") ?? stringField(statusSnapshot, "status"),
|
|
5485
|
+
summary: stringField(statusSnapshot, "summary"),
|
|
5486
|
+
statusReason: stringField(agent, "statusReason"),
|
|
5487
|
+
threadId: stringField(agent, "threadId"),
|
|
5488
|
+
rolloutPath: stringField(agent, "rolloutPath"),
|
|
5489
|
+
pid: numberField(agent, "pid"),
|
|
5490
|
+
lastEventSeq: codewithAgentLastEventSeq(readJson)
|
|
5491
|
+
};
|
|
5492
|
+
}
|
|
5372
5493
|
function sleep(ms) {
|
|
5373
5494
|
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
5374
5495
|
}
|
|
@@ -5403,6 +5524,7 @@ async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt)
|
|
|
5403
5524
|
if (!agentId) {
|
|
5404
5525
|
return failureResult(startedAt, "codewith agent start did not return agent.agentId", { stdout: start.stdout, stderr: stderr.value() });
|
|
5405
5526
|
}
|
|
5527
|
+
opts.onAgentProgress?.(codewithAgentProgress(startJson));
|
|
5406
5528
|
const stopAgent = async () => {
|
|
5407
5529
|
await spawnCapture(spec.command, codewithAgentControlArgs(target, "stop", agentId), {
|
|
5408
5530
|
cwd: spec.cwd,
|
|
@@ -5445,10 +5567,15 @@ async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt)
|
|
|
5445
5567
|
});
|
|
5446
5568
|
}
|
|
5447
5569
|
const status = codewithAgentStatus(lastReadJson);
|
|
5448
|
-
const fingerprint = JSON.stringify({
|
|
5570
|
+
const fingerprint = JSON.stringify({
|
|
5571
|
+
status,
|
|
5572
|
+
agentLastEventSeq: numberField(recordField(lastReadJson, "agent"), "lastEventSeq"),
|
|
5573
|
+
snapshot: recordField(lastReadJson, "statusSnapshot") ?? null
|
|
5574
|
+
});
|
|
5449
5575
|
if (fingerprint !== lastFingerprint) {
|
|
5450
5576
|
lastFingerprint = fingerprint;
|
|
5451
5577
|
lastProgressAt = Date.now();
|
|
5578
|
+
opts.onAgentProgress?.(codewithAgentProgress(lastReadJson));
|
|
5452
5579
|
}
|
|
5453
5580
|
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
5454
5581
|
const logs = await spawnCapture(spec.command, codewithAgentControlArgs(target, "logs", agentId), {
|
|
@@ -6376,7 +6503,7 @@ ${failure.evidence.stderr}` : undefined
|
|
|
6376
6503
|
|
|
6377
6504
|
`);
|
|
6378
6505
|
const dedupeKey = `openloops:${loop.id}:${failure.fingerprint}`;
|
|
6379
|
-
const tags = ["bug", "openloops", "loop-health", failure.classification];
|
|
6506
|
+
const tags = ["bug", "openloops", "loops", "loop-health", failure.classification];
|
|
6380
6507
|
const priority = failure.classification === "auth" || failure.classification === "rate_limit" ? "high" : "medium";
|
|
6381
6508
|
return {
|
|
6382
6509
|
title,
|
|
@@ -6391,7 +6518,7 @@ ${failure.evidence.stderr}` : undefined
|
|
|
6391
6518
|
comment: ["todos", "comment", "<task-id>", description]
|
|
6392
6519
|
},
|
|
6393
6520
|
futureNativeUpsert: {
|
|
6394
|
-
command: "todos upsert",
|
|
6521
|
+
command: "todos task upsert",
|
|
6395
6522
|
fields: {
|
|
6396
6523
|
title,
|
|
6397
6524
|
description,
|
|
@@ -7595,173 +7722,213 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
7595
7722
|
if (run.status === "succeeded" || run.status === "failed" || run.status === "timed_out" || run.status === "cancelled") {
|
|
7596
7723
|
return workflowResult(run, run.status, startedAt, run.finishedAt ?? nowIso(), store.listWorkflowStepRuns(run.id), run.error);
|
|
7597
7724
|
}
|
|
7725
|
+
const resumedRunningSteps = store.listWorkflowStepRuns(run.id).filter((step) => step.status === "running");
|
|
7726
|
+
if (resumedRunningSteps.length > 0 && resumedRunningSteps.every((step) => step.pid !== undefined)) {
|
|
7727
|
+
try {
|
|
7728
|
+
store.recoverWorkflowRun(run.id, "workflow run resumed after lease takeover");
|
|
7729
|
+
} catch {}
|
|
7730
|
+
}
|
|
7598
7731
|
const ordered = workflowExecutionOrder(workflow);
|
|
7599
7732
|
const byId = new Map(workflow.steps.map((step) => [step.id, step]));
|
|
7600
7733
|
let blockingError;
|
|
7601
7734
|
let terminalStatus = "succeeded";
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
|
|
7618
|
-
const
|
|
7619
|
-
|
|
7620
|
-
|
|
7621
|
-
|
|
7622
|
-
|
|
7623
|
-
|
|
7624
|
-
|
|
7625
|
-
|
|
7626
|
-
|
|
7627
|
-
store.
|
|
7628
|
-
|
|
7629
|
-
|
|
7735
|
+
try {
|
|
7736
|
+
for (const step of ordered) {
|
|
7737
|
+
if (store.isWorkflowRunTerminal(run.id)) {
|
|
7738
|
+
terminalStatus = store.requireWorkflowRun(run.id).status;
|
|
7739
|
+
blockingError = "workflow run was cancelled";
|
|
7740
|
+
break;
|
|
7741
|
+
}
|
|
7742
|
+
const pendingTimeout = opts.signal?.aborted ? opts.signalTimeoutMessage?.() : undefined;
|
|
7743
|
+
if (pendingTimeout) {
|
|
7744
|
+
terminalStatus = "timed_out";
|
|
7745
|
+
blockingError = pendingTimeout;
|
|
7746
|
+
break;
|
|
7747
|
+
}
|
|
7748
|
+
const existing = store.getWorkflowStepRun(run.id, step.id);
|
|
7749
|
+
if (existing?.status === "succeeded" || existing?.status === "skipped" || existing?.status === "cancelled")
|
|
7750
|
+
continue;
|
|
7751
|
+
const blockedBy = (step.dependsOn ?? []).find((dependencyId) => {
|
|
7752
|
+
const dependencyRun = store.getWorkflowStepRun(run.id, dependencyId);
|
|
7753
|
+
const dependencyStep = byId.get(dependencyId);
|
|
7754
|
+
if (dependencyRun?.status === "succeeded")
|
|
7755
|
+
return false;
|
|
7756
|
+
return !dependencyStep?.continueOnFailure;
|
|
7757
|
+
});
|
|
7758
|
+
if (blockedBy) {
|
|
7759
|
+
opts.beforePersist?.();
|
|
7760
|
+
if (isBlockedStepRun(store.getWorkflowStepRun(run.id, blockedBy))) {
|
|
7761
|
+
store.skipWorkflowStepRun(run.id, step.id, `${BLOCKED_STEP_ERROR_PREFIX} upstream step ${blockedBy} was blocked`, {
|
|
7762
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
7763
|
+
});
|
|
7764
|
+
continue;
|
|
7765
|
+
}
|
|
7766
|
+
store.skipWorkflowStepRun(run.id, step.id, `dependency did not succeed: ${blockedBy}`, { daemonLeaseId: opts.daemonLeaseId });
|
|
7767
|
+
blockingError ??= `step ${step.id} blocked by dependency ${blockedBy}`;
|
|
7768
|
+
terminalStatus = "failed";
|
|
7630
7769
|
continue;
|
|
7631
7770
|
}
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
});
|
|
7652
|
-
let result;
|
|
7653
|
-
const controller = new AbortController;
|
|
7654
|
-
const externalAbort = () => controller.abort();
|
|
7655
|
-
if (opts.signal?.aborted)
|
|
7656
|
-
controller.abort();
|
|
7657
|
-
opts.signal?.addEventListener("abort", externalAbort, { once: true });
|
|
7658
|
-
const cancelTimer = setInterval(() => {
|
|
7659
|
-
if (store.getWorkflowRun(run.id)?.status === "cancelled")
|
|
7771
|
+
opts.beforePersist?.();
|
|
7772
|
+
const startedStep = store.startWorkflowStepRun(run.id, step.id, { daemonLeaseId: opts.daemonLeaseId });
|
|
7773
|
+
if (startedStep.status !== "running") {
|
|
7774
|
+
terminalStatus = "failed";
|
|
7775
|
+
blockingError = `step ${step.id} could not start because workflow is no longer running`;
|
|
7776
|
+
break;
|
|
7777
|
+
}
|
|
7778
|
+
const stepContext = goalExecutionContext({
|
|
7779
|
+
loop: opts.loop,
|
|
7780
|
+
loopRun: opts.loopRun,
|
|
7781
|
+
scheduledFor: opts.scheduledFor,
|
|
7782
|
+
workflow,
|
|
7783
|
+
workflowRunId: run.id,
|
|
7784
|
+
workflowStepId: step.id
|
|
7785
|
+
});
|
|
7786
|
+
let result;
|
|
7787
|
+
const controller = new AbortController;
|
|
7788
|
+
const externalAbort = () => controller.abort();
|
|
7789
|
+
if (opts.signal?.aborted)
|
|
7660
7790
|
controller.abort();
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
opts
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7791
|
+
opts.signal?.addEventListener("abort", externalAbort, { once: true });
|
|
7792
|
+
const cancelTimer = setInterval(() => {
|
|
7793
|
+
if (store.getWorkflowRun(run.id)?.status === "cancelled")
|
|
7794
|
+
controller.abort();
|
|
7795
|
+
}, opts.cancelPollMs ?? 500);
|
|
7796
|
+
cancelTimer.unref();
|
|
7797
|
+
try {
|
|
7798
|
+
if (step.goal) {
|
|
7799
|
+
result = await runGoal(store, step.goal, {
|
|
7800
|
+
...opts,
|
|
7801
|
+
model: opts.goalModel,
|
|
7802
|
+
target: targetWithStepAccount(step),
|
|
7803
|
+
signal: controller.signal,
|
|
7804
|
+
context: stepContext
|
|
7805
|
+
});
|
|
7806
|
+
} else {
|
|
7807
|
+
result = await executeTarget(targetWithStepAccount(step, opts.goalNodePrompt), executionMetadata(stepContext), {
|
|
7808
|
+
...opts,
|
|
7809
|
+
machine: opts.machine ?? opts.loop?.machine,
|
|
7810
|
+
signal: controller.signal,
|
|
7811
|
+
onAgentProgress: (progress) => {
|
|
7812
|
+
const stdout = JSON.stringify({ agentProgress: progress }, null, 2);
|
|
7813
|
+
opts.beforePersist?.();
|
|
7814
|
+
store.recordWorkflowStepProgress(run.id, step.id, {
|
|
7815
|
+
stdout,
|
|
7816
|
+
payload: progress
|
|
7817
|
+
}, {
|
|
7818
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
7819
|
+
});
|
|
7820
|
+
opts.onAgentProgress?.(progress);
|
|
7821
|
+
},
|
|
7822
|
+
onSpawn: (pid) => {
|
|
7823
|
+
opts.beforePersist?.();
|
|
7824
|
+
store.markWorkflowStepPid(run.id, step.id, pid, { daemonLeaseId: opts.daemonLeaseId });
|
|
7825
|
+
opts.onSpawn?.(pid);
|
|
7826
|
+
}
|
|
7827
|
+
});
|
|
7828
|
+
}
|
|
7829
|
+
} catch (error) {
|
|
7830
|
+
const finishedAt2 = nowIso();
|
|
7831
|
+
result = {
|
|
7832
|
+
status: "failed",
|
|
7833
|
+
stdout: "",
|
|
7834
|
+
stderr: "",
|
|
7835
|
+
error: error instanceof Error ? error.message : String(error),
|
|
7836
|
+
startedAt: startedStep.startedAt ?? finishedAt2,
|
|
7837
|
+
finishedAt: finishedAt2,
|
|
7838
|
+
durationMs: new Date(finishedAt2).getTime() - new Date(startedStep.startedAt ?? finishedAt2).getTime()
|
|
7839
|
+
};
|
|
7840
|
+
} finally {
|
|
7841
|
+
clearInterval(cancelTimer);
|
|
7842
|
+
opts.signal?.removeEventListener("abort", externalAbort);
|
|
7843
|
+
}
|
|
7844
|
+
const timeoutMessage = opts.signal?.aborted ? opts.signalTimeoutMessage?.() : undefined;
|
|
7845
|
+
if (timeoutMessage && result.status === "failed") {
|
|
7846
|
+
result = { ...result, status: "timed_out", error: timeoutMessage };
|
|
7847
|
+
}
|
|
7848
|
+
if (store.isWorkflowRunTerminal(run.id)) {
|
|
7849
|
+
terminalStatus = store.requireWorkflowRun(run.id).status;
|
|
7850
|
+
blockingError = "workflow run was cancelled";
|
|
7851
|
+
break;
|
|
7852
|
+
}
|
|
7853
|
+
opts.beforePersist?.();
|
|
7854
|
+
const blockedExit = result.status === "failed" && result.exitCode !== undefined && blockedExitCodesForStep(step).includes(result.exitCode);
|
|
7855
|
+
if (blockedExit) {
|
|
7856
|
+
store.finalizeWorkflowStepRun(run.id, step.id, {
|
|
7857
|
+
status: "skipped",
|
|
7858
|
+
finishedAt: result.finishedAt,
|
|
7859
|
+
durationMs: result.durationMs,
|
|
7860
|
+
stdout: result.stdout,
|
|
7861
|
+
stderr: result.stderr,
|
|
7862
|
+
exitCode: result.exitCode,
|
|
7863
|
+
error: `${BLOCKED_STEP_ERROR_PREFIX} step ${step.id} exited with blocked exit code ${result.exitCode}`
|
|
7864
|
+
}, {
|
|
7865
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
7682
7866
|
});
|
|
7867
|
+
continue;
|
|
7683
7868
|
}
|
|
7684
|
-
} catch (error) {
|
|
7685
|
-
const finishedAt2 = nowIso();
|
|
7686
|
-
result = {
|
|
7687
|
-
status: "failed",
|
|
7688
|
-
stdout: "",
|
|
7689
|
-
stderr: "",
|
|
7690
|
-
error: error instanceof Error ? error.message : String(error),
|
|
7691
|
-
startedAt: startedStep.startedAt ?? finishedAt2,
|
|
7692
|
-
finishedAt: finishedAt2,
|
|
7693
|
-
durationMs: new Date(finishedAt2).getTime() - new Date(startedStep.startedAt ?? finishedAt2).getTime()
|
|
7694
|
-
};
|
|
7695
|
-
} finally {
|
|
7696
|
-
clearInterval(cancelTimer);
|
|
7697
|
-
opts.signal?.removeEventListener("abort", externalAbort);
|
|
7698
|
-
}
|
|
7699
|
-
const timeoutMessage = opts.signal?.aborted ? opts.signalTimeoutMessage?.() : undefined;
|
|
7700
|
-
if (timeoutMessage && result.status === "failed") {
|
|
7701
|
-
result = { ...result, status: "timed_out", error: timeoutMessage };
|
|
7702
|
-
}
|
|
7703
|
-
if (store.isWorkflowRunTerminal(run.id)) {
|
|
7704
|
-
terminalStatus = store.requireWorkflowRun(run.id).status;
|
|
7705
|
-
blockingError = "workflow run was cancelled";
|
|
7706
|
-
break;
|
|
7707
|
-
}
|
|
7708
|
-
opts.beforePersist?.();
|
|
7709
|
-
const blockedExit = result.status === "failed" && result.exitCode !== undefined && blockedExitCodesForStep(step).includes(result.exitCode);
|
|
7710
|
-
if (blockedExit) {
|
|
7711
7869
|
store.finalizeWorkflowStepRun(run.id, step.id, {
|
|
7712
|
-
status:
|
|
7870
|
+
status: result.status,
|
|
7713
7871
|
finishedAt: result.finishedAt,
|
|
7714
7872
|
durationMs: result.durationMs,
|
|
7715
7873
|
stdout: result.stdout,
|
|
7716
7874
|
stderr: result.stderr,
|
|
7717
7875
|
exitCode: result.exitCode,
|
|
7718
|
-
error:
|
|
7876
|
+
error: result.error
|
|
7719
7877
|
}, {
|
|
7720
7878
|
daemonLeaseId: opts.daemonLeaseId
|
|
7721
7879
|
});
|
|
7722
|
-
|
|
7880
|
+
if (result.status !== "succeeded" && !step.continueOnFailure) {
|
|
7881
|
+
terminalStatus = result.status;
|
|
7882
|
+
blockingError = `step ${step.id} ${result.status}${result.error ? `: ${result.error}` : ""}`;
|
|
7883
|
+
break;
|
|
7884
|
+
}
|
|
7723
7885
|
}
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7886
|
+
if (terminalStatus !== "succeeded") {
|
|
7887
|
+
for (const step of ordered) {
|
|
7888
|
+
const existing = store.getWorkflowStepRun(run.id, step.id);
|
|
7889
|
+
if (existing?.status === "pending" || existing?.status === "running") {
|
|
7890
|
+
store.skipWorkflowStepRun(run.id, step.id, blockingError ?? "workflow stopped before step could run", {
|
|
7891
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
7892
|
+
});
|
|
7893
|
+
}
|
|
7894
|
+
}
|
|
7895
|
+
}
|
|
7896
|
+
const finishedAt = nowIso();
|
|
7897
|
+
if (store.isWorkflowRunTerminal(run.id)) {
|
|
7898
|
+
const terminalRun = store.requireWorkflowRun(run.id);
|
|
7899
|
+
return workflowResult(terminalRun, terminalRun.status, startedAt, terminalRun.finishedAt ?? finishedAt, store.listWorkflowStepRuns(run.id), terminalRun.error ?? blockingError);
|
|
7900
|
+
}
|
|
7901
|
+
opts.beforePersist?.();
|
|
7902
|
+
const finalRun = store.finalizeWorkflowRun(run.id, terminalStatus, {
|
|
7903
|
+
finishedAt,
|
|
7904
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime(),
|
|
7905
|
+
error: blockingError
|
|
7732
7906
|
}, {
|
|
7733
7907
|
daemonLeaseId: opts.daemonLeaseId
|
|
7734
7908
|
});
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
if (
|
|
7745
|
-
store.
|
|
7909
|
+
return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, store.listWorkflowStepRuns(run.id), blockingError);
|
|
7910
|
+
} catch (error) {
|
|
7911
|
+
if (opts.daemonLeaseId && error instanceof Error && error.message === "daemon lease lost")
|
|
7912
|
+
throw error;
|
|
7913
|
+
if (error instanceof Error && error.message.includes("workflow step is not claimable"))
|
|
7914
|
+
throw error;
|
|
7915
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7916
|
+
const finishedAt = nowIso();
|
|
7917
|
+
try {
|
|
7918
|
+
if (!store.isWorkflowRunTerminal(run.id)) {
|
|
7919
|
+
store.finalizeWorkflowRun(run.id, "failed", {
|
|
7920
|
+
finishedAt,
|
|
7921
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime(),
|
|
7922
|
+
error: message
|
|
7923
|
+
}, {
|
|
7746
7924
|
daemonLeaseId: opts.daemonLeaseId
|
|
7747
7925
|
});
|
|
7748
7926
|
}
|
|
7749
|
-
}
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
const terminalRun = store.requireWorkflowRun(run.id);
|
|
7754
|
-
return workflowResult(terminalRun, terminalRun.status, startedAt, terminalRun.finishedAt ?? finishedAt, store.listWorkflowStepRuns(run.id), terminalRun.error ?? blockingError);
|
|
7927
|
+
} catch {}
|
|
7928
|
+
const current = store.getWorkflowRun(run.id) ?? run;
|
|
7929
|
+
const resultStatus = current.status === "running" ? "failed" : current.status;
|
|
7930
|
+
return workflowResult(current, resultStatus, startedAt, finishedAt, store.listWorkflowStepRuns(run.id), current.error ?? message);
|
|
7755
7931
|
}
|
|
7756
|
-
opts.beforePersist?.();
|
|
7757
|
-
const finalRun = store.finalizeWorkflowRun(run.id, terminalStatus, {
|
|
7758
|
-
finishedAt,
|
|
7759
|
-
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime(),
|
|
7760
|
-
error: blockingError
|
|
7761
|
-
}, {
|
|
7762
|
-
daemonLeaseId: opts.daemonLeaseId
|
|
7763
|
-
});
|
|
7764
|
-
return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, store.listWorkflowStepRuns(run.id), blockingError);
|
|
7765
7932
|
}
|
|
7766
7933
|
function preflightWorkflow(workflow, opts = {}) {
|
|
7767
7934
|
return workflowExecutionOrder(workflow).map((step) => {
|
|
@@ -8132,6 +8299,25 @@ function advanceOptions(deps) {
|
|
|
8132
8299
|
onRun: deps.onRun
|
|
8133
8300
|
};
|
|
8134
8301
|
}
|
|
8302
|
+
var TERMINAL_RUN_STATUSES2 = new Set([
|
|
8303
|
+
"succeeded",
|
|
8304
|
+
"failed",
|
|
8305
|
+
"timed_out",
|
|
8306
|
+
"abandoned",
|
|
8307
|
+
"skipped"
|
|
8308
|
+
]);
|
|
8309
|
+
function repairWedgedTerminalSlot(deps, loop, scheduledFor, now) {
|
|
8310
|
+
const existing = deps.store.getRunBySlot(loop.id, scheduledFor);
|
|
8311
|
+
if (!existing || !TERMINAL_RUN_STATUSES2.has(existing.status))
|
|
8312
|
+
return;
|
|
8313
|
+
try {
|
|
8314
|
+
advanceLoop(deps.store, loop, existing, new Date(existing.finishedAt ?? now), existing.status === "succeeded", advanceOptions(deps));
|
|
8315
|
+
} catch (error) {
|
|
8316
|
+
if (deps.daemonLeaseId && isDaemonLeaseLost(error))
|
|
8317
|
+
return;
|
|
8318
|
+
throw error;
|
|
8319
|
+
}
|
|
8320
|
+
}
|
|
8135
8321
|
async function runSlot(deps, loop, scheduledFor) {
|
|
8136
8322
|
const now = deps.now?.() ?? new Date;
|
|
8137
8323
|
deps.beforeRun?.(loop, scheduledFor);
|
|
@@ -8158,8 +8344,10 @@ async function runSlot(deps, loop, scheduledFor) {
|
|
|
8158
8344
|
return;
|
|
8159
8345
|
throw error;
|
|
8160
8346
|
}
|
|
8161
|
-
if (!claim)
|
|
8347
|
+
if (!claim) {
|
|
8348
|
+
repairWedgedTerminalSlot(deps, loop, scheduledFor, now);
|
|
8162
8349
|
return;
|
|
8350
|
+
}
|
|
8163
8351
|
deps.beforeRun?.(claim.loop, claim.run.scheduledFor);
|
|
8164
8352
|
deps.onRun?.(claim.run);
|
|
8165
8353
|
const finalRun = await executeClaimedRun({
|
|
@@ -8205,8 +8393,10 @@ function claimSlot(deps, loop, scheduledFor) {
|
|
|
8205
8393
|
return;
|
|
8206
8394
|
throw error;
|
|
8207
8395
|
}
|
|
8208
|
-
if (!claim)
|
|
8396
|
+
if (!claim) {
|
|
8397
|
+
repairWedgedTerminalSlot(deps, loop, scheduledFor, now);
|
|
8209
8398
|
return;
|
|
8399
|
+
}
|
|
8210
8400
|
deps.beforeRun?.(claim.loop, claim.run.scheduledFor);
|
|
8211
8401
|
deps.onRun?.(claim.run);
|
|
8212
8402
|
return claim;
|
|
@@ -8323,13 +8513,19 @@ class LoopsClient {
|
|
|
8323
8513
|
return this.store.requireLoop(idOrName);
|
|
8324
8514
|
}
|
|
8325
8515
|
pause(idOrName) {
|
|
8326
|
-
return this.store.updateLoop(this.
|
|
8516
|
+
return this.store.updateLoop(this.store.requireUniqueLoop(idOrName).id, { status: "paused" });
|
|
8327
8517
|
}
|
|
8328
8518
|
resume(idOrName) {
|
|
8329
|
-
|
|
8519
|
+
const loop = this.store.requireUniqueLoop(idOrName);
|
|
8520
|
+
let nextRunAt = loop.nextRunAt;
|
|
8521
|
+
if (!nextRunAt) {
|
|
8522
|
+
const now = new Date;
|
|
8523
|
+
nextRunAt = computeNextAfter(loop.schedule, now, now);
|
|
8524
|
+
}
|
|
8525
|
+
return this.store.updateLoop(loop.id, { status: "active", nextRunAt });
|
|
8330
8526
|
}
|
|
8331
8527
|
stop(idOrName) {
|
|
8332
|
-
return this.store.updateLoop(this.
|
|
8528
|
+
return this.store.updateLoop(this.store.requireUniqueLoop(idOrName).id, { status: "stopped", nextRunAt: undefined });
|
|
8333
8529
|
}
|
|
8334
8530
|
archive(idOrName) {
|
|
8335
8531
|
return this.store.archiveLoop(idOrName);
|
|
@@ -8338,7 +8534,7 @@ class LoopsClient {
|
|
|
8338
8534
|
return this.store.unarchiveLoop(idOrName);
|
|
8339
8535
|
}
|
|
8340
8536
|
delete(idOrName) {
|
|
8341
|
-
return this.store.deleteLoop(idOrName);
|
|
8537
|
+
return this.store.deleteLoop(this.store.requireUniqueLoop(idOrName).id);
|
|
8342
8538
|
}
|
|
8343
8539
|
runs(idOrName, filters = {}) {
|
|
8344
8540
|
let loopId;
|
|
@@ -8370,7 +8566,7 @@ class LoopsClient {
|
|
|
8370
8566
|
return tick({ store: this.store, runnerId: this.runnerId });
|
|
8371
8567
|
}
|
|
8372
8568
|
async runNow(idOrName) {
|
|
8373
|
-
const result = await runLoopNow({ store: this.store, idOrName, runnerId: this.runnerId });
|
|
8569
|
+
const result = await runLoopNow({ store: this.store, idOrName: this.store.requireUniqueLoop(idOrName).id, runnerId: this.runnerId });
|
|
8374
8570
|
return result.run;
|
|
8375
8571
|
}
|
|
8376
8572
|
exportBundle(opts = {}) {
|