@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/mcp/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
|
|
|
@@ -4359,10 +4411,7 @@ async function stopDaemon(opts = {}) {
|
|
|
4359
4411
|
const store = new Store(join4(dirname3(path), "loops.db"));
|
|
4360
4412
|
try {
|
|
4361
4413
|
const lease = store.getDaemonLease();
|
|
4362
|
-
|
|
4363
|
-
removePid(path);
|
|
4364
|
-
return { wasRunning: false, stopped: false, forced: false, pid: state.pid };
|
|
4365
|
-
}
|
|
4414
|
+
const leaseVerified = Boolean(lease && lease.pid === state.pid && new Date(lease.expiresAt).getTime() > Date.now());
|
|
4366
4415
|
try {
|
|
4367
4416
|
process.kill(state.pid, "SIGTERM");
|
|
4368
4417
|
} catch {
|
|
@@ -4382,11 +4431,14 @@ async function stopDaemon(opts = {}) {
|
|
|
4382
4431
|
} catch {}
|
|
4383
4432
|
await sleep(150);
|
|
4384
4433
|
removePid(path);
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
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
|
+
}
|
|
4390
4442
|
return { wasRunning: true, stopped: !isAlive(state.pid, record?.startedAt), forced: true, pid: state.pid, reapedPgids };
|
|
4391
4443
|
} finally {
|
|
4392
4444
|
store.close();
|
|
@@ -4724,7 +4776,12 @@ function notifySpawn(pid, opts) {
|
|
|
4724
4776
|
if (!pid)
|
|
4725
4777
|
return;
|
|
4726
4778
|
opts.onSpawn?.(pid);
|
|
4727
|
-
|
|
4779
|
+
const startedMs = processStartTimeMs(pid);
|
|
4780
|
+
opts.onSpawnProcess?.({
|
|
4781
|
+
pid,
|
|
4782
|
+
pgid: pid,
|
|
4783
|
+
processStartedAt: startedMs !== undefined ? new Date(startedMs).toISOString() : nowIso()
|
|
4784
|
+
});
|
|
4728
4785
|
}
|
|
4729
4786
|
function shellQuote(value) {
|
|
4730
4787
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -5049,18 +5106,60 @@ function codewithAgentControlArgs(target, command, agentId) {
|
|
|
5049
5106
|
"agent",
|
|
5050
5107
|
command,
|
|
5051
5108
|
...command === "logs" ? ["--limit", "20"] : [],
|
|
5109
|
+
"--json",
|
|
5052
5110
|
agentId
|
|
5053
5111
|
];
|
|
5054
5112
|
}
|
|
5113
|
+
function extractFirstJsonObject(text) {
|
|
5114
|
+
let depth = 0;
|
|
5115
|
+
let start = -1;
|
|
5116
|
+
let inString = false;
|
|
5117
|
+
let escaped = false;
|
|
5118
|
+
for (let i = 0;i < text.length; i += 1) {
|
|
5119
|
+
const ch = text[i];
|
|
5120
|
+
if (inString) {
|
|
5121
|
+
if (escaped)
|
|
5122
|
+
escaped = false;
|
|
5123
|
+
else if (ch === "\\")
|
|
5124
|
+
escaped = true;
|
|
5125
|
+
else if (ch === '"')
|
|
5126
|
+
inString = false;
|
|
5127
|
+
continue;
|
|
5128
|
+
}
|
|
5129
|
+
if (ch === '"') {
|
|
5130
|
+
inString = true;
|
|
5131
|
+
} else if (ch === "{") {
|
|
5132
|
+
if (depth === 0)
|
|
5133
|
+
start = i;
|
|
5134
|
+
depth += 1;
|
|
5135
|
+
} else if (ch === "}") {
|
|
5136
|
+
if (depth > 0) {
|
|
5137
|
+
depth -= 1;
|
|
5138
|
+
if (depth === 0 && start !== -1)
|
|
5139
|
+
return text.slice(start, i + 1);
|
|
5140
|
+
}
|
|
5141
|
+
}
|
|
5142
|
+
}
|
|
5143
|
+
return;
|
|
5144
|
+
}
|
|
5055
5145
|
function parseJsonOutput(stdout, label) {
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5146
|
+
const raw = stdout || "{}";
|
|
5147
|
+
const candidates = [raw.trim()];
|
|
5148
|
+
const scanned = extractFirstJsonObject(raw);
|
|
5149
|
+
if (scanned && scanned !== raw.trim())
|
|
5150
|
+
candidates.push(scanned);
|
|
5151
|
+
let lastError;
|
|
5152
|
+
for (const candidate of candidates) {
|
|
5153
|
+
try {
|
|
5154
|
+
const value = JSON.parse(candidate);
|
|
5155
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5156
|
+
throw new Error("not an object");
|
|
5157
|
+
return value;
|
|
5158
|
+
} catch (error) {
|
|
5159
|
+
lastError = error;
|
|
5160
|
+
}
|
|
5063
5161
|
}
|
|
5162
|
+
throw new Error(`${label} did not return JSON${lastError instanceof Error ? `: ${lastError.message}` : ""}`);
|
|
5064
5163
|
}
|
|
5065
5164
|
function recordField(value, key) {
|
|
5066
5165
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -5079,6 +5178,13 @@ function numberField(value, key) {
|
|
|
5079
5178
|
function codewithAgentStatus(readJson) {
|
|
5080
5179
|
return stringField(recordField(readJson, "agent"), "status") ?? stringField(recordField(readJson, "statusSnapshot"), "status");
|
|
5081
5180
|
}
|
|
5181
|
+
function codewithAgentLastEventSeq(readJson) {
|
|
5182
|
+
const agentSeq = numberField(recordField(readJson, "agent"), "lastEventSeq");
|
|
5183
|
+
const snapshotSeq = numberField(recordField(readJson, "statusSnapshot"), "lastEventSeq");
|
|
5184
|
+
if (agentSeq !== undefined && snapshotSeq !== undefined)
|
|
5185
|
+
return Math.max(agentSeq, snapshotSeq);
|
|
5186
|
+
return agentSeq ?? snapshotSeq;
|
|
5187
|
+
}
|
|
5082
5188
|
function codewithAgentEvidence(startJson, readJson, logsJson) {
|
|
5083
5189
|
const agent = recordField(readJson, "agent") ?? recordField(startJson, "agent");
|
|
5084
5190
|
const statusSnapshot = recordField(readJson, "statusSnapshot");
|
|
@@ -5109,6 +5215,21 @@ function codewithAgentEvidence(startJson, readJson, logsJson) {
|
|
|
5109
5215
|
events
|
|
5110
5216
|
}, null, 2);
|
|
5111
5217
|
}
|
|
5218
|
+
function codewithAgentProgress(readJson) {
|
|
5219
|
+
const agent = recordField(readJson, "agent");
|
|
5220
|
+
const statusSnapshot = recordField(readJson, "statusSnapshot");
|
|
5221
|
+
return {
|
|
5222
|
+
provider: "codewith",
|
|
5223
|
+
agentId: stringField(agent, "agentId"),
|
|
5224
|
+
status: stringField(agent, "status") ?? stringField(statusSnapshot, "status"),
|
|
5225
|
+
summary: stringField(statusSnapshot, "summary"),
|
|
5226
|
+
statusReason: stringField(agent, "statusReason"),
|
|
5227
|
+
threadId: stringField(agent, "threadId"),
|
|
5228
|
+
rolloutPath: stringField(agent, "rolloutPath"),
|
|
5229
|
+
pid: numberField(agent, "pid"),
|
|
5230
|
+
lastEventSeq: codewithAgentLastEventSeq(readJson)
|
|
5231
|
+
};
|
|
5232
|
+
}
|
|
5112
5233
|
function sleep(ms) {
|
|
5113
5234
|
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
5114
5235
|
}
|
|
@@ -5143,6 +5264,7 @@ async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt)
|
|
|
5143
5264
|
if (!agentId) {
|
|
5144
5265
|
return failureResult(startedAt, "codewith agent start did not return agent.agentId", { stdout: start.stdout, stderr: stderr.value() });
|
|
5145
5266
|
}
|
|
5267
|
+
opts.onAgentProgress?.(codewithAgentProgress(startJson));
|
|
5146
5268
|
const stopAgent = async () => {
|
|
5147
5269
|
await spawnCapture(spec.command, codewithAgentControlArgs(target, "stop", agentId), {
|
|
5148
5270
|
cwd: spec.cwd,
|
|
@@ -5185,10 +5307,15 @@ async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt)
|
|
|
5185
5307
|
});
|
|
5186
5308
|
}
|
|
5187
5309
|
const status = codewithAgentStatus(lastReadJson);
|
|
5188
|
-
const fingerprint = JSON.stringify({
|
|
5310
|
+
const fingerprint = JSON.stringify({
|
|
5311
|
+
status,
|
|
5312
|
+
agentLastEventSeq: numberField(recordField(lastReadJson, "agent"), "lastEventSeq"),
|
|
5313
|
+
snapshot: recordField(lastReadJson, "statusSnapshot") ?? null
|
|
5314
|
+
});
|
|
5189
5315
|
if (fingerprint !== lastFingerprint) {
|
|
5190
5316
|
lastFingerprint = fingerprint;
|
|
5191
5317
|
lastProgressAt = Date.now();
|
|
5318
|
+
opts.onAgentProgress?.(codewithAgentProgress(lastReadJson));
|
|
5192
5319
|
}
|
|
5193
5320
|
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
5194
5321
|
const logs = await spawnCapture(spec.command, codewithAgentControlArgs(target, "logs", agentId), {
|
|
@@ -6114,7 +6241,7 @@ ${failure.evidence.stderr}` : undefined
|
|
|
6114
6241
|
|
|
6115
6242
|
`);
|
|
6116
6243
|
const dedupeKey = `openloops:${loop.id}:${failure.fingerprint}`;
|
|
6117
|
-
const tags = ["bug", "openloops", "loop-health", failure.classification];
|
|
6244
|
+
const tags = ["bug", "openloops", "loops", "loop-health", failure.classification];
|
|
6118
6245
|
const priority = failure.classification === "auth" || failure.classification === "rate_limit" ? "high" : "medium";
|
|
6119
6246
|
return {
|
|
6120
6247
|
title,
|
|
@@ -6129,7 +6256,7 @@ ${failure.evidence.stderr}` : undefined
|
|
|
6129
6256
|
comment: ["todos", "comment", "<task-id>", description]
|
|
6130
6257
|
},
|
|
6131
6258
|
futureNativeUpsert: {
|
|
6132
|
-
command: "todos upsert",
|
|
6259
|
+
command: "todos task upsert",
|
|
6133
6260
|
fields: {
|
|
6134
6261
|
title,
|
|
6135
6262
|
description,
|
|
@@ -6824,173 +6951,213 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
6824
6951
|
if (run.status === "succeeded" || run.status === "failed" || run.status === "timed_out" || run.status === "cancelled") {
|
|
6825
6952
|
return workflowResult(run, run.status, startedAt, run.finishedAt ?? nowIso(), store.listWorkflowStepRuns(run.id), run.error);
|
|
6826
6953
|
}
|
|
6954
|
+
const resumedRunningSteps = store.listWorkflowStepRuns(run.id).filter((step) => step.status === "running");
|
|
6955
|
+
if (resumedRunningSteps.length > 0 && resumedRunningSteps.every((step) => step.pid !== undefined)) {
|
|
6956
|
+
try {
|
|
6957
|
+
store.recoverWorkflowRun(run.id, "workflow run resumed after lease takeover");
|
|
6958
|
+
} catch {}
|
|
6959
|
+
}
|
|
6827
6960
|
const ordered = workflowExecutionOrder(workflow);
|
|
6828
6961
|
const byId = new Map(workflow.steps.map((step) => [step.id, step]));
|
|
6829
6962
|
let blockingError;
|
|
6830
6963
|
let terminalStatus = "succeeded";
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
const
|
|
6848
|
-
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
store.
|
|
6857
|
-
|
|
6858
|
-
|
|
6964
|
+
try {
|
|
6965
|
+
for (const step of ordered) {
|
|
6966
|
+
if (store.isWorkflowRunTerminal(run.id)) {
|
|
6967
|
+
terminalStatus = store.requireWorkflowRun(run.id).status;
|
|
6968
|
+
blockingError = "workflow run was cancelled";
|
|
6969
|
+
break;
|
|
6970
|
+
}
|
|
6971
|
+
const pendingTimeout = opts.signal?.aborted ? opts.signalTimeoutMessage?.() : undefined;
|
|
6972
|
+
if (pendingTimeout) {
|
|
6973
|
+
terminalStatus = "timed_out";
|
|
6974
|
+
blockingError = pendingTimeout;
|
|
6975
|
+
break;
|
|
6976
|
+
}
|
|
6977
|
+
const existing = store.getWorkflowStepRun(run.id, step.id);
|
|
6978
|
+
if (existing?.status === "succeeded" || existing?.status === "skipped" || existing?.status === "cancelled")
|
|
6979
|
+
continue;
|
|
6980
|
+
const blockedBy = (step.dependsOn ?? []).find((dependencyId) => {
|
|
6981
|
+
const dependencyRun = store.getWorkflowStepRun(run.id, dependencyId);
|
|
6982
|
+
const dependencyStep = byId.get(dependencyId);
|
|
6983
|
+
if (dependencyRun?.status === "succeeded")
|
|
6984
|
+
return false;
|
|
6985
|
+
return !dependencyStep?.continueOnFailure;
|
|
6986
|
+
});
|
|
6987
|
+
if (blockedBy) {
|
|
6988
|
+
opts.beforePersist?.();
|
|
6989
|
+
if (isBlockedStepRun(store.getWorkflowStepRun(run.id, blockedBy))) {
|
|
6990
|
+
store.skipWorkflowStepRun(run.id, step.id, `${BLOCKED_STEP_ERROR_PREFIX} upstream step ${blockedBy} was blocked`, {
|
|
6991
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
6992
|
+
});
|
|
6993
|
+
continue;
|
|
6994
|
+
}
|
|
6995
|
+
store.skipWorkflowStepRun(run.id, step.id, `dependency did not succeed: ${blockedBy}`, { daemonLeaseId: opts.daemonLeaseId });
|
|
6996
|
+
blockingError ??= `step ${step.id} blocked by dependency ${blockedBy}`;
|
|
6997
|
+
terminalStatus = "failed";
|
|
6859
6998
|
continue;
|
|
6860
6999
|
}
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
|
|
6872
|
-
|
|
6873
|
-
|
|
6874
|
-
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
});
|
|
6881
|
-
let result;
|
|
6882
|
-
const controller = new AbortController;
|
|
6883
|
-
const externalAbort = () => controller.abort();
|
|
6884
|
-
if (opts.signal?.aborted)
|
|
6885
|
-
controller.abort();
|
|
6886
|
-
opts.signal?.addEventListener("abort", externalAbort, { once: true });
|
|
6887
|
-
const cancelTimer = setInterval(() => {
|
|
6888
|
-
if (store.getWorkflowRun(run.id)?.status === "cancelled")
|
|
7000
|
+
opts.beforePersist?.();
|
|
7001
|
+
const startedStep = store.startWorkflowStepRun(run.id, step.id, { daemonLeaseId: opts.daemonLeaseId });
|
|
7002
|
+
if (startedStep.status !== "running") {
|
|
7003
|
+
terminalStatus = "failed";
|
|
7004
|
+
blockingError = `step ${step.id} could not start because workflow is no longer running`;
|
|
7005
|
+
break;
|
|
7006
|
+
}
|
|
7007
|
+
const stepContext = goalExecutionContext({
|
|
7008
|
+
loop: opts.loop,
|
|
7009
|
+
loopRun: opts.loopRun,
|
|
7010
|
+
scheduledFor: opts.scheduledFor,
|
|
7011
|
+
workflow,
|
|
7012
|
+
workflowRunId: run.id,
|
|
7013
|
+
workflowStepId: step.id
|
|
7014
|
+
});
|
|
7015
|
+
let result;
|
|
7016
|
+
const controller = new AbortController;
|
|
7017
|
+
const externalAbort = () => controller.abort();
|
|
7018
|
+
if (opts.signal?.aborted)
|
|
6889
7019
|
controller.abort();
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
opts
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
7020
|
+
opts.signal?.addEventListener("abort", externalAbort, { once: true });
|
|
7021
|
+
const cancelTimer = setInterval(() => {
|
|
7022
|
+
if (store.getWorkflowRun(run.id)?.status === "cancelled")
|
|
7023
|
+
controller.abort();
|
|
7024
|
+
}, opts.cancelPollMs ?? 500);
|
|
7025
|
+
cancelTimer.unref();
|
|
7026
|
+
try {
|
|
7027
|
+
if (step.goal) {
|
|
7028
|
+
result = await runGoal(store, step.goal, {
|
|
7029
|
+
...opts,
|
|
7030
|
+
model: opts.goalModel,
|
|
7031
|
+
target: targetWithStepAccount(step),
|
|
7032
|
+
signal: controller.signal,
|
|
7033
|
+
context: stepContext
|
|
7034
|
+
});
|
|
7035
|
+
} else {
|
|
7036
|
+
result = await executeTarget(targetWithStepAccount(step, opts.goalNodePrompt), executionMetadata(stepContext), {
|
|
7037
|
+
...opts,
|
|
7038
|
+
machine: opts.machine ?? opts.loop?.machine,
|
|
7039
|
+
signal: controller.signal,
|
|
7040
|
+
onAgentProgress: (progress) => {
|
|
7041
|
+
const stdout = JSON.stringify({ agentProgress: progress }, null, 2);
|
|
7042
|
+
opts.beforePersist?.();
|
|
7043
|
+
store.recordWorkflowStepProgress(run.id, step.id, {
|
|
7044
|
+
stdout,
|
|
7045
|
+
payload: progress
|
|
7046
|
+
}, {
|
|
7047
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
7048
|
+
});
|
|
7049
|
+
opts.onAgentProgress?.(progress);
|
|
7050
|
+
},
|
|
7051
|
+
onSpawn: (pid) => {
|
|
7052
|
+
opts.beforePersist?.();
|
|
7053
|
+
store.markWorkflowStepPid(run.id, step.id, pid, { daemonLeaseId: opts.daemonLeaseId });
|
|
7054
|
+
opts.onSpawn?.(pid);
|
|
7055
|
+
}
|
|
7056
|
+
});
|
|
7057
|
+
}
|
|
7058
|
+
} catch (error) {
|
|
7059
|
+
const finishedAt2 = nowIso();
|
|
7060
|
+
result = {
|
|
7061
|
+
status: "failed",
|
|
7062
|
+
stdout: "",
|
|
7063
|
+
stderr: "",
|
|
7064
|
+
error: error instanceof Error ? error.message : String(error),
|
|
7065
|
+
startedAt: startedStep.startedAt ?? finishedAt2,
|
|
7066
|
+
finishedAt: finishedAt2,
|
|
7067
|
+
durationMs: new Date(finishedAt2).getTime() - new Date(startedStep.startedAt ?? finishedAt2).getTime()
|
|
7068
|
+
};
|
|
7069
|
+
} finally {
|
|
7070
|
+
clearInterval(cancelTimer);
|
|
7071
|
+
opts.signal?.removeEventListener("abort", externalAbort);
|
|
7072
|
+
}
|
|
7073
|
+
const timeoutMessage = opts.signal?.aborted ? opts.signalTimeoutMessage?.() : undefined;
|
|
7074
|
+
if (timeoutMessage && result.status === "failed") {
|
|
7075
|
+
result = { ...result, status: "timed_out", error: timeoutMessage };
|
|
7076
|
+
}
|
|
7077
|
+
if (store.isWorkflowRunTerminal(run.id)) {
|
|
7078
|
+
terminalStatus = store.requireWorkflowRun(run.id).status;
|
|
7079
|
+
blockingError = "workflow run was cancelled";
|
|
7080
|
+
break;
|
|
7081
|
+
}
|
|
7082
|
+
opts.beforePersist?.();
|
|
7083
|
+
const blockedExit = result.status === "failed" && result.exitCode !== undefined && blockedExitCodesForStep(step).includes(result.exitCode);
|
|
7084
|
+
if (blockedExit) {
|
|
7085
|
+
store.finalizeWorkflowStepRun(run.id, step.id, {
|
|
7086
|
+
status: "skipped",
|
|
7087
|
+
finishedAt: result.finishedAt,
|
|
7088
|
+
durationMs: result.durationMs,
|
|
7089
|
+
stdout: result.stdout,
|
|
7090
|
+
stderr: result.stderr,
|
|
7091
|
+
exitCode: result.exitCode,
|
|
7092
|
+
error: `${BLOCKED_STEP_ERROR_PREFIX} step ${step.id} exited with blocked exit code ${result.exitCode}`
|
|
7093
|
+
}, {
|
|
7094
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
6911
7095
|
});
|
|
7096
|
+
continue;
|
|
6912
7097
|
}
|
|
6913
|
-
} catch (error) {
|
|
6914
|
-
const finishedAt2 = nowIso();
|
|
6915
|
-
result = {
|
|
6916
|
-
status: "failed",
|
|
6917
|
-
stdout: "",
|
|
6918
|
-
stderr: "",
|
|
6919
|
-
error: error instanceof Error ? error.message : String(error),
|
|
6920
|
-
startedAt: startedStep.startedAt ?? finishedAt2,
|
|
6921
|
-
finishedAt: finishedAt2,
|
|
6922
|
-
durationMs: new Date(finishedAt2).getTime() - new Date(startedStep.startedAt ?? finishedAt2).getTime()
|
|
6923
|
-
};
|
|
6924
|
-
} finally {
|
|
6925
|
-
clearInterval(cancelTimer);
|
|
6926
|
-
opts.signal?.removeEventListener("abort", externalAbort);
|
|
6927
|
-
}
|
|
6928
|
-
const timeoutMessage = opts.signal?.aborted ? opts.signalTimeoutMessage?.() : undefined;
|
|
6929
|
-
if (timeoutMessage && result.status === "failed") {
|
|
6930
|
-
result = { ...result, status: "timed_out", error: timeoutMessage };
|
|
6931
|
-
}
|
|
6932
|
-
if (store.isWorkflowRunTerminal(run.id)) {
|
|
6933
|
-
terminalStatus = store.requireWorkflowRun(run.id).status;
|
|
6934
|
-
blockingError = "workflow run was cancelled";
|
|
6935
|
-
break;
|
|
6936
|
-
}
|
|
6937
|
-
opts.beforePersist?.();
|
|
6938
|
-
const blockedExit = result.status === "failed" && result.exitCode !== undefined && blockedExitCodesForStep(step).includes(result.exitCode);
|
|
6939
|
-
if (blockedExit) {
|
|
6940
7098
|
store.finalizeWorkflowStepRun(run.id, step.id, {
|
|
6941
|
-
status:
|
|
7099
|
+
status: result.status,
|
|
6942
7100
|
finishedAt: result.finishedAt,
|
|
6943
7101
|
durationMs: result.durationMs,
|
|
6944
7102
|
stdout: result.stdout,
|
|
6945
7103
|
stderr: result.stderr,
|
|
6946
7104
|
exitCode: result.exitCode,
|
|
6947
|
-
error:
|
|
7105
|
+
error: result.error
|
|
6948
7106
|
}, {
|
|
6949
7107
|
daemonLeaseId: opts.daemonLeaseId
|
|
6950
7108
|
});
|
|
6951
|
-
|
|
7109
|
+
if (result.status !== "succeeded" && !step.continueOnFailure) {
|
|
7110
|
+
terminalStatus = result.status;
|
|
7111
|
+
blockingError = `step ${step.id} ${result.status}${result.error ? `: ${result.error}` : ""}`;
|
|
7112
|
+
break;
|
|
7113
|
+
}
|
|
6952
7114
|
}
|
|
6953
|
-
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
7115
|
+
if (terminalStatus !== "succeeded") {
|
|
7116
|
+
for (const step of ordered) {
|
|
7117
|
+
const existing = store.getWorkflowStepRun(run.id, step.id);
|
|
7118
|
+
if (existing?.status === "pending" || existing?.status === "running") {
|
|
7119
|
+
store.skipWorkflowStepRun(run.id, step.id, blockingError ?? "workflow stopped before step could run", {
|
|
7120
|
+
daemonLeaseId: opts.daemonLeaseId
|
|
7121
|
+
});
|
|
7122
|
+
}
|
|
7123
|
+
}
|
|
7124
|
+
}
|
|
7125
|
+
const finishedAt = nowIso();
|
|
7126
|
+
if (store.isWorkflowRunTerminal(run.id)) {
|
|
7127
|
+
const terminalRun = store.requireWorkflowRun(run.id);
|
|
7128
|
+
return workflowResult(terminalRun, terminalRun.status, startedAt, terminalRun.finishedAt ?? finishedAt, store.listWorkflowStepRuns(run.id), terminalRun.error ?? blockingError);
|
|
7129
|
+
}
|
|
7130
|
+
opts.beforePersist?.();
|
|
7131
|
+
const finalRun = store.finalizeWorkflowRun(run.id, terminalStatus, {
|
|
7132
|
+
finishedAt,
|
|
7133
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime(),
|
|
7134
|
+
error: blockingError
|
|
6961
7135
|
}, {
|
|
6962
7136
|
daemonLeaseId: opts.daemonLeaseId
|
|
6963
7137
|
});
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
if (
|
|
6974
|
-
store.
|
|
7138
|
+
return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, store.listWorkflowStepRuns(run.id), blockingError);
|
|
7139
|
+
} catch (error) {
|
|
7140
|
+
if (opts.daemonLeaseId && error instanceof Error && error.message === "daemon lease lost")
|
|
7141
|
+
throw error;
|
|
7142
|
+
if (error instanceof Error && error.message.includes("workflow step is not claimable"))
|
|
7143
|
+
throw error;
|
|
7144
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7145
|
+
const finishedAt = nowIso();
|
|
7146
|
+
try {
|
|
7147
|
+
if (!store.isWorkflowRunTerminal(run.id)) {
|
|
7148
|
+
store.finalizeWorkflowRun(run.id, "failed", {
|
|
7149
|
+
finishedAt,
|
|
7150
|
+
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime(),
|
|
7151
|
+
error: message
|
|
7152
|
+
}, {
|
|
6975
7153
|
daemonLeaseId: opts.daemonLeaseId
|
|
6976
7154
|
});
|
|
6977
7155
|
}
|
|
6978
|
-
}
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
const terminalRun = store.requireWorkflowRun(run.id);
|
|
6983
|
-
return workflowResult(terminalRun, terminalRun.status, startedAt, terminalRun.finishedAt ?? finishedAt, store.listWorkflowStepRuns(run.id), terminalRun.error ?? blockingError);
|
|
7156
|
+
} catch {}
|
|
7157
|
+
const current = store.getWorkflowRun(run.id) ?? run;
|
|
7158
|
+
const resultStatus = current.status === "running" ? "failed" : current.status;
|
|
7159
|
+
return workflowResult(current, resultStatus, startedAt, finishedAt, store.listWorkflowStepRuns(run.id), current.error ?? message);
|
|
6984
7160
|
}
|
|
6985
|
-
opts.beforePersist?.();
|
|
6986
|
-
const finalRun = store.finalizeWorkflowRun(run.id, terminalStatus, {
|
|
6987
|
-
finishedAt,
|
|
6988
|
-
durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime(),
|
|
6989
|
-
error: blockingError
|
|
6990
|
-
}, {
|
|
6991
|
-
daemonLeaseId: opts.daemonLeaseId
|
|
6992
|
-
});
|
|
6993
|
-
return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, store.listWorkflowStepRuns(run.id), blockingError);
|
|
6994
7161
|
}
|
|
6995
7162
|
function preflightWorkflow(workflow, opts = {}) {
|
|
6996
7163
|
return workflowExecutionOrder(workflow).map((step) => {
|
|
@@ -7361,6 +7528,25 @@ function advanceOptions(deps) {
|
|
|
7361
7528
|
onRun: deps.onRun
|
|
7362
7529
|
};
|
|
7363
7530
|
}
|
|
7531
|
+
var TERMINAL_RUN_STATUSES2 = new Set([
|
|
7532
|
+
"succeeded",
|
|
7533
|
+
"failed",
|
|
7534
|
+
"timed_out",
|
|
7535
|
+
"abandoned",
|
|
7536
|
+
"skipped"
|
|
7537
|
+
]);
|
|
7538
|
+
function repairWedgedTerminalSlot(deps, loop, scheduledFor, now) {
|
|
7539
|
+
const existing = deps.store.getRunBySlot(loop.id, scheduledFor);
|
|
7540
|
+
if (!existing || !TERMINAL_RUN_STATUSES2.has(existing.status))
|
|
7541
|
+
return;
|
|
7542
|
+
try {
|
|
7543
|
+
advanceLoop(deps.store, loop, existing, new Date(existing.finishedAt ?? now), existing.status === "succeeded", advanceOptions(deps));
|
|
7544
|
+
} catch (error) {
|
|
7545
|
+
if (deps.daemonLeaseId && isDaemonLeaseLost(error))
|
|
7546
|
+
return;
|
|
7547
|
+
throw error;
|
|
7548
|
+
}
|
|
7549
|
+
}
|
|
7364
7550
|
async function runSlot(deps, loop, scheduledFor) {
|
|
7365
7551
|
const now = deps.now?.() ?? new Date;
|
|
7366
7552
|
deps.beforeRun?.(loop, scheduledFor);
|
|
@@ -7387,8 +7573,10 @@ async function runSlot(deps, loop, scheduledFor) {
|
|
|
7387
7573
|
return;
|
|
7388
7574
|
throw error;
|
|
7389
7575
|
}
|
|
7390
|
-
if (!claim)
|
|
7576
|
+
if (!claim) {
|
|
7577
|
+
repairWedgedTerminalSlot(deps, loop, scheduledFor, now);
|
|
7391
7578
|
return;
|
|
7579
|
+
}
|
|
7392
7580
|
deps.beforeRun?.(claim.loop, claim.run.scheduledFor);
|
|
7393
7581
|
deps.onRun?.(claim.run);
|
|
7394
7582
|
const finalRun = await executeClaimedRun({
|
|
@@ -7434,8 +7622,10 @@ function claimSlot(deps, loop, scheduledFor) {
|
|
|
7434
7622
|
return;
|
|
7435
7623
|
throw error;
|
|
7436
7624
|
}
|
|
7437
|
-
if (!claim)
|
|
7625
|
+
if (!claim) {
|
|
7626
|
+
repairWedgedTerminalSlot(deps, loop, scheduledFor, now);
|
|
7438
7627
|
return;
|
|
7628
|
+
}
|
|
7439
7629
|
deps.beforeRun?.(claim.loop, claim.run.scheduledFor);
|
|
7440
7630
|
deps.onRun?.(claim.run);
|
|
7441
7631
|
return claim;
|
|
@@ -7529,7 +7719,7 @@ async function tick(deps) {
|
|
|
7529
7719
|
// package.json
|
|
7530
7720
|
var package_default = {
|
|
7531
7721
|
name: "@hasna/loops",
|
|
7532
|
-
version: "0.4.
|
|
7722
|
+
version: "0.4.7",
|
|
7533
7723
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
7534
7724
|
type: "module",
|
|
7535
7725
|
main: "dist/index.js",
|
|
@@ -8076,7 +8266,7 @@ var TOOL_REGISTRATIONS = [
|
|
|
8076
8266
|
guarded: true,
|
|
8077
8267
|
annotations: mutationAnnotations({ idempotent: true }),
|
|
8078
8268
|
inputSchema: { idOrName: loopIdOrNameSchema },
|
|
8079
|
-
handler: ({ idOrName }) => withStore((store) => ({ loop: publicLoop(store.updateLoop(store.
|
|
8269
|
+
handler: ({ idOrName }) => withStore((store) => ({ loop: publicLoop(store.updateLoop(store.requireUniqueLoop(idOrName).id, { status: "paused" })) }))
|
|
8080
8270
|
},
|
|
8081
8271
|
{
|
|
8082
8272
|
name: "loops_resume",
|
|
@@ -8086,7 +8276,15 @@ var TOOL_REGISTRATIONS = [
|
|
|
8086
8276
|
guarded: true,
|
|
8087
8277
|
annotations: mutationAnnotations({ idempotent: true }),
|
|
8088
8278
|
inputSchema: { idOrName: loopIdOrNameSchema },
|
|
8089
|
-
handler: ({ idOrName }) => withStore((store) =>
|
|
8279
|
+
handler: ({ idOrName }) => withStore((store) => {
|
|
8280
|
+
const loop = store.requireUniqueLoop(idOrName);
|
|
8281
|
+
let nextRunAt = loop.nextRunAt;
|
|
8282
|
+
if (!nextRunAt) {
|
|
8283
|
+
const now = new Date;
|
|
8284
|
+
nextRunAt = computeNextAfter(loop.schedule, now, now);
|
|
8285
|
+
}
|
|
8286
|
+
return { loop: publicLoop(store.updateLoop(loop.id, { status: "active", nextRunAt })) };
|
|
8287
|
+
})
|
|
8090
8288
|
},
|
|
8091
8289
|
{
|
|
8092
8290
|
name: "loops_stop",
|
|
@@ -8097,7 +8295,7 @@ var TOOL_REGISTRATIONS = [
|
|
|
8097
8295
|
annotations: mutationAnnotations({ idempotent: true }),
|
|
8098
8296
|
inputSchema: { idOrName: loopIdOrNameSchema },
|
|
8099
8297
|
handler: ({ idOrName }) => withStore((store) => ({
|
|
8100
|
-
loop: publicLoop(store.updateLoop(store.
|
|
8298
|
+
loop: publicLoop(store.updateLoop(store.requireUniqueLoop(idOrName).id, { status: "stopped", nextRunAt: undefined }))
|
|
8101
8299
|
}))
|
|
8102
8300
|
},
|
|
8103
8301
|
{
|
|
@@ -8110,7 +8308,7 @@ var TOOL_REGISTRATIONS = [
|
|
|
8110
8308
|
inputSchema: { idOrName: loopIdOrNameSchema },
|
|
8111
8309
|
handler: ({ idOrName }) => withStore(async (store) => {
|
|
8112
8310
|
const daemon = daemonStatus(store);
|
|
8113
|
-
const result = await runLoopNow({ store, idOrName, runnerId: `mcp:${process.pid}`, mode: "schedule" });
|
|
8311
|
+
const result = await runLoopNow({ store, idOrName: store.requireUniqueLoop(idOrName).id, runnerId: `mcp:${process.pid}`, mode: "schedule" });
|
|
8114
8312
|
return {
|
|
8115
8313
|
scheduledFor: result.scheduledFor,
|
|
8116
8314
|
loop: publicLoop(result.loop),
|