@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/mcp/index.js
CHANGED
|
@@ -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;");
|
|
@@ -1642,7 +1645,6 @@ class Store {
|
|
|
1642
1645
|
CREATE INDEX IF NOT EXISTS idx_runs_status ON loop_runs(status);
|
|
1643
1646
|
CREATE INDEX IF NOT EXISTS idx_runs_status_lease ON loop_runs(status, lease_expires_at);
|
|
1644
1647
|
CREATE INDEX IF NOT EXISTS idx_runs_scheduled ON loop_runs(scheduled_for);
|
|
1645
|
-
CREATE INDEX IF NOT EXISTS idx_runs_claim_token ON loop_runs(claim_token) WHERE claim_token IS NOT NULL;
|
|
1646
1648
|
|
|
1647
1649
|
CREATE TABLE IF NOT EXISTS daemon_lease (
|
|
1648
1650
|
id TEXT PRIMARY KEY,
|
|
@@ -1951,9 +1953,12 @@ class Store {
|
|
|
1951
1953
|
const rows = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 2").all(idOrName);
|
|
1952
1954
|
if (rows.length === 0)
|
|
1953
1955
|
throw new LoopNotFoundError(idOrName);
|
|
1954
|
-
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)
|
|
1955
1960
|
throw new AmbiguousNameError(idOrName);
|
|
1956
|
-
return rowToLoop(
|
|
1961
|
+
return rowToLoop(active[0]);
|
|
1957
1962
|
}
|
|
1958
1963
|
requireLoop(idOrName) {
|
|
1959
1964
|
return this.getLoop(idOrName) ?? this.findLoopByName(idOrName) ?? (() => {
|
|
@@ -2294,6 +2299,7 @@ class Store {
|
|
|
2294
2299
|
return this.transact(() => {
|
|
2295
2300
|
const loop = this.requireLoop(idOrName);
|
|
2296
2301
|
this.setWorkflowWorkItemsForLoop(loop.id, "cancelled", "loop deleted", nowIso());
|
|
2302
|
+
this.db.query("DELETE FROM loop_runs WHERE loop_id = ?").run(loop.id);
|
|
2297
2303
|
const res = this.db.query("DELETE FROM loops WHERE id = ?").run(loop.id);
|
|
2298
2304
|
return res.changes > 0;
|
|
2299
2305
|
});
|
|
@@ -2778,7 +2784,10 @@ class Store {
|
|
|
2778
2784
|
return rowToGoal(row);
|
|
2779
2785
|
}
|
|
2780
2786
|
if (context.sourceType && context.sourceId) {
|
|
2781
|
-
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);
|
|
2782
2791
|
if (row)
|
|
2783
2792
|
return rowToGoal(row);
|
|
2784
2793
|
}
|
|
@@ -3198,6 +3207,41 @@ class Store {
|
|
|
3198
3207
|
throw new Error(`workflow step run not found after pid update: ${workflowRunId}/${stepId}`);
|
|
3199
3208
|
return run;
|
|
3200
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
|
+
}
|
|
3201
3245
|
recoverWorkflowRun(workflowRunId, reason = "workflow run recovered for retry") {
|
|
3202
3246
|
return this.transact(() => {
|
|
3203
3247
|
const now = nowIso();
|
|
@@ -3635,7 +3679,8 @@ class Store {
|
|
|
3635
3679
|
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
3636
3680
|
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
3637
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,
|
|
3638
|
-
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);
|
|
3639
3684
|
const run = this.getRun(id);
|
|
3640
3685
|
if (!run)
|
|
3641
3686
|
throw new Error(`run not found after finalize: ${id}`);
|
|
@@ -4163,6 +4208,12 @@ class Store {
|
|
|
4163
4208
|
}
|
|
4164
4209
|
close() {
|
|
4165
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
|
+
}
|
|
4166
4217
|
}
|
|
4167
4218
|
}
|
|
4168
4219
|
|
|
@@ -4360,10 +4411,7 @@ async function stopDaemon(opts = {}) {
|
|
|
4360
4411
|
const store = new Store(join4(dirname3(path), "loops.db"));
|
|
4361
4412
|
try {
|
|
4362
4413
|
const lease = store.getDaemonLease();
|
|
4363
|
-
|
|
4364
|
-
removePid(path);
|
|
4365
|
-
return { wasRunning: false, stopped: false, forced: false, pid: state.pid };
|
|
4366
|
-
}
|
|
4414
|
+
const leaseVerified = Boolean(lease && lease.pid === state.pid && new Date(lease.expiresAt).getTime() > Date.now());
|
|
4367
4415
|
try {
|
|
4368
4416
|
process.kill(state.pid, "SIGTERM");
|
|
4369
4417
|
} catch {
|
|
@@ -4383,11 +4431,14 @@ async function stopDaemon(opts = {}) {
|
|
|
4383
4431
|
} catch {}
|
|
4384
4432
|
await sleep(150);
|
|
4385
4433
|
removePid(path);
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4434
|
+
let reapedPgids = [];
|
|
4435
|
+
if (leaseVerified && lease) {
|
|
4436
|
+
const ownedRuns = store.listRuns({ status: "running", limit: 1000 }).filter((run) => run.claimedBy !== undefined && run.claimedBy.endsWith(`:${lease.id}`));
|
|
4437
|
+
reapedPgids = await reapProcessGroups(ownedRuns.map(toReapableProcess), {
|
|
4438
|
+
sleep,
|
|
4439
|
+
graceMs: opts.reapGraceMs
|
|
4440
|
+
});
|
|
4441
|
+
}
|
|
4391
4442
|
return { wasRunning: true, stopped: !isAlive(state.pid, record?.startedAt), forced: true, pid: state.pid, reapedPgids };
|
|
4392
4443
|
} finally {
|
|
4393
4444
|
store.close();
|
|
@@ -4725,7 +4776,12 @@ function notifySpawn(pid, opts) {
|
|
|
4725
4776
|
if (!pid)
|
|
4726
4777
|
return;
|
|
4727
4778
|
opts.onSpawn?.(pid);
|
|
4728
|
-
|
|
4779
|
+
const startedMs = processStartTimeMs(pid);
|
|
4780
|
+
opts.onSpawnProcess?.({
|
|
4781
|
+
pid,
|
|
4782
|
+
pgid: pid,
|
|
4783
|
+
processStartedAt: startedMs !== undefined ? new Date(startedMs).toISOString() : nowIso()
|
|
4784
|
+
});
|
|
4729
4785
|
}
|
|
4730
4786
|
function shellQuote(value) {
|
|
4731
4787
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -5080,6 +5136,13 @@ function numberField(value, key) {
|
|
|
5080
5136
|
function codewithAgentStatus(readJson) {
|
|
5081
5137
|
return stringField(recordField(readJson, "agent"), "status") ?? stringField(recordField(readJson, "statusSnapshot"), "status");
|
|
5082
5138
|
}
|
|
5139
|
+
function codewithAgentLastEventSeq(readJson) {
|
|
5140
|
+
const agentSeq = numberField(recordField(readJson, "agent"), "lastEventSeq");
|
|
5141
|
+
const snapshotSeq = numberField(recordField(readJson, "statusSnapshot"), "lastEventSeq");
|
|
5142
|
+
if (agentSeq !== undefined && snapshotSeq !== undefined)
|
|
5143
|
+
return Math.max(agentSeq, snapshotSeq);
|
|
5144
|
+
return agentSeq ?? snapshotSeq;
|
|
5145
|
+
}
|
|
5083
5146
|
function codewithAgentEvidence(startJson, readJson, logsJson) {
|
|
5084
5147
|
const agent = recordField(readJson, "agent") ?? recordField(startJson, "agent");
|
|
5085
5148
|
const statusSnapshot = recordField(readJson, "statusSnapshot");
|
|
@@ -5110,6 +5173,21 @@ function codewithAgentEvidence(startJson, readJson, logsJson) {
|
|
|
5110
5173
|
events
|
|
5111
5174
|
}, null, 2);
|
|
5112
5175
|
}
|
|
5176
|
+
function codewithAgentProgress(readJson) {
|
|
5177
|
+
const agent = recordField(readJson, "agent");
|
|
5178
|
+
const statusSnapshot = recordField(readJson, "statusSnapshot");
|
|
5179
|
+
return {
|
|
5180
|
+
provider: "codewith",
|
|
5181
|
+
agentId: stringField(agent, "agentId"),
|
|
5182
|
+
status: stringField(agent, "status") ?? stringField(statusSnapshot, "status"),
|
|
5183
|
+
summary: stringField(statusSnapshot, "summary"),
|
|
5184
|
+
statusReason: stringField(agent, "statusReason"),
|
|
5185
|
+
threadId: stringField(agent, "threadId"),
|
|
5186
|
+
rolloutPath: stringField(agent, "rolloutPath"),
|
|
5187
|
+
pid: numberField(agent, "pid"),
|
|
5188
|
+
lastEventSeq: codewithAgentLastEventSeq(readJson)
|
|
5189
|
+
};
|
|
5190
|
+
}
|
|
5113
5191
|
function sleep(ms) {
|
|
5114
5192
|
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
5115
5193
|
}
|
|
@@ -5144,6 +5222,7 @@ async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt)
|
|
|
5144
5222
|
if (!agentId) {
|
|
5145
5223
|
return failureResult(startedAt, "codewith agent start did not return agent.agentId", { stdout: start.stdout, stderr: stderr.value() });
|
|
5146
5224
|
}
|
|
5225
|
+
opts.onAgentProgress?.(codewithAgentProgress(startJson));
|
|
5147
5226
|
const stopAgent = async () => {
|
|
5148
5227
|
await spawnCapture(spec.command, codewithAgentControlArgs(target, "stop", agentId), {
|
|
5149
5228
|
cwd: spec.cwd,
|
|
@@ -5186,10 +5265,15 @@ async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt)
|
|
|
5186
5265
|
});
|
|
5187
5266
|
}
|
|
5188
5267
|
const status = codewithAgentStatus(lastReadJson);
|
|
5189
|
-
const fingerprint = JSON.stringify({
|
|
5268
|
+
const fingerprint = JSON.stringify({
|
|
5269
|
+
status,
|
|
5270
|
+
agentLastEventSeq: numberField(recordField(lastReadJson, "agent"), "lastEventSeq"),
|
|
5271
|
+
snapshot: recordField(lastReadJson, "statusSnapshot") ?? null
|
|
5272
|
+
});
|
|
5190
5273
|
if (fingerprint !== lastFingerprint) {
|
|
5191
5274
|
lastFingerprint = fingerprint;
|
|
5192
5275
|
lastProgressAt = Date.now();
|
|
5276
|
+
opts.onAgentProgress?.(codewithAgentProgress(lastReadJson));
|
|
5193
5277
|
}
|
|
5194
5278
|
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
5195
5279
|
const logs = await spawnCapture(spec.command, codewithAgentControlArgs(target, "logs", agentId), {
|
|
@@ -6115,7 +6199,7 @@ ${failure.evidence.stderr}` : undefined
|
|
|
6115
6199
|
|
|
6116
6200
|
`);
|
|
6117
6201
|
const dedupeKey = `openloops:${loop.id}:${failure.fingerprint}`;
|
|
6118
|
-
const tags = ["bug", "openloops", "loop-health", failure.classification];
|
|
6202
|
+
const tags = ["bug", "openloops", "loops", "loop-health", failure.classification];
|
|
6119
6203
|
const priority = failure.classification === "auth" || failure.classification === "rate_limit" ? "high" : "medium";
|
|
6120
6204
|
return {
|
|
6121
6205
|
title,
|
|
@@ -6130,7 +6214,7 @@ ${failure.evidence.stderr}` : undefined
|
|
|
6130
6214
|
comment: ["todos", "comment", "<task-id>", description]
|
|
6131
6215
|
},
|
|
6132
6216
|
futureNativeUpsert: {
|
|
6133
|
-
command: "todos upsert",
|
|
6217
|
+
command: "todos task upsert",
|
|
6134
6218
|
fields: {
|
|
6135
6219
|
title,
|
|
6136
6220
|
description,
|
|
@@ -6825,173 +6909,213 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
6825
6909
|
if (run.status === "succeeded" || run.status === "failed" || run.status === "timed_out" || run.status === "cancelled") {
|
|
6826
6910
|
return workflowResult(run, run.status, startedAt, run.finishedAt ?? nowIso(), store.listWorkflowStepRuns(run.id), run.error);
|
|
6827
6911
|
}
|
|
6912
|
+
const resumedRunningSteps = store.listWorkflowStepRuns(run.id).filter((step) => step.status === "running");
|
|
6913
|
+
if (resumedRunningSteps.length > 0 && resumedRunningSteps.every((step) => step.pid !== undefined)) {
|
|
6914
|
+
try {
|
|
6915
|
+
store.recoverWorkflowRun(run.id, "workflow run resumed after lease takeover");
|
|
6916
|
+
} catch {}
|
|
6917
|
+
}
|
|
6828
6918
|
const ordered = workflowExecutionOrder(workflow);
|
|
6829
6919
|
const byId = new Map(workflow.steps.map((step) => [step.id, step]));
|
|
6830
6920
|
let blockingError;
|
|
6831
6921
|
let terminalStatus = "succeeded";
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
const
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
store.
|
|
6858
|
-
|
|
6859
|
-
|
|
6922
|
+
try {
|
|
6923
|
+
for (const step of ordered) {
|
|
6924
|
+
if (store.isWorkflowRunTerminal(run.id)) {
|
|
6925
|
+
terminalStatus = store.requireWorkflowRun(run.id).status;
|
|
6926
|
+
blockingError = "workflow run was cancelled";
|
|
6927
|
+
break;
|
|
6928
|
+
}
|
|
6929
|
+
const pendingTimeout = opts.signal?.aborted ? opts.signalTimeoutMessage?.() : undefined;
|
|
6930
|
+
if (pendingTimeout) {
|
|
6931
|
+
terminalStatus = "timed_out";
|
|
6932
|
+
blockingError = pendingTimeout;
|
|
6933
|
+
break;
|
|
6934
|
+
}
|
|
6935
|
+
const existing = store.getWorkflowStepRun(run.id, step.id);
|
|
6936
|
+
if (existing?.status === "succeeded" || existing?.status === "skipped" || existing?.status === "cancelled")
|
|
6937
|
+
continue;
|
|
6938
|
+
const blockedBy = (step.dependsOn ?? []).find((dependencyId) => {
|
|
6939
|
+
const dependencyRun = store.getWorkflowStepRun(run.id, dependencyId);
|
|
6940
|
+
const dependencyStep = byId.get(dependencyId);
|
|
6941
|
+
if (dependencyRun?.status === "succeeded")
|
|
6942
|
+
return false;
|
|
6943
|
+
return !dependencyStep?.continueOnFailure;
|
|
6944
|
+
});
|
|
6945
|
+
if (blockedBy) {
|
|
6946
|
+
opts.beforePersist?.();
|
|
6947
|
+
if (isBlockedStepRun(store.getWorkflowStepRun(run.id, blockedBy))) {
|
|
6948
|
+
store.skipWorkflowStepRun(run.id, step.id, `${BLOCKED_STEP_ERROR_PREFIX} upstream step ${blockedBy} was blocked`, {
|
|
6949
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
6950
|
+
});
|
|
6951
|
+
continue;
|
|
6952
|
+
}
|
|
6953
|
+
store.skipWorkflowStepRun(run.id, step.id, `dependency did not succeed: ${blockedBy}`, { daemonLeaseId: opts.daemonLeaseId });
|
|
6954
|
+
blockingError ??= `step ${step.id} blocked by dependency ${blockedBy}`;
|
|
6955
|
+
terminalStatus = "failed";
|
|
6860
6956
|
continue;
|
|
6861
6957
|
}
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
|
|
6872
|
-
|
|
6873
|
-
|
|
6874
|
-
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
|
|
6881
|
-
});
|
|
6882
|
-
let result;
|
|
6883
|
-
const controller = new AbortController;
|
|
6884
|
-
const externalAbort = () => controller.abort();
|
|
6885
|
-
if (opts.signal?.aborted)
|
|
6886
|
-
controller.abort();
|
|
6887
|
-
opts.signal?.addEventListener("abort", externalAbort, { once: true });
|
|
6888
|
-
const cancelTimer = setInterval(() => {
|
|
6889
|
-
if (store.getWorkflowRun(run.id)?.status === "cancelled")
|
|
6958
|
+
opts.beforePersist?.();
|
|
6959
|
+
const startedStep = store.startWorkflowStepRun(run.id, step.id, { daemonLeaseId: opts.daemonLeaseId });
|
|
6960
|
+
if (startedStep.status !== "running") {
|
|
6961
|
+
terminalStatus = "failed";
|
|
6962
|
+
blockingError = `step ${step.id} could not start because workflow is no longer running`;
|
|
6963
|
+
break;
|
|
6964
|
+
}
|
|
6965
|
+
const stepContext = goalExecutionContext({
|
|
6966
|
+
loop: opts.loop,
|
|
6967
|
+
loopRun: opts.loopRun,
|
|
6968
|
+
scheduledFor: opts.scheduledFor,
|
|
6969
|
+
workflow,
|
|
6970
|
+
workflowRunId: run.id,
|
|
6971
|
+
workflowStepId: step.id
|
|
6972
|
+
});
|
|
6973
|
+
let result;
|
|
6974
|
+
const controller = new AbortController;
|
|
6975
|
+
const externalAbort = () => controller.abort();
|
|
6976
|
+
if (opts.signal?.aborted)
|
|
6890
6977
|
controller.abort();
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
opts
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6978
|
+
opts.signal?.addEventListener("abort", externalAbort, { once: true });
|
|
6979
|
+
const cancelTimer = setInterval(() => {
|
|
6980
|
+
if (store.getWorkflowRun(run.id)?.status === "cancelled")
|
|
6981
|
+
controller.abort();
|
|
6982
|
+
}, opts.cancelPollMs ?? 500);
|
|
6983
|
+
cancelTimer.unref();
|
|
6984
|
+
try {
|
|
6985
|
+
if (step.goal) {
|
|
6986
|
+
result = await runGoal(store, step.goal, {
|
|
6987
|
+
...opts,
|
|
6988
|
+
model: opts.goalModel,
|
|
6989
|
+
target: targetWithStepAccount(step),
|
|
6990
|
+
signal: controller.signal,
|
|
6991
|
+
context: stepContext
|
|
6992
|
+
});
|
|
6993
|
+
} else {
|
|
6994
|
+
result = await executeTarget(targetWithStepAccount(step, opts.goalNodePrompt), executionMetadata(stepContext), {
|
|
6995
|
+
...opts,
|
|
6996
|
+
machine: opts.machine ?? opts.loop?.machine,
|
|
6997
|
+
signal: controller.signal,
|
|
6998
|
+
onAgentProgress: (progress) => {
|
|
6999
|
+
const stdout = JSON.stringify({ agentProgress: progress }, null, 2);
|
|
7000
|
+
opts.beforePersist?.();
|
|
7001
|
+
store.recordWorkflowStepProgress(run.id, step.id, {
|
|
7002
|
+
stdout,
|
|
7003
|
+
payload: progress
|
|
7004
|
+
}, {
|
|
7005
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
7006
|
+
});
|
|
7007
|
+
opts.onAgentProgress?.(progress);
|
|
7008
|
+
},
|
|
7009
|
+
onSpawn: (pid) => {
|
|
7010
|
+
opts.beforePersist?.();
|
|
7011
|
+
store.markWorkflowStepPid(run.id, step.id, pid, { daemonLeaseId: opts.daemonLeaseId });
|
|
7012
|
+
opts.onSpawn?.(pid);
|
|
7013
|
+
}
|
|
7014
|
+
});
|
|
7015
|
+
}
|
|
7016
|
+
} catch (error) {
|
|
7017
|
+
const finishedAt2 = nowIso();
|
|
7018
|
+
result = {
|
|
7019
|
+
status: "failed",
|
|
7020
|
+
stdout: "",
|
|
7021
|
+
stderr: "",
|
|
7022
|
+
error: error instanceof Error ? error.message : String(error),
|
|
7023
|
+
startedAt: startedStep.startedAt ?? finishedAt2,
|
|
7024
|
+
finishedAt: finishedAt2,
|
|
7025
|
+
durationMs: new Date(finishedAt2).getTime() - new Date(startedStep.startedAt ?? finishedAt2).getTime()
|
|
7026
|
+
};
|
|
7027
|
+
} finally {
|
|
7028
|
+
clearInterval(cancelTimer);
|
|
7029
|
+
opts.signal?.removeEventListener("abort", externalAbort);
|
|
7030
|
+
}
|
|
7031
|
+
const timeoutMessage = opts.signal?.aborted ? opts.signalTimeoutMessage?.() : undefined;
|
|
7032
|
+
if (timeoutMessage && result.status === "failed") {
|
|
7033
|
+
result = { ...result, status: "timed_out", error: timeoutMessage };
|
|
7034
|
+
}
|
|
7035
|
+
if (store.isWorkflowRunTerminal(run.id)) {
|
|
7036
|
+
terminalStatus = store.requireWorkflowRun(run.id).status;
|
|
7037
|
+
blockingError = "workflow run was cancelled";
|
|
7038
|
+
break;
|
|
7039
|
+
}
|
|
7040
|
+
opts.beforePersist?.();
|
|
7041
|
+
const blockedExit = result.status === "failed" && result.exitCode !== undefined && blockedExitCodesForStep(step).includes(result.exitCode);
|
|
7042
|
+
if (blockedExit) {
|
|
7043
|
+
store.finalizeWorkflowStepRun(run.id, step.id, {
|
|
7044
|
+
status: "skipped",
|
|
7045
|
+
finishedAt: result.finishedAt,
|
|
7046
|
+
durationMs: result.durationMs,
|
|
7047
|
+
stdout: result.stdout,
|
|
7048
|
+
stderr: result.stderr,
|
|
7049
|
+
exitCode: result.exitCode,
|
|
7050
|
+
error: `${BLOCKED_STEP_ERROR_PREFIX} step ${step.id} exited with blocked exit code ${result.exitCode}`
|
|
7051
|
+
}, {
|
|
7052
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
6912
7053
|
});
|
|
7054
|
+
continue;
|
|
6913
7055
|
}
|
|
6914
|
-
} catch (error) {
|
|
6915
|
-
const finishedAt2 = nowIso();
|
|
6916
|
-
result = {
|
|
6917
|
-
status: "failed",
|
|
6918
|
-
stdout: "",
|
|
6919
|
-
stderr: "",
|
|
6920
|
-
error: error instanceof Error ? error.message : String(error),
|
|
6921
|
-
startedAt: startedStep.startedAt ?? finishedAt2,
|
|
6922
|
-
finishedAt: finishedAt2,
|
|
6923
|
-
durationMs: new Date(finishedAt2).getTime() - new Date(startedStep.startedAt ?? finishedAt2).getTime()
|
|
6924
|
-
};
|
|
6925
|
-
} finally {
|
|
6926
|
-
clearInterval(cancelTimer);
|
|
6927
|
-
opts.signal?.removeEventListener("abort", externalAbort);
|
|
6928
|
-
}
|
|
6929
|
-
const timeoutMessage = opts.signal?.aborted ? opts.signalTimeoutMessage?.() : undefined;
|
|
6930
|
-
if (timeoutMessage && result.status === "failed") {
|
|
6931
|
-
result = { ...result, status: "timed_out", error: timeoutMessage };
|
|
6932
|
-
}
|
|
6933
|
-
if (store.isWorkflowRunTerminal(run.id)) {
|
|
6934
|
-
terminalStatus = store.requireWorkflowRun(run.id).status;
|
|
6935
|
-
blockingError = "workflow run was cancelled";
|
|
6936
|
-
break;
|
|
6937
|
-
}
|
|
6938
|
-
opts.beforePersist?.();
|
|
6939
|
-
const blockedExit = result.status === "failed" && result.exitCode !== undefined && blockedExitCodesForStep(step).includes(result.exitCode);
|
|
6940
|
-
if (blockedExit) {
|
|
6941
7056
|
store.finalizeWorkflowStepRun(run.id, step.id, {
|
|
6942
|
-
status:
|
|
7057
|
+
status: result.status,
|
|
6943
7058
|
finishedAt: result.finishedAt,
|
|
6944
7059
|
durationMs: result.durationMs,
|
|
6945
7060
|
stdout: result.stdout,
|
|
6946
7061
|
stderr: result.stderr,
|
|
6947
7062
|
exitCode: result.exitCode,
|
|
6948
|
-
error:
|
|
7063
|
+
error: result.error
|
|
6949
7064
|
}, {
|
|
6950
7065
|
daemonLeaseId: opts.daemonLeaseId
|
|
6951
7066
|
});
|
|
6952
|
-
|
|
7067
|
+
if (result.status !== "succeeded" && !step.continueOnFailure) {
|
|
7068
|
+
terminalStatus = result.status;
|
|
7069
|
+
blockingError = `step ${step.id} ${result.status}${result.error ? `: ${result.error}` : ""}`;
|
|
7070
|
+
break;
|
|
7071
|
+
}
|
|
6953
7072
|
}
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
6961
|
-
|
|
7073
|
+
if (terminalStatus !== "succeeded") {
|
|
7074
|
+
for (const step of ordered) {
|
|
7075
|
+
const existing = store.getWorkflowStepRun(run.id, step.id);
|
|
7076
|
+
if (existing?.status === "pending" || existing?.status === "running") {
|
|
7077
|
+
store.skipWorkflowStepRun(run.id, step.id, blockingError ?? "workflow stopped before step could run", {
|
|
7078
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
7079
|
+
});
|
|
7080
|
+
}
|
|
7081
|
+
}
|
|
7082
|
+
}
|
|
7083
|
+
const finishedAt = nowIso();
|
|
7084
|
+
if (store.isWorkflowRunTerminal(run.id)) {
|
|
7085
|
+
const terminalRun = store.requireWorkflowRun(run.id);
|
|
7086
|
+
return workflowResult(terminalRun, terminalRun.status, startedAt, terminalRun.finishedAt ?? finishedAt, store.listWorkflowStepRuns(run.id), terminalRun.error ?? blockingError);
|
|
7087
|
+
}
|
|
7088
|
+
opts.beforePersist?.();
|
|
7089
|
+
const finalRun = store.finalizeWorkflowRun(run.id, terminalStatus, {
|
|
7090
|
+
finishedAt,
|
|
7091
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime(),
|
|
7092
|
+
error: blockingError
|
|
6962
7093
|
}, {
|
|
6963
7094
|
daemonLeaseId: opts.daemonLeaseId
|
|
6964
7095
|
});
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
if (
|
|
6975
|
-
store.
|
|
7096
|
+
return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, store.listWorkflowStepRuns(run.id), blockingError);
|
|
7097
|
+
} catch (error) {
|
|
7098
|
+
if (opts.daemonLeaseId && error instanceof Error && error.message === "daemon lease lost")
|
|
7099
|
+
throw error;
|
|
7100
|
+
if (error instanceof Error && error.message.includes("workflow step is not claimable"))
|
|
7101
|
+
throw error;
|
|
7102
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7103
|
+
const finishedAt = nowIso();
|
|
7104
|
+
try {
|
|
7105
|
+
if (!store.isWorkflowRunTerminal(run.id)) {
|
|
7106
|
+
store.finalizeWorkflowRun(run.id, "failed", {
|
|
7107
|
+
finishedAt,
|
|
7108
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime(),
|
|
7109
|
+
error: message
|
|
7110
|
+
}, {
|
|
6976
7111
|
daemonLeaseId: opts.daemonLeaseId
|
|
6977
7112
|
});
|
|
6978
7113
|
}
|
|
6979
|
-
}
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
const terminalRun = store.requireWorkflowRun(run.id);
|
|
6984
|
-
return workflowResult(terminalRun, terminalRun.status, startedAt, terminalRun.finishedAt ?? finishedAt, store.listWorkflowStepRuns(run.id), terminalRun.error ?? blockingError);
|
|
7114
|
+
} catch {}
|
|
7115
|
+
const current = store.getWorkflowRun(run.id) ?? run;
|
|
7116
|
+
const resultStatus = current.status === "running" ? "failed" : current.status;
|
|
7117
|
+
return workflowResult(current, resultStatus, startedAt, finishedAt, store.listWorkflowStepRuns(run.id), current.error ?? message);
|
|
6985
7118
|
}
|
|
6986
|
-
opts.beforePersist?.();
|
|
6987
|
-
const finalRun = store.finalizeWorkflowRun(run.id, terminalStatus, {
|
|
6988
|
-
finishedAt,
|
|
6989
|
-
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime(),
|
|
6990
|
-
error: blockingError
|
|
6991
|
-
}, {
|
|
6992
|
-
daemonLeaseId: opts.daemonLeaseId
|
|
6993
|
-
});
|
|
6994
|
-
return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, store.listWorkflowStepRuns(run.id), blockingError);
|
|
6995
7119
|
}
|
|
6996
7120
|
function preflightWorkflow(workflow, opts = {}) {
|
|
6997
7121
|
return workflowExecutionOrder(workflow).map((step) => {
|
|
@@ -7362,6 +7486,25 @@ function advanceOptions(deps) {
|
|
|
7362
7486
|
onRun: deps.onRun
|
|
7363
7487
|
};
|
|
7364
7488
|
}
|
|
7489
|
+
var TERMINAL_RUN_STATUSES2 = new Set([
|
|
7490
|
+
"succeeded",
|
|
7491
|
+
"failed",
|
|
7492
|
+
"timed_out",
|
|
7493
|
+
"abandoned",
|
|
7494
|
+
"skipped"
|
|
7495
|
+
]);
|
|
7496
|
+
function repairWedgedTerminalSlot(deps, loop, scheduledFor, now) {
|
|
7497
|
+
const existing = deps.store.getRunBySlot(loop.id, scheduledFor);
|
|
7498
|
+
if (!existing || !TERMINAL_RUN_STATUSES2.has(existing.status))
|
|
7499
|
+
return;
|
|
7500
|
+
try {
|
|
7501
|
+
advanceLoop(deps.store, loop, existing, new Date(existing.finishedAt ?? now), existing.status === "succeeded", advanceOptions(deps));
|
|
7502
|
+
} catch (error) {
|
|
7503
|
+
if (deps.daemonLeaseId && isDaemonLeaseLost(error))
|
|
7504
|
+
return;
|
|
7505
|
+
throw error;
|
|
7506
|
+
}
|
|
7507
|
+
}
|
|
7365
7508
|
async function runSlot(deps, loop, scheduledFor) {
|
|
7366
7509
|
const now = deps.now?.() ?? new Date;
|
|
7367
7510
|
deps.beforeRun?.(loop, scheduledFor);
|
|
@@ -7388,8 +7531,10 @@ async function runSlot(deps, loop, scheduledFor) {
|
|
|
7388
7531
|
return;
|
|
7389
7532
|
throw error;
|
|
7390
7533
|
}
|
|
7391
|
-
if (!claim)
|
|
7534
|
+
if (!claim) {
|
|
7535
|
+
repairWedgedTerminalSlot(deps, loop, scheduledFor, now);
|
|
7392
7536
|
return;
|
|
7537
|
+
}
|
|
7393
7538
|
deps.beforeRun?.(claim.loop, claim.run.scheduledFor);
|
|
7394
7539
|
deps.onRun?.(claim.run);
|
|
7395
7540
|
const finalRun = await executeClaimedRun({
|
|
@@ -7435,8 +7580,10 @@ function claimSlot(deps, loop, scheduledFor) {
|
|
|
7435
7580
|
return;
|
|
7436
7581
|
throw error;
|
|
7437
7582
|
}
|
|
7438
|
-
if (!claim)
|
|
7583
|
+
if (!claim) {
|
|
7584
|
+
repairWedgedTerminalSlot(deps, loop, scheduledFor, now);
|
|
7439
7585
|
return;
|
|
7586
|
+
}
|
|
7440
7587
|
deps.beforeRun?.(claim.loop, claim.run.scheduledFor);
|
|
7441
7588
|
deps.onRun?.(claim.run);
|
|
7442
7589
|
return claim;
|
|
@@ -7530,7 +7677,7 @@ async function tick(deps) {
|
|
|
7530
7677
|
// package.json
|
|
7531
7678
|
var package_default = {
|
|
7532
7679
|
name: "@hasna/loops",
|
|
7533
|
-
version: "0.4.
|
|
7680
|
+
version: "0.4.6",
|
|
7534
7681
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
7535
7682
|
type: "module",
|
|
7536
7683
|
main: "dist/index.js",
|
|
@@ -8077,7 +8224,7 @@ var TOOL_REGISTRATIONS = [
|
|
|
8077
8224
|
guarded: true,
|
|
8078
8225
|
annotations: mutationAnnotations({ idempotent: true }),
|
|
8079
8226
|
inputSchema: { idOrName: loopIdOrNameSchema },
|
|
8080
|
-
handler: ({ idOrName }) => withStore((store) => ({ loop: publicLoop(store.updateLoop(store.
|
|
8227
|
+
handler: ({ idOrName }) => withStore((store) => ({ loop: publicLoop(store.updateLoop(store.requireUniqueLoop(idOrName).id, { status: "paused" })) }))
|
|
8081
8228
|
},
|
|
8082
8229
|
{
|
|
8083
8230
|
name: "loops_resume",
|
|
@@ -8087,7 +8234,15 @@ var TOOL_REGISTRATIONS = [
|
|
|
8087
8234
|
guarded: true,
|
|
8088
8235
|
annotations: mutationAnnotations({ idempotent: true }),
|
|
8089
8236
|
inputSchema: { idOrName: loopIdOrNameSchema },
|
|
8090
|
-
handler: ({ idOrName }) => withStore((store) =>
|
|
8237
|
+
handler: ({ idOrName }) => withStore((store) => {
|
|
8238
|
+
const loop = store.requireUniqueLoop(idOrName);
|
|
8239
|
+
let nextRunAt = loop.nextRunAt;
|
|
8240
|
+
if (!nextRunAt) {
|
|
8241
|
+
const now = new Date;
|
|
8242
|
+
nextRunAt = computeNextAfter(loop.schedule, now, now);
|
|
8243
|
+
}
|
|
8244
|
+
return { loop: publicLoop(store.updateLoop(loop.id, { status: "active", nextRunAt })) };
|
|
8245
|
+
})
|
|
8091
8246
|
},
|
|
8092
8247
|
{
|
|
8093
8248
|
name: "loops_stop",
|
|
@@ -8098,7 +8253,7 @@ var TOOL_REGISTRATIONS = [
|
|
|
8098
8253
|
annotations: mutationAnnotations({ idempotent: true }),
|
|
8099
8254
|
inputSchema: { idOrName: loopIdOrNameSchema },
|
|
8100
8255
|
handler: ({ idOrName }) => withStore((store) => ({
|
|
8101
|
-
loop: publicLoop(store.updateLoop(store.
|
|
8256
|
+
loop: publicLoop(store.updateLoop(store.requireUniqueLoop(idOrName).id, { status: "stopped", nextRunAt: undefined }))
|
|
8102
8257
|
}))
|
|
8103
8258
|
},
|
|
8104
8259
|
{
|
|
@@ -8111,7 +8266,7 @@ var TOOL_REGISTRATIONS = [
|
|
|
8111
8266
|
inputSchema: { idOrName: loopIdOrNameSchema },
|
|
8112
8267
|
handler: ({ idOrName }) => withStore(async (store) => {
|
|
8113
8268
|
const daemon = daemonStatus(store);
|
|
8114
|
-
const result = await runLoopNow({ store, idOrName, runnerId: `mcp:${process.pid}`, mode: "schedule" });
|
|
8269
|
+
const result = await runLoopNow({ store, idOrName: store.requireUniqueLoop(idOrName).id, runnerId: `mcp:${process.pid}`, mode: "schedule" });
|
|
8115
8270
|
return {
|
|
8116
8271
|
scheduledFor: result.scheduledFor,
|
|
8117
8272
|
loop: publicLoop(result.loop),
|