@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/daemon/index.js
CHANGED
|
@@ -726,7 +726,7 @@ function buildAgentInvocation(target) {
|
|
|
726
726
|
if (target.model)
|
|
727
727
|
args.push("--model", target.model);
|
|
728
728
|
args.push(...target.extraArgs ?? []);
|
|
729
|
-
args.push("agent", "start");
|
|
729
|
+
args.push("agent", "start", "--json");
|
|
730
730
|
if (target.cwd)
|
|
731
731
|
args.push("--cwd", target.cwd);
|
|
732
732
|
args.push(target.prompt);
|
|
@@ -1493,11 +1493,14 @@ function ensurePrivateStorePath(file) {
|
|
|
1493
1493
|
class Store {
|
|
1494
1494
|
db;
|
|
1495
1495
|
rootDir;
|
|
1496
|
+
memoryRootDir;
|
|
1496
1497
|
constructor(path) {
|
|
1497
1498
|
const file = path ?? dbPath();
|
|
1498
1499
|
if (file !== ":memory:")
|
|
1499
1500
|
ensurePrivateStorePath(file);
|
|
1500
1501
|
this.rootDir = file === ":memory:" ? mkdtempSync(join3(tmpdir(), "open-loops-store-")) : dirname2(file);
|
|
1502
|
+
if (file === ":memory:")
|
|
1503
|
+
this.memoryRootDir = this.rootDir;
|
|
1501
1504
|
this.db = new Database(file);
|
|
1502
1505
|
this.db.exec("PRAGMA foreign_keys = ON;");
|
|
1503
1506
|
this.db.exec("PRAGMA busy_timeout = 5000;");
|
|
@@ -1950,9 +1953,12 @@ class Store {
|
|
|
1950
1953
|
const rows = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 2").all(idOrName);
|
|
1951
1954
|
if (rows.length === 0)
|
|
1952
1955
|
throw new LoopNotFoundError(idOrName);
|
|
1953
|
-
if (rows.length
|
|
1956
|
+
if (rows.length === 1)
|
|
1957
|
+
return rowToLoop(rows[0]);
|
|
1958
|
+
const active = this.db.query("SELECT * FROM loops WHERE name = ? AND archived_at IS NULL ORDER BY created_at DESC LIMIT 2").all(idOrName);
|
|
1959
|
+
if (active.length !== 1)
|
|
1954
1960
|
throw new AmbiguousNameError(idOrName);
|
|
1955
|
-
return rowToLoop(
|
|
1961
|
+
return rowToLoop(active[0]);
|
|
1956
1962
|
}
|
|
1957
1963
|
requireLoop(idOrName) {
|
|
1958
1964
|
return this.getLoop(idOrName) ?? this.findLoopByName(idOrName) ?? (() => {
|
|
@@ -2293,6 +2299,7 @@ class Store {
|
|
|
2293
2299
|
return this.transact(() => {
|
|
2294
2300
|
const loop = this.requireLoop(idOrName);
|
|
2295
2301
|
this.setWorkflowWorkItemsForLoop(loop.id, "cancelled", "loop deleted", nowIso());
|
|
2302
|
+
this.db.query("DELETE FROM loop_runs WHERE loop_id = ?").run(loop.id);
|
|
2296
2303
|
const res = this.db.query("DELETE FROM loops WHERE id = ?").run(loop.id);
|
|
2297
2304
|
return res.changes > 0;
|
|
2298
2305
|
});
|
|
@@ -2777,7 +2784,10 @@ class Store {
|
|
|
2777
2784
|
return rowToGoal(row);
|
|
2778
2785
|
}
|
|
2779
2786
|
if (context.sourceType && context.sourceId) {
|
|
2780
|
-
const
|
|
2787
|
+
const terminalPlaceholders = GOAL_TERMINAL.map(() => "?").join(", ");
|
|
2788
|
+
const row = this.db.query(`SELECT * FROM goals
|
|
2789
|
+
WHERE source_type = ? AND source_id = ? AND status NOT IN (${terminalPlaceholders})
|
|
2790
|
+
ORDER BY created_at DESC LIMIT 1`).get(context.sourceType, context.sourceId, ...GOAL_TERMINAL);
|
|
2781
2791
|
if (row)
|
|
2782
2792
|
return rowToGoal(row);
|
|
2783
2793
|
}
|
|
@@ -3197,6 +3207,41 @@ class Store {
|
|
|
3197
3207
|
throw new Error(`workflow step run not found after pid update: ${workflowRunId}/${stepId}`);
|
|
3198
3208
|
return run;
|
|
3199
3209
|
}
|
|
3210
|
+
recordWorkflowStepProgress(workflowRunId, stepId, progress, opts = {}) {
|
|
3211
|
+
const now = (opts.now ?? new Date).toISOString();
|
|
3212
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
3213
|
+
try {
|
|
3214
|
+
const res = this.db.query(`UPDATE workflow_step_runs
|
|
3215
|
+
SET stdout=COALESCE($stdout, stdout),
|
|
3216
|
+
stderr=COALESCE($stderr, stderr),
|
|
3217
|
+
updated_at=$updated
|
|
3218
|
+
WHERE workflow_run_id=$workflowRunId AND step_id=$stepId AND status='running'
|
|
3219
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
3220
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
3221
|
+
))`).run({
|
|
3222
|
+
$workflowRunId: workflowRunId,
|
|
3223
|
+
$stepId: stepId,
|
|
3224
|
+
$stdout: progress.stdout === undefined ? null : scrubbedOrNull(progress.stdout),
|
|
3225
|
+
$stderr: progress.stderr === undefined ? null : scrubbedOrNull(progress.stderr),
|
|
3226
|
+
$updated: now,
|
|
3227
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
3228
|
+
$now: now
|
|
3229
|
+
});
|
|
3230
|
+
if (res.changes === 1) {
|
|
3231
|
+
this.appendWorkflowEvent(workflowRunId, "step_progress", stepId, progress.payload);
|
|
3232
|
+
}
|
|
3233
|
+
this.db.exec("COMMIT");
|
|
3234
|
+
} catch (error) {
|
|
3235
|
+
try {
|
|
3236
|
+
this.db.exec("ROLLBACK");
|
|
3237
|
+
} catch {}
|
|
3238
|
+
throw error;
|
|
3239
|
+
}
|
|
3240
|
+
const run = this.getWorkflowStepRun(workflowRunId, stepId);
|
|
3241
|
+
if (!run)
|
|
3242
|
+
throw new Error(`workflow step run not found after progress update: ${workflowRunId}/${stepId}`);
|
|
3243
|
+
return run;
|
|
3244
|
+
}
|
|
3200
3245
|
recoverWorkflowRun(workflowRunId, reason = "workflow run recovered for retry") {
|
|
3201
3246
|
return this.transact(() => {
|
|
3202
3247
|
const now = nowIso();
|
|
@@ -3634,7 +3679,8 @@ class Store {
|
|
|
3634
3679
|
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
3635
3680
|
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
3636
3681
|
))`).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,
|
|
3637
|
-
duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated
|
|
3682
|
+
duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated
|
|
3683
|
+
WHERE id=$id AND status='running'`).run(params);
|
|
3638
3684
|
const run = this.getRun(id);
|
|
3639
3685
|
if (!run)
|
|
3640
3686
|
throw new Error(`run not found after finalize: ${id}`);
|
|
@@ -4162,6 +4208,12 @@ class Store {
|
|
|
4162
4208
|
}
|
|
4163
4209
|
close() {
|
|
4164
4210
|
this.db.close();
|
|
4211
|
+
if (this.memoryRootDir) {
|
|
4212
|
+
try {
|
|
4213
|
+
rmSync2(this.memoryRootDir, { recursive: true, force: true });
|
|
4214
|
+
} catch {}
|
|
4215
|
+
this.memoryRootDir = undefined;
|
|
4216
|
+
}
|
|
4165
4217
|
}
|
|
4166
4218
|
}
|
|
4167
4219
|
|
|
@@ -4599,7 +4651,7 @@ ${failure.evidence.stderr}` : undefined
|
|
|
4599
4651
|
|
|
4600
4652
|
`);
|
|
4601
4653
|
const dedupeKey = `openloops:${loop.id}:${failure.fingerprint}`;
|
|
4602
|
-
const tags = ["bug", "openloops", "loop-health", failure.classification];
|
|
4654
|
+
const tags = ["bug", "openloops", "loops", "loop-health", failure.classification];
|
|
4603
4655
|
const priority = failure.classification === "auth" || failure.classification === "rate_limit" ? "high" : "medium";
|
|
4604
4656
|
return {
|
|
4605
4657
|
title,
|
|
@@ -4614,7 +4666,7 @@ ${failure.evidence.stderr}` : undefined
|
|
|
4614
4666
|
comment: ["todos", "comment", "<task-id>", description]
|
|
4615
4667
|
},
|
|
4616
4668
|
futureNativeUpsert: {
|
|
4617
|
-
command: "todos upsert",
|
|
4669
|
+
command: "todos task upsert",
|
|
4618
4670
|
fields: {
|
|
4619
4671
|
title,
|
|
4620
4672
|
description,
|
|
@@ -5043,7 +5095,12 @@ function notifySpawn(pid, opts) {
|
|
|
5043
5095
|
if (!pid)
|
|
5044
5096
|
return;
|
|
5045
5097
|
opts.onSpawn?.(pid);
|
|
5046
|
-
|
|
5098
|
+
const startedMs = processStartTimeMs(pid);
|
|
5099
|
+
opts.onSpawnProcess?.({
|
|
5100
|
+
pid,
|
|
5101
|
+
pgid: pid,
|
|
5102
|
+
processStartedAt: startedMs !== undefined ? new Date(startedMs).toISOString() : nowIso()
|
|
5103
|
+
});
|
|
5047
5104
|
}
|
|
5048
5105
|
function shellQuote(value) {
|
|
5049
5106
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -5368,18 +5425,60 @@ function codewithAgentControlArgs(target, command, agentId) {
|
|
|
5368
5425
|
"agent",
|
|
5369
5426
|
command,
|
|
5370
5427
|
...command === "logs" ? ["--limit", "20"] : [],
|
|
5428
|
+
"--json",
|
|
5371
5429
|
agentId
|
|
5372
5430
|
];
|
|
5373
5431
|
}
|
|
5432
|
+
function extractFirstJsonObject(text) {
|
|
5433
|
+
let depth = 0;
|
|
5434
|
+
let start = -1;
|
|
5435
|
+
let inString = false;
|
|
5436
|
+
let escaped = false;
|
|
5437
|
+
for (let i = 0;i < text.length; i += 1) {
|
|
5438
|
+
const ch = text[i];
|
|
5439
|
+
if (inString) {
|
|
5440
|
+
if (escaped)
|
|
5441
|
+
escaped = false;
|
|
5442
|
+
else if (ch === "\\")
|
|
5443
|
+
escaped = true;
|
|
5444
|
+
else if (ch === '"')
|
|
5445
|
+
inString = false;
|
|
5446
|
+
continue;
|
|
5447
|
+
}
|
|
5448
|
+
if (ch === '"') {
|
|
5449
|
+
inString = true;
|
|
5450
|
+
} else if (ch === "{") {
|
|
5451
|
+
if (depth === 0)
|
|
5452
|
+
start = i;
|
|
5453
|
+
depth += 1;
|
|
5454
|
+
} else if (ch === "}") {
|
|
5455
|
+
if (depth > 0) {
|
|
5456
|
+
depth -= 1;
|
|
5457
|
+
if (depth === 0 && start !== -1)
|
|
5458
|
+
return text.slice(start, i + 1);
|
|
5459
|
+
}
|
|
5460
|
+
}
|
|
5461
|
+
}
|
|
5462
|
+
return;
|
|
5463
|
+
}
|
|
5374
5464
|
function parseJsonOutput(stdout, label) {
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5465
|
+
const raw = stdout || "{}";
|
|
5466
|
+
const candidates = [raw.trim()];
|
|
5467
|
+
const scanned = extractFirstJsonObject(raw);
|
|
5468
|
+
if (scanned && scanned !== raw.trim())
|
|
5469
|
+
candidates.push(scanned);
|
|
5470
|
+
let lastError;
|
|
5471
|
+
for (const candidate of candidates) {
|
|
5472
|
+
try {
|
|
5473
|
+
const value = JSON.parse(candidate);
|
|
5474
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5475
|
+
throw new Error("not an object");
|
|
5476
|
+
return value;
|
|
5477
|
+
} catch (error) {
|
|
5478
|
+
lastError = error;
|
|
5479
|
+
}
|
|
5382
5480
|
}
|
|
5481
|
+
throw new Error(`${label} did not return JSON${lastError instanceof Error ? `: ${lastError.message}` : ""}`);
|
|
5383
5482
|
}
|
|
5384
5483
|
function recordField(value, key) {
|
|
5385
5484
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -5398,6 +5497,13 @@ function numberField(value, key) {
|
|
|
5398
5497
|
function codewithAgentStatus(readJson) {
|
|
5399
5498
|
return stringField(recordField(readJson, "agent"), "status") ?? stringField(recordField(readJson, "statusSnapshot"), "status");
|
|
5400
5499
|
}
|
|
5500
|
+
function codewithAgentLastEventSeq(readJson) {
|
|
5501
|
+
const agentSeq = numberField(recordField(readJson, "agent"), "lastEventSeq");
|
|
5502
|
+
const snapshotSeq = numberField(recordField(readJson, "statusSnapshot"), "lastEventSeq");
|
|
5503
|
+
if (agentSeq !== undefined && snapshotSeq !== undefined)
|
|
5504
|
+
return Math.max(agentSeq, snapshotSeq);
|
|
5505
|
+
return agentSeq ?? snapshotSeq;
|
|
5506
|
+
}
|
|
5401
5507
|
function codewithAgentEvidence(startJson, readJson, logsJson) {
|
|
5402
5508
|
const agent = recordField(readJson, "agent") ?? recordField(startJson, "agent");
|
|
5403
5509
|
const statusSnapshot = recordField(readJson, "statusSnapshot");
|
|
@@ -5428,6 +5534,21 @@ function codewithAgentEvidence(startJson, readJson, logsJson) {
|
|
|
5428
5534
|
events
|
|
5429
5535
|
}, null, 2);
|
|
5430
5536
|
}
|
|
5537
|
+
function codewithAgentProgress(readJson) {
|
|
5538
|
+
const agent = recordField(readJson, "agent");
|
|
5539
|
+
const statusSnapshot = recordField(readJson, "statusSnapshot");
|
|
5540
|
+
return {
|
|
5541
|
+
provider: "codewith",
|
|
5542
|
+
agentId: stringField(agent, "agentId"),
|
|
5543
|
+
status: stringField(agent, "status") ?? stringField(statusSnapshot, "status"),
|
|
5544
|
+
summary: stringField(statusSnapshot, "summary"),
|
|
5545
|
+
statusReason: stringField(agent, "statusReason"),
|
|
5546
|
+
threadId: stringField(agent, "threadId"),
|
|
5547
|
+
rolloutPath: stringField(agent, "rolloutPath"),
|
|
5548
|
+
pid: numberField(agent, "pid"),
|
|
5549
|
+
lastEventSeq: codewithAgentLastEventSeq(readJson)
|
|
5550
|
+
};
|
|
5551
|
+
}
|
|
5431
5552
|
function sleep(ms) {
|
|
5432
5553
|
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
5433
5554
|
}
|
|
@@ -5462,6 +5583,7 @@ async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt)
|
|
|
5462
5583
|
if (!agentId) {
|
|
5463
5584
|
return failureResult(startedAt, "codewith agent start did not return agent.agentId", { stdout: start.stdout, stderr: stderr.value() });
|
|
5464
5585
|
}
|
|
5586
|
+
opts.onAgentProgress?.(codewithAgentProgress(startJson));
|
|
5465
5587
|
const stopAgent = async () => {
|
|
5466
5588
|
await spawnCapture(spec.command, codewithAgentControlArgs(target, "stop", agentId), {
|
|
5467
5589
|
cwd: spec.cwd,
|
|
@@ -5504,10 +5626,15 @@ async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt)
|
|
|
5504
5626
|
});
|
|
5505
5627
|
}
|
|
5506
5628
|
const status = codewithAgentStatus(lastReadJson);
|
|
5507
|
-
const fingerprint = JSON.stringify({
|
|
5629
|
+
const fingerprint = JSON.stringify({
|
|
5630
|
+
status,
|
|
5631
|
+
agentLastEventSeq: numberField(recordField(lastReadJson, "agent"), "lastEventSeq"),
|
|
5632
|
+
snapshot: recordField(lastReadJson, "statusSnapshot") ?? null
|
|
5633
|
+
});
|
|
5508
5634
|
if (fingerprint !== lastFingerprint) {
|
|
5509
5635
|
lastFingerprint = fingerprint;
|
|
5510
5636
|
lastProgressAt = Date.now();
|
|
5637
|
+
opts.onAgentProgress?.(codewithAgentProgress(lastReadJson));
|
|
5511
5638
|
}
|
|
5512
5639
|
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
5513
5640
|
const logs = await spawnCapture(spec.command, codewithAgentControlArgs(target, "logs", agentId), {
|
|
@@ -6508,173 +6635,213 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
6508
6635
|
if (run.status === "succeeded" || run.status === "failed" || run.status === "timed_out" || run.status === "cancelled") {
|
|
6509
6636
|
return workflowResult(run, run.status, startedAt, run.finishedAt ?? nowIso(), store.listWorkflowStepRuns(run.id), run.error);
|
|
6510
6637
|
}
|
|
6638
|
+
const resumedRunningSteps = store.listWorkflowStepRuns(run.id).filter((step) => step.status === "running");
|
|
6639
|
+
if (resumedRunningSteps.length > 0 && resumedRunningSteps.every((step) => step.pid !== undefined)) {
|
|
6640
|
+
try {
|
|
6641
|
+
store.recoverWorkflowRun(run.id, "workflow run resumed after lease takeover");
|
|
6642
|
+
} catch {}
|
|
6643
|
+
}
|
|
6511
6644
|
const ordered = workflowExecutionOrder(workflow);
|
|
6512
6645
|
const byId = new Map(workflow.steps.map((step) => [step.id, step]));
|
|
6513
6646
|
let blockingError;
|
|
6514
6647
|
let terminalStatus = "succeeded";
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
const
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
store.
|
|
6541
|
-
|
|
6542
|
-
|
|
6648
|
+
try {
|
|
6649
|
+
for (const step of ordered) {
|
|
6650
|
+
if (store.isWorkflowRunTerminal(run.id)) {
|
|
6651
|
+
terminalStatus = store.requireWorkflowRun(run.id).status;
|
|
6652
|
+
blockingError = "workflow run was cancelled";
|
|
6653
|
+
break;
|
|
6654
|
+
}
|
|
6655
|
+
const pendingTimeout = opts.signal?.aborted ? opts.signalTimeoutMessage?.() : undefined;
|
|
6656
|
+
if (pendingTimeout) {
|
|
6657
|
+
terminalStatus = "timed_out";
|
|
6658
|
+
blockingError = pendingTimeout;
|
|
6659
|
+
break;
|
|
6660
|
+
}
|
|
6661
|
+
const existing = store.getWorkflowStepRun(run.id, step.id);
|
|
6662
|
+
if (existing?.status === "succeeded" || existing?.status === "skipped" || existing?.status === "cancelled")
|
|
6663
|
+
continue;
|
|
6664
|
+
const blockedBy = (step.dependsOn ?? []).find((dependencyId) => {
|
|
6665
|
+
const dependencyRun = store.getWorkflowStepRun(run.id, dependencyId);
|
|
6666
|
+
const dependencyStep = byId.get(dependencyId);
|
|
6667
|
+
if (dependencyRun?.status === "succeeded")
|
|
6668
|
+
return false;
|
|
6669
|
+
return !dependencyStep?.continueOnFailure;
|
|
6670
|
+
});
|
|
6671
|
+
if (blockedBy) {
|
|
6672
|
+
opts.beforePersist?.();
|
|
6673
|
+
if (isBlockedStepRun(store.getWorkflowStepRun(run.id, blockedBy))) {
|
|
6674
|
+
store.skipWorkflowStepRun(run.id, step.id, `${BLOCKED_STEP_ERROR_PREFIX} upstream step ${blockedBy} was blocked`, {
|
|
6675
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
6676
|
+
});
|
|
6677
|
+
continue;
|
|
6678
|
+
}
|
|
6679
|
+
store.skipWorkflowStepRun(run.id, step.id, `dependency did not succeed: ${blockedBy}`, { daemonLeaseId: opts.daemonLeaseId });
|
|
6680
|
+
blockingError ??= `step ${step.id} blocked by dependency ${blockedBy}`;
|
|
6681
|
+
terminalStatus = "failed";
|
|
6543
6682
|
continue;
|
|
6544
6683
|
}
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
});
|
|
6565
|
-
let result;
|
|
6566
|
-
const controller = new AbortController;
|
|
6567
|
-
const externalAbort = () => controller.abort();
|
|
6568
|
-
if (opts.signal?.aborted)
|
|
6569
|
-
controller.abort();
|
|
6570
|
-
opts.signal?.addEventListener("abort", externalAbort, { once: true });
|
|
6571
|
-
const cancelTimer = setInterval(() => {
|
|
6572
|
-
if (store.getWorkflowRun(run.id)?.status === "cancelled")
|
|
6684
|
+
opts.beforePersist?.();
|
|
6685
|
+
const startedStep = store.startWorkflowStepRun(run.id, step.id, { daemonLeaseId: opts.daemonLeaseId });
|
|
6686
|
+
if (startedStep.status !== "running") {
|
|
6687
|
+
terminalStatus = "failed";
|
|
6688
|
+
blockingError = `step ${step.id} could not start because workflow is no longer running`;
|
|
6689
|
+
break;
|
|
6690
|
+
}
|
|
6691
|
+
const stepContext = goalExecutionContext({
|
|
6692
|
+
loop: opts.loop,
|
|
6693
|
+
loopRun: opts.loopRun,
|
|
6694
|
+
scheduledFor: opts.scheduledFor,
|
|
6695
|
+
workflow,
|
|
6696
|
+
workflowRunId: run.id,
|
|
6697
|
+
workflowStepId: step.id
|
|
6698
|
+
});
|
|
6699
|
+
let result;
|
|
6700
|
+
const controller = new AbortController;
|
|
6701
|
+
const externalAbort = () => controller.abort();
|
|
6702
|
+
if (opts.signal?.aborted)
|
|
6573
6703
|
controller.abort();
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
opts
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6704
|
+
opts.signal?.addEventListener("abort", externalAbort, { once: true });
|
|
6705
|
+
const cancelTimer = setInterval(() => {
|
|
6706
|
+
if (store.getWorkflowRun(run.id)?.status === "cancelled")
|
|
6707
|
+
controller.abort();
|
|
6708
|
+
}, opts.cancelPollMs ?? 500);
|
|
6709
|
+
cancelTimer.unref();
|
|
6710
|
+
try {
|
|
6711
|
+
if (step.goal) {
|
|
6712
|
+
result = await runGoal(store, step.goal, {
|
|
6713
|
+
...opts,
|
|
6714
|
+
model: opts.goalModel,
|
|
6715
|
+
target: targetWithStepAccount(step),
|
|
6716
|
+
signal: controller.signal,
|
|
6717
|
+
context: stepContext
|
|
6718
|
+
});
|
|
6719
|
+
} else {
|
|
6720
|
+
result = await executeTarget(targetWithStepAccount(step, opts.goalNodePrompt), executionMetadata(stepContext), {
|
|
6721
|
+
...opts,
|
|
6722
|
+
machine: opts.machine ?? opts.loop?.machine,
|
|
6723
|
+
signal: controller.signal,
|
|
6724
|
+
onAgentProgress: (progress) => {
|
|
6725
|
+
const stdout = JSON.stringify({ agentProgress: progress }, null, 2);
|
|
6726
|
+
opts.beforePersist?.();
|
|
6727
|
+
store.recordWorkflowStepProgress(run.id, step.id, {
|
|
6728
|
+
stdout,
|
|
6729
|
+
payload: progress
|
|
6730
|
+
}, {
|
|
6731
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
6732
|
+
});
|
|
6733
|
+
opts.onAgentProgress?.(progress);
|
|
6734
|
+
},
|
|
6735
|
+
onSpawn: (pid) => {
|
|
6736
|
+
opts.beforePersist?.();
|
|
6737
|
+
store.markWorkflowStepPid(run.id, step.id, pid, { daemonLeaseId: opts.daemonLeaseId });
|
|
6738
|
+
opts.onSpawn?.(pid);
|
|
6739
|
+
}
|
|
6740
|
+
});
|
|
6741
|
+
}
|
|
6742
|
+
} catch (error) {
|
|
6743
|
+
const finishedAt2 = nowIso();
|
|
6744
|
+
result = {
|
|
6745
|
+
status: "failed",
|
|
6746
|
+
stdout: "",
|
|
6747
|
+
stderr: "",
|
|
6748
|
+
error: error instanceof Error ? error.message : String(error),
|
|
6749
|
+
startedAt: startedStep.startedAt ?? finishedAt2,
|
|
6750
|
+
finishedAt: finishedAt2,
|
|
6751
|
+
durationMs: new Date(finishedAt2).getTime() - new Date(startedStep.startedAt ?? finishedAt2).getTime()
|
|
6752
|
+
};
|
|
6753
|
+
} finally {
|
|
6754
|
+
clearInterval(cancelTimer);
|
|
6755
|
+
opts.signal?.removeEventListener("abort", externalAbort);
|
|
6756
|
+
}
|
|
6757
|
+
const timeoutMessage = opts.signal?.aborted ? opts.signalTimeoutMessage?.() : undefined;
|
|
6758
|
+
if (timeoutMessage && result.status === "failed") {
|
|
6759
|
+
result = { ...result, status: "timed_out", error: timeoutMessage };
|
|
6760
|
+
}
|
|
6761
|
+
if (store.isWorkflowRunTerminal(run.id)) {
|
|
6762
|
+
terminalStatus = store.requireWorkflowRun(run.id).status;
|
|
6763
|
+
blockingError = "workflow run was cancelled";
|
|
6764
|
+
break;
|
|
6765
|
+
}
|
|
6766
|
+
opts.beforePersist?.();
|
|
6767
|
+
const blockedExit = result.status === "failed" && result.exitCode !== undefined && blockedExitCodesForStep(step).includes(result.exitCode);
|
|
6768
|
+
if (blockedExit) {
|
|
6769
|
+
store.finalizeWorkflowStepRun(run.id, step.id, {
|
|
6770
|
+
status: "skipped",
|
|
6771
|
+
finishedAt: result.finishedAt,
|
|
6772
|
+
durationMs: result.durationMs,
|
|
6773
|
+
stdout: result.stdout,
|
|
6774
|
+
stderr: result.stderr,
|
|
6775
|
+
exitCode: result.exitCode,
|
|
6776
|
+
error: `${BLOCKED_STEP_ERROR_PREFIX} step ${step.id} exited with blocked exit code ${result.exitCode}`
|
|
6777
|
+
}, {
|
|
6778
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
6595
6779
|
});
|
|
6780
|
+
continue;
|
|
6596
6781
|
}
|
|
6597
|
-
} catch (error) {
|
|
6598
|
-
const finishedAt2 = nowIso();
|
|
6599
|
-
result = {
|
|
6600
|
-
status: "failed",
|
|
6601
|
-
stdout: "",
|
|
6602
|
-
stderr: "",
|
|
6603
|
-
error: error instanceof Error ? error.message : String(error),
|
|
6604
|
-
startedAt: startedStep.startedAt ?? finishedAt2,
|
|
6605
|
-
finishedAt: finishedAt2,
|
|
6606
|
-
durationMs: new Date(finishedAt2).getTime() - new Date(startedStep.startedAt ?? finishedAt2).getTime()
|
|
6607
|
-
};
|
|
6608
|
-
} finally {
|
|
6609
|
-
clearInterval(cancelTimer);
|
|
6610
|
-
opts.signal?.removeEventListener("abort", externalAbort);
|
|
6611
|
-
}
|
|
6612
|
-
const timeoutMessage = opts.signal?.aborted ? opts.signalTimeoutMessage?.() : undefined;
|
|
6613
|
-
if (timeoutMessage && result.status === "failed") {
|
|
6614
|
-
result = { ...result, status: "timed_out", error: timeoutMessage };
|
|
6615
|
-
}
|
|
6616
|
-
if (store.isWorkflowRunTerminal(run.id)) {
|
|
6617
|
-
terminalStatus = store.requireWorkflowRun(run.id).status;
|
|
6618
|
-
blockingError = "workflow run was cancelled";
|
|
6619
|
-
break;
|
|
6620
|
-
}
|
|
6621
|
-
opts.beforePersist?.();
|
|
6622
|
-
const blockedExit = result.status === "failed" && result.exitCode !== undefined && blockedExitCodesForStep(step).includes(result.exitCode);
|
|
6623
|
-
if (blockedExit) {
|
|
6624
6782
|
store.finalizeWorkflowStepRun(run.id, step.id, {
|
|
6625
|
-
status:
|
|
6783
|
+
status: result.status,
|
|
6626
6784
|
finishedAt: result.finishedAt,
|
|
6627
6785
|
durationMs: result.durationMs,
|
|
6628
6786
|
stdout: result.stdout,
|
|
6629
6787
|
stderr: result.stderr,
|
|
6630
6788
|
exitCode: result.exitCode,
|
|
6631
|
-
error:
|
|
6789
|
+
error: result.error
|
|
6632
6790
|
}, {
|
|
6633
6791
|
daemonLeaseId: opts.daemonLeaseId
|
|
6634
6792
|
});
|
|
6635
|
-
|
|
6793
|
+
if (result.status !== "succeeded" && !step.continueOnFailure) {
|
|
6794
|
+
terminalStatus = result.status;
|
|
6795
|
+
blockingError = `step ${step.id} ${result.status}${result.error ? `: ${result.error}` : ""}`;
|
|
6796
|
+
break;
|
|
6797
|
+
}
|
|
6636
6798
|
}
|
|
6637
|
-
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6799
|
+
if (terminalStatus !== "succeeded") {
|
|
6800
|
+
for (const step of ordered) {
|
|
6801
|
+
const existing = store.getWorkflowStepRun(run.id, step.id);
|
|
6802
|
+
if (existing?.status === "pending" || existing?.status === "running") {
|
|
6803
|
+
store.skipWorkflowStepRun(run.id, step.id, blockingError ?? "workflow stopped before step could run", {
|
|
6804
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
6805
|
+
});
|
|
6806
|
+
}
|
|
6807
|
+
}
|
|
6808
|
+
}
|
|
6809
|
+
const finishedAt = nowIso();
|
|
6810
|
+
if (store.isWorkflowRunTerminal(run.id)) {
|
|
6811
|
+
const terminalRun = store.requireWorkflowRun(run.id);
|
|
6812
|
+
return workflowResult(terminalRun, terminalRun.status, startedAt, terminalRun.finishedAt ?? finishedAt, store.listWorkflowStepRuns(run.id), terminalRun.error ?? blockingError);
|
|
6813
|
+
}
|
|
6814
|
+
opts.beforePersist?.();
|
|
6815
|
+
const finalRun = store.finalizeWorkflowRun(run.id, terminalStatus, {
|
|
6816
|
+
finishedAt,
|
|
6817
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime(),
|
|
6818
|
+
error: blockingError
|
|
6645
6819
|
}, {
|
|
6646
6820
|
daemonLeaseId: opts.daemonLeaseId
|
|
6647
6821
|
});
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
if (
|
|
6658
|
-
store.
|
|
6822
|
+
return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, store.listWorkflowStepRuns(run.id), blockingError);
|
|
6823
|
+
} catch (error) {
|
|
6824
|
+
if (opts.daemonLeaseId && error instanceof Error && error.message === "daemon lease lost")
|
|
6825
|
+
throw error;
|
|
6826
|
+
if (error instanceof Error && error.message.includes("workflow step is not claimable"))
|
|
6827
|
+
throw error;
|
|
6828
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6829
|
+
const finishedAt = nowIso();
|
|
6830
|
+
try {
|
|
6831
|
+
if (!store.isWorkflowRunTerminal(run.id)) {
|
|
6832
|
+
store.finalizeWorkflowRun(run.id, "failed", {
|
|
6833
|
+
finishedAt,
|
|
6834
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime(),
|
|
6835
|
+
error: message
|
|
6836
|
+
}, {
|
|
6659
6837
|
daemonLeaseId: opts.daemonLeaseId
|
|
6660
6838
|
});
|
|
6661
6839
|
}
|
|
6662
|
-
}
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
const terminalRun = store.requireWorkflowRun(run.id);
|
|
6667
|
-
return workflowResult(terminalRun, terminalRun.status, startedAt, terminalRun.finishedAt ?? finishedAt, store.listWorkflowStepRuns(run.id), terminalRun.error ?? blockingError);
|
|
6840
|
+
} catch {}
|
|
6841
|
+
const current = store.getWorkflowRun(run.id) ?? run;
|
|
6842
|
+
const resultStatus = current.status === "running" ? "failed" : current.status;
|
|
6843
|
+
return workflowResult(current, resultStatus, startedAt, finishedAt, store.listWorkflowStepRuns(run.id), current.error ?? message);
|
|
6668
6844
|
}
|
|
6669
|
-
opts.beforePersist?.();
|
|
6670
|
-
const finalRun = store.finalizeWorkflowRun(run.id, terminalStatus, {
|
|
6671
|
-
finishedAt,
|
|
6672
|
-
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime(),
|
|
6673
|
-
error: blockingError
|
|
6674
|
-
}, {
|
|
6675
|
-
daemonLeaseId: opts.daemonLeaseId
|
|
6676
|
-
});
|
|
6677
|
-
return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, store.listWorkflowStepRuns(run.id), blockingError);
|
|
6678
6845
|
}
|
|
6679
6846
|
function preflightWorkflow(workflow, opts = {}) {
|
|
6680
6847
|
return workflowExecutionOrder(workflow).map((step) => {
|
|
@@ -7045,6 +7212,25 @@ function advanceOptions(deps) {
|
|
|
7045
7212
|
onRun: deps.onRun
|
|
7046
7213
|
};
|
|
7047
7214
|
}
|
|
7215
|
+
var TERMINAL_RUN_STATUSES2 = new Set([
|
|
7216
|
+
"succeeded",
|
|
7217
|
+
"failed",
|
|
7218
|
+
"timed_out",
|
|
7219
|
+
"abandoned",
|
|
7220
|
+
"skipped"
|
|
7221
|
+
]);
|
|
7222
|
+
function repairWedgedTerminalSlot(deps, loop, scheduledFor, now) {
|
|
7223
|
+
const existing = deps.store.getRunBySlot(loop.id, scheduledFor);
|
|
7224
|
+
if (!existing || !TERMINAL_RUN_STATUSES2.has(existing.status))
|
|
7225
|
+
return;
|
|
7226
|
+
try {
|
|
7227
|
+
advanceLoop(deps.store, loop, existing, new Date(existing.finishedAt ?? now), existing.status === "succeeded", advanceOptions(deps));
|
|
7228
|
+
} catch (error) {
|
|
7229
|
+
if (deps.daemonLeaseId && isDaemonLeaseLost(error))
|
|
7230
|
+
return;
|
|
7231
|
+
throw error;
|
|
7232
|
+
}
|
|
7233
|
+
}
|
|
7048
7234
|
async function runSlot(deps, loop, scheduledFor) {
|
|
7049
7235
|
const now = deps.now?.() ?? new Date;
|
|
7050
7236
|
deps.beforeRun?.(loop, scheduledFor);
|
|
@@ -7071,8 +7257,10 @@ async function runSlot(deps, loop, scheduledFor) {
|
|
|
7071
7257
|
return;
|
|
7072
7258
|
throw error;
|
|
7073
7259
|
}
|
|
7074
|
-
if (!claim)
|
|
7260
|
+
if (!claim) {
|
|
7261
|
+
repairWedgedTerminalSlot(deps, loop, scheduledFor, now);
|
|
7075
7262
|
return;
|
|
7263
|
+
}
|
|
7076
7264
|
deps.beforeRun?.(claim.loop, claim.run.scheduledFor);
|
|
7077
7265
|
deps.onRun?.(claim.run);
|
|
7078
7266
|
const finalRun = await executeClaimedRun({
|
|
@@ -7118,8 +7306,10 @@ function claimSlot(deps, loop, scheduledFor) {
|
|
|
7118
7306
|
return;
|
|
7119
7307
|
throw error;
|
|
7120
7308
|
}
|
|
7121
|
-
if (!claim)
|
|
7309
|
+
if (!claim) {
|
|
7310
|
+
repairWedgedTerminalSlot(deps, loop, scheduledFor, now);
|
|
7122
7311
|
return;
|
|
7312
|
+
}
|
|
7123
7313
|
deps.beforeRun?.(claim.loop, claim.run.scheduledFor);
|
|
7124
7314
|
deps.onRun?.(claim.run);
|
|
7125
7315
|
return claim;
|
|
@@ -7400,10 +7590,7 @@ async function stopDaemon(opts = {}) {
|
|
|
7400
7590
|
const store = new Store(join5(dirname4(path), "loops.db"));
|
|
7401
7591
|
try {
|
|
7402
7592
|
const lease = store.getDaemonLease();
|
|
7403
|
-
|
|
7404
|
-
removePid(path);
|
|
7405
|
-
return { wasRunning: false, stopped: false, forced: false, pid: state.pid };
|
|
7406
|
-
}
|
|
7593
|
+
const leaseVerified = Boolean(lease && lease.pid === state.pid && new Date(lease.expiresAt).getTime() > Date.now());
|
|
7407
7594
|
try {
|
|
7408
7595
|
process.kill(state.pid, "SIGTERM");
|
|
7409
7596
|
} catch {
|
|
@@ -7423,11 +7610,14 @@ async function stopDaemon(opts = {}) {
|
|
|
7423
7610
|
} catch {}
|
|
7424
7611
|
await sleep2(150);
|
|
7425
7612
|
removePid(path);
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7613
|
+
let reapedPgids = [];
|
|
7614
|
+
if (leaseVerified && lease) {
|
|
7615
|
+
const ownedRuns = store.listRuns({ status: "running", limit: 1000 }).filter((run) => run.claimedBy !== undefined && run.claimedBy.endsWith(`:${lease.id}`));
|
|
7616
|
+
reapedPgids = await reapProcessGroups(ownedRuns.map(toReapableProcess), {
|
|
7617
|
+
sleep: sleep2,
|
|
7618
|
+
graceMs: opts.reapGraceMs
|
|
7619
|
+
});
|
|
7620
|
+
}
|
|
7431
7621
|
return { wasRunning: true, stopped: !isAlive(state.pid, record?.startedAt), forced: true, pid: state.pid, reapedPgids };
|
|
7432
7622
|
} finally {
|
|
7433
7623
|
store.close();
|
|
@@ -7612,7 +7802,14 @@ async function runDaemon(opts = {}) {
|
|
|
7612
7802
|
try {
|
|
7613
7803
|
const liveInlineOwner = (run) => {
|
|
7614
7804
|
const ownerPid = inlineRunnerOwnerPid(run.claimedBy);
|
|
7615
|
-
|
|
7805
|
+
if (ownerPid === undefined || !isAlive(ownerPid))
|
|
7806
|
+
return false;
|
|
7807
|
+
const ownerStartMs = processStartTimeMs(ownerPid);
|
|
7808
|
+
const runStartMs = run.startedAt ? Date.parse(run.startedAt) : Number.NaN;
|
|
7809
|
+
if (ownerStartMs !== undefined && Number.isFinite(runStartMs) && ownerStartMs > runStartMs + START_TIME_TOLERANCE_MS) {
|
|
7810
|
+
return false;
|
|
7811
|
+
}
|
|
7812
|
+
return true;
|
|
7616
7813
|
};
|
|
7617
7814
|
const staleCandidates = store.listRuns({ status: "running", limit: 1000 }).filter((run) => run.leaseExpiresAt !== undefined && new Date(run.leaseExpiresAt).getTime() <= Date.now());
|
|
7618
7815
|
const startup = claimDueRuns({ store, runnerId, daemonLeaseId: leaseId, maxClaims: 0 });
|
|
@@ -7806,7 +8003,7 @@ function enableStartup(result) {
|
|
|
7806
8003
|
// package.json
|
|
7807
8004
|
var package_default = {
|
|
7808
8005
|
name: "@hasna/loops",
|
|
7809
|
-
version: "0.4.
|
|
8006
|
+
version: "0.4.7",
|
|
7810
8007
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
7811
8008
|
type: "module",
|
|
7812
8009
|
main: "dist/index.js",
|