@hasna/loops 0.4.13 → 0.4.22
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 +99 -3
- package/README.md +171 -39
- package/dist/api/index.d.ts +36 -0
- package/dist/api/index.js +1518 -15
- package/dist/cli/index.js +7280 -3213
- package/dist/cli/ui.d.ts +44 -0
- package/dist/daemon/index.js +1433 -303
- package/dist/generated/storage-kit/health.d.ts +19 -0
- package/dist/generated/storage-kit/index.d.ts +7 -0
- package/dist/generated/storage-kit/migrations.d.ts +47 -0
- package/dist/generated/storage-kit/mode.d.ts +47 -0
- package/dist/generated/storage-kit/pool.d.ts +33 -0
- package/dist/generated/storage-kit/query.d.ts +35 -0
- package/dist/generated/storage-kit/tls.d.ts +25 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +8689 -4837
- package/dist/lib/cloud/mode.d.ts +17 -0
- package/dist/lib/cloud/resolve.d.ts +16 -0
- package/dist/lib/cloud/storage.d.ts +82 -0
- package/dist/lib/cloud/transport.d.ts +148 -0
- package/dist/lib/format.d.ts +2 -1
- package/dist/lib/health.d.ts +83 -3
- package/dist/lib/migration.d.ts +40 -0
- package/dist/lib/mode.js +15 -3
- package/dist/lib/route/index.d.ts +2 -0
- package/dist/lib/route/policies.d.ts +51 -0
- package/dist/lib/route/provider-admission.d.ts +37 -0
- package/dist/lib/route/throttle.d.ts +4 -0
- package/dist/lib/route/types.d.ts +8 -0
- package/dist/lib/run-receipts.d.ts +14 -0
- package/dist/lib/scheduler.d.ts +5 -2
- package/dist/lib/storage/contract.d.ts +7 -1
- package/dist/lib/storage/index.d.ts +1 -0
- package/dist/lib/storage/index.js +2028 -231
- package/dist/lib/storage/pg-executor.d.ts +27 -0
- package/dist/lib/storage/pg-runner-claim.d.ts +40 -0
- package/dist/lib/storage/postgres-loop-storage.d.ts +120 -0
- package/dist/lib/storage/postgres-schema.js +29 -0
- package/dist/lib/storage/postgres.js +29 -0
- package/dist/lib/storage/sqlite.d.ts +6 -0
- package/dist/lib/storage/sqlite.js +748 -26
- package/dist/lib/store/index.d.ts +268 -0
- package/dist/lib/store.d.ts +282 -1
- package/dist/lib/store.js +746 -26
- package/dist/lib/template-kit.d.ts +25 -0
- package/dist/lib/templates.d.ts +16 -1
- package/dist/mcp/http.d.ts +16 -0
- package/dist/mcp/index.js +2885 -634
- package/dist/runner/index.js +198 -32
- package/dist/sdk/http.d.ts +182 -0
- package/dist/sdk/http.js +166 -0
- package/dist/sdk/index.d.ts +55 -18
- package/dist/sdk/index.js +2734 -617
- package/dist/serve/index.d.ts +4 -0
- package/dist/serve/index.js +9095 -0
- package/dist/types.d.ts +52 -0
- package/docs/AUTOMATION_RUNTIME_DESIGN.md +442 -1
- package/docs/CUTOVER-RUNBOOK.md +78 -0
- package/docs/DEPLOYMENT_MODES.md +35 -18
- package/docs/RUNTIME_BOUNDARY.md +203 -0
- package/docs/USAGE.md +195 -38
- package/package.json +15 -3
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// src/lib/store.ts
|
|
3
3
|
import { Database } from "bun:sqlite";
|
|
4
|
-
import { chmodSync, existsSync, mkdirSync as mkdirSync3, mkdtempSync, rmSync as
|
|
5
|
-
import { tmpdir } from "os";
|
|
6
|
-
import { basename as basename2, dirname as dirname2, join as
|
|
4
|
+
import { chmodSync, existsSync, mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync2, rmSync as rmSync3 } from "fs";
|
|
5
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
6
|
+
import { basename as basename2, dirname as dirname2, join as join4 } from "path";
|
|
7
7
|
|
|
8
8
|
// src/lib/errors.ts
|
|
9
9
|
class CodedError extends Error {
|
|
@@ -1202,15 +1202,378 @@ function discardWorkflowRunManifest(staged) {
|
|
|
1202
1202
|
} catch {}
|
|
1203
1203
|
}
|
|
1204
1204
|
|
|
1205
|
+
// src/lib/run-receipts.ts
|
|
1206
|
+
import { createHash as createHash2 } from "crypto";
|
|
1207
|
+
import { hostname } from "os";
|
|
1208
|
+
|
|
1209
|
+
// src/lib/run-envelope.ts
|
|
1210
|
+
var ENVELOPE_EXCERPT_CHARS = 2048;
|
|
1211
|
+
var BLOCKED_STEP_ERROR_PREFIX = "blocked:";
|
|
1212
|
+
function boundedExcerpt(text, limit = ENVELOPE_EXCERPT_CHARS) {
|
|
1213
|
+
if (!text)
|
|
1214
|
+
return;
|
|
1215
|
+
const scrubbed = scrubSecrets(text);
|
|
1216
|
+
if (scrubbed.length <= limit * 2)
|
|
1217
|
+
return scrubbed;
|
|
1218
|
+
const omitted = scrubbed.length - limit * 2;
|
|
1219
|
+
return `${scrubbed.slice(0, limit)}
|
|
1220
|
+
[... ${omitted} chars omitted ...]
|
|
1221
|
+
${scrubbed.slice(-limit)}`;
|
|
1222
|
+
}
|
|
1223
|
+
function summarizeOutput(stdout, stderr) {
|
|
1224
|
+
return {
|
|
1225
|
+
stdoutBytes: stdout ? Buffer.byteLength(stdout, "utf8") : 0,
|
|
1226
|
+
stderrBytes: stderr ? Buffer.byteLength(stderr, "utf8") : 0,
|
|
1227
|
+
stdoutExcerpt: boundedExcerpt(stdout),
|
|
1228
|
+
stderrExcerpt: boundedExcerpt(stderr)
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
function summarizeExecutorResult(result) {
|
|
1232
|
+
return {
|
|
1233
|
+
...summarizeOutput(result.stdout, result.stderr),
|
|
1234
|
+
status: result.status,
|
|
1235
|
+
exitCode: result.exitCode,
|
|
1236
|
+
durationMs: result.durationMs,
|
|
1237
|
+
error: boundedExcerpt(result.error)
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
function isBlockedStepRun(step) {
|
|
1241
|
+
return step?.status === "skipped" && (step.error?.startsWith(BLOCKED_STEP_ERROR_PREFIX) ?? false);
|
|
1242
|
+
}
|
|
1243
|
+
function summarizeWorkflowStepRun(step) {
|
|
1244
|
+
return {
|
|
1245
|
+
...summarizeOutput(step.stdout, step.stderr),
|
|
1246
|
+
stepId: step.stepId,
|
|
1247
|
+
status: step.status,
|
|
1248
|
+
exitCode: step.exitCode,
|
|
1249
|
+
durationMs: step.durationMs,
|
|
1250
|
+
error: boundedExcerpt(step.error),
|
|
1251
|
+
blocked: isBlockedStepRun(step)
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
function workflowRunEnvelope(run, steps) {
|
|
1255
|
+
return JSON.stringify({ workflowRun: run, steps: steps.map(summarizeWorkflowStepRun) }, null, 2);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// src/lib/run-receipts.ts
|
|
1259
|
+
var RUN_RECEIPT_SUMMARY_TEXT_CHARS = 4096;
|
|
1260
|
+
var RUN_RECEIPT_MAX_IDS = 100;
|
|
1261
|
+
var RUN_RECEIPT_MAX_EVIDENCE_PATHS = 100;
|
|
1262
|
+
var RUN_RECEIPT_MAX_PATH_CHARS = 1024;
|
|
1263
|
+
function canonicalJson(value) {
|
|
1264
|
+
if (Array.isArray(value))
|
|
1265
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
1266
|
+
if (value && typeof value === "object") {
|
|
1267
|
+
return `{${Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
|
|
1268
|
+
}
|
|
1269
|
+
return JSON.stringify(value);
|
|
1270
|
+
}
|
|
1271
|
+
function digestReceipt(receipt) {
|
|
1272
|
+
return `sha256:${createHash2("sha256").update(canonicalJson(receipt)).digest("hex")}`;
|
|
1273
|
+
}
|
|
1274
|
+
function isoOrNull(value, label) {
|
|
1275
|
+
if (value === undefined || value === null || value === "")
|
|
1276
|
+
return null;
|
|
1277
|
+
const date = new Date(value);
|
|
1278
|
+
if (Number.isNaN(date.getTime()))
|
|
1279
|
+
throw new Error(`${label} must be a valid ISO date/time`);
|
|
1280
|
+
return date.toISOString();
|
|
1281
|
+
}
|
|
1282
|
+
function nonEmpty(value, label) {
|
|
1283
|
+
const trimmed = value?.trim();
|
|
1284
|
+
if (!trimmed)
|
|
1285
|
+
throw new Error(`${label} must be non-empty`);
|
|
1286
|
+
return trimmed;
|
|
1287
|
+
}
|
|
1288
|
+
function normalizedStringArray(values, opts) {
|
|
1289
|
+
const out = [];
|
|
1290
|
+
const seen = new Set;
|
|
1291
|
+
for (const raw of values ?? []) {
|
|
1292
|
+
const value = String(raw).trim();
|
|
1293
|
+
if (!value || seen.has(value))
|
|
1294
|
+
continue;
|
|
1295
|
+
seen.add(value);
|
|
1296
|
+
out.push(opts.itemMax ? value.slice(0, opts.itemMax) : value);
|
|
1297
|
+
if (out.length >= opts.max)
|
|
1298
|
+
break;
|
|
1299
|
+
}
|
|
1300
|
+
if ((values?.length ?? 0) > opts.max) {
|
|
1301
|
+
out.push(`[truncated ${values.length - opts.max} ${opts.label}]`);
|
|
1302
|
+
}
|
|
1303
|
+
return out;
|
|
1304
|
+
}
|
|
1305
|
+
function receiptMachine(input, opts) {
|
|
1306
|
+
if (typeof input.machine === "string")
|
|
1307
|
+
return nonEmpty(input.machine, "machine");
|
|
1308
|
+
if (input.machine && typeof input.machine === "object")
|
|
1309
|
+
return input.machine;
|
|
1310
|
+
if (opts.loop?.machine)
|
|
1311
|
+
return { ...opts.loop.machine };
|
|
1312
|
+
if (typeof opts.defaultMachine === "string")
|
|
1313
|
+
return nonEmpty(opts.defaultMachine, "machine");
|
|
1314
|
+
if (opts.defaultMachine && typeof opts.defaultMachine === "object")
|
|
1315
|
+
return opts.defaultMachine;
|
|
1316
|
+
return hostname();
|
|
1317
|
+
}
|
|
1318
|
+
function normalizeSummary(input, run) {
|
|
1319
|
+
const stdout = input.stdout ?? run?.stdout;
|
|
1320
|
+
const stderr = input.stderr ?? run?.stderr;
|
|
1321
|
+
const output = summarizeOutput(stdout, stderr);
|
|
1322
|
+
const summaryInput = input.summary;
|
|
1323
|
+
const provided = typeof summaryInput === "object" && summaryInput !== null ? summaryInput : {};
|
|
1324
|
+
const text = typeof summaryInput === "string" ? boundedExcerpt(summaryInput, Math.floor(RUN_RECEIPT_SUMMARY_TEXT_CHARS / 2)) : typeof provided.text === "string" ? boundedExcerpt(provided.text, Math.floor(RUN_RECEIPT_SUMMARY_TEXT_CHARS / 2)) : undefined;
|
|
1325
|
+
return {
|
|
1326
|
+
text,
|
|
1327
|
+
stdout_bytes: Number.isFinite(provided.stdout_bytes) ? Number(provided.stdout_bytes) : output.stdoutBytes,
|
|
1328
|
+
stderr_bytes: Number.isFinite(provided.stderr_bytes) ? Number(provided.stderr_bytes) : output.stderrBytes,
|
|
1329
|
+
stdout_excerpt: typeof provided.stdout_excerpt === "string" ? boundedExcerpt(provided.stdout_excerpt) : output.stdoutExcerpt,
|
|
1330
|
+
stderr_excerpt: typeof provided.stderr_excerpt === "string" ? boundedExcerpt(provided.stderr_excerpt) : output.stderrExcerpt,
|
|
1331
|
+
error: typeof provided.error === "string" ? boundedExcerpt(provided.error) : boundedExcerpt(input.error ?? run?.error),
|
|
1332
|
+
duration_ms: Number.isFinite(provided.duration_ms) ? Number(provided.duration_ms) : input.duration_ms ?? run?.durationMs
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
function normalizeRunReceipt(input, opts = {}) {
|
|
1336
|
+
const now = (opts.now ?? new Date).toISOString();
|
|
1337
|
+
const run = opts.run;
|
|
1338
|
+
const loopId = input.loop_id ?? run?.loopId ?? opts.loop?.id;
|
|
1339
|
+
const normalized = {
|
|
1340
|
+
loop_id: nonEmpty(loopId, "loop_id"),
|
|
1341
|
+
run_id: nonEmpty(input.run_id ?? run?.id, "run_id"),
|
|
1342
|
+
machine: receiptMachine(input, opts),
|
|
1343
|
+
repo: nonEmpty(input.repo ?? opts.defaultRepo ?? targetRepo(opts.loop) ?? process.cwd(), "repo"),
|
|
1344
|
+
task_ids: normalizedStringArray(input.task_ids, { max: RUN_RECEIPT_MAX_IDS, label: "task_ids" }),
|
|
1345
|
+
knowledge_ids: normalizedStringArray(input.knowledge_ids, { max: RUN_RECEIPT_MAX_IDS, label: "knowledge_ids" }),
|
|
1346
|
+
started_at: isoOrNull(input.started_at ?? run?.startedAt, "started_at"),
|
|
1347
|
+
finished_at: isoOrNull(input.finished_at ?? run?.finishedAt, "finished_at"),
|
|
1348
|
+
status: nonEmpty(input.status ?? run?.status, "status"),
|
|
1349
|
+
exit_code: input.exit_code === undefined ? run?.exitCode ?? null : input.exit_code,
|
|
1350
|
+
summary: normalizeSummary(input, run),
|
|
1351
|
+
evidence_paths: normalizedStringArray(input.evidence_paths, {
|
|
1352
|
+
max: RUN_RECEIPT_MAX_EVIDENCE_PATHS,
|
|
1353
|
+
label: "evidence_paths",
|
|
1354
|
+
itemMax: RUN_RECEIPT_MAX_PATH_CHARS
|
|
1355
|
+
})
|
|
1356
|
+
};
|
|
1357
|
+
return {
|
|
1358
|
+
...normalized,
|
|
1359
|
+
digest_id: input.digest_id?.trim() || digestReceipt(normalized),
|
|
1360
|
+
created_at: opts.existing?.created_at ?? now,
|
|
1361
|
+
updated_at: now
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
function targetRepo(loop) {
|
|
1365
|
+
if (!loop)
|
|
1366
|
+
return;
|
|
1367
|
+
if (loop.target.type === "command")
|
|
1368
|
+
return loop.target.cwd;
|
|
1369
|
+
if (loop.target.type === "agent")
|
|
1370
|
+
return loop.target.cwd;
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// src/lib/route/todos-cli.ts
|
|
1375
|
+
import { closeSync, mkdtempSync, openSync, readFileSync as readFileSync3, rmSync as rmSync2 } from "fs";
|
|
1376
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1377
|
+
import { join as join3 } from "path";
|
|
1378
|
+
import { homedir as homedir2, tmpdir } from "os";
|
|
1379
|
+
|
|
1380
|
+
// src/lib/format.ts
|
|
1381
|
+
var TEXT_OUTPUT_LIMIT = 32 * 1024;
|
|
1382
|
+
var SENSITIVE_PAYLOAD_KEYS = new Set(["env", "error", "prompt", "reason", "stderr", "stdout"]);
|
|
1383
|
+
function redact(value, visible = 0) {
|
|
1384
|
+
if (!value)
|
|
1385
|
+
return value;
|
|
1386
|
+
if (value.length <= visible)
|
|
1387
|
+
return value;
|
|
1388
|
+
if (visible <= 0)
|
|
1389
|
+
return `[redacted ${value.length} chars]`;
|
|
1390
|
+
return `${value.slice(0, visible)}... [redacted ${value.length - visible} chars]`;
|
|
1391
|
+
}
|
|
1392
|
+
function truncateTextOutput(value) {
|
|
1393
|
+
if (value.length <= TEXT_OUTPUT_LIMIT)
|
|
1394
|
+
return value;
|
|
1395
|
+
return `${value.slice(0, TEXT_OUTPUT_LIMIT)}
|
|
1396
|
+
[truncated ${value.length - TEXT_OUTPUT_LIMIT} chars]`;
|
|
1397
|
+
}
|
|
1398
|
+
function redactSensitivePayload(value, key) {
|
|
1399
|
+
if (key && SENSITIVE_PAYLOAD_KEYS.has(key)) {
|
|
1400
|
+
if (typeof value === "string")
|
|
1401
|
+
return redact(value);
|
|
1402
|
+
if (value === undefined || value === null)
|
|
1403
|
+
return value;
|
|
1404
|
+
return "[redacted]";
|
|
1405
|
+
}
|
|
1406
|
+
if (Array.isArray(value))
|
|
1407
|
+
return value.map((item) => redactSensitivePayload(item));
|
|
1408
|
+
if (value && typeof value === "object") {
|
|
1409
|
+
return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [entryKey, redactSensitivePayload(entryValue, entryKey)]));
|
|
1410
|
+
}
|
|
1411
|
+
return value;
|
|
1412
|
+
}
|
|
1413
|
+
function textOutputBlocks(value, opts = {}) {
|
|
1414
|
+
const indent = opts.indent ?? "";
|
|
1415
|
+
const nested = `${indent} `;
|
|
1416
|
+
const blocks = [];
|
|
1417
|
+
for (const [label, output] of [
|
|
1418
|
+
["stdout", value.stdout],
|
|
1419
|
+
["stderr", value.stderr]
|
|
1420
|
+
]) {
|
|
1421
|
+
if (!output)
|
|
1422
|
+
continue;
|
|
1423
|
+
blocks.push(`${indent}${label}:`);
|
|
1424
|
+
for (const line of truncateTextOutput(output).replace(/\s+$/, "").split(/\r?\n/)) {
|
|
1425
|
+
blocks.push(`${nested}${line}`);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
return blocks;
|
|
1429
|
+
}
|
|
1430
|
+
function publicLoop(loop) {
|
|
1431
|
+
const target = loop.target.type === "command" ? { ...loop.target, env: loop.target.env ? "[redacted]" : undefined } : loop.target.type === "agent" ? { ...loop.target, prompt: redact(loop.target.prompt) } : loop.target;
|
|
1432
|
+
return {
|
|
1433
|
+
...loop,
|
|
1434
|
+
target
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
function publicRun(run, showOutput = false, opts = {}) {
|
|
1438
|
+
return {
|
|
1439
|
+
...run,
|
|
1440
|
+
stdout: showOutput ? run.stdout : run.stdout ? `[redacted ${run.stdout.length} chars]` : undefined,
|
|
1441
|
+
stderr: showOutput ? run.stderr : run.stderr ? `[redacted ${run.stderr.length} chars]` : undefined,
|
|
1442
|
+
error: opts.redactError ? redact(run.error) : run.error
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
function publicRunReceipt(receipt) {
|
|
1446
|
+
return { ...receipt };
|
|
1447
|
+
}
|
|
1448
|
+
function publicExecutorResult(result, showOutput = false) {
|
|
1449
|
+
return {
|
|
1450
|
+
...result,
|
|
1451
|
+
stdout: showOutput ? result.stdout : result.stdout ? `[redacted ${result.stdout.length} chars]` : undefined,
|
|
1452
|
+
stderr: showOutput ? result.stderr : result.stderr ? `[redacted ${result.stderr.length} chars]` : undefined,
|
|
1453
|
+
error: redact(result.error)
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
function publicWorkflow(workflow) {
|
|
1457
|
+
return {
|
|
1458
|
+
...workflow,
|
|
1459
|
+
steps: workflow.steps.map((step) => ({
|
|
1460
|
+
...step,
|
|
1461
|
+
target: step.target.type === "agent" ? { ...step.target, prompt: redact(step.target.prompt) } : step.target.type === "command" && step.target.env ? { ...step.target, env: "[redacted]" } : step.target
|
|
1462
|
+
}))
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
function publicWorkflowRun(run) {
|
|
1466
|
+
return { ...run, error: redact(run.error) };
|
|
1467
|
+
}
|
|
1468
|
+
function publicWorkflowInvocation(invocation) {
|
|
1469
|
+
return redactSensitivePayload(invocation);
|
|
1470
|
+
}
|
|
1471
|
+
function publicWorkflowWorkItem(item) {
|
|
1472
|
+
return { ...item, lastReason: redact(item.lastReason, 240) };
|
|
1473
|
+
}
|
|
1474
|
+
function publicWorkflowStepRun(run, showOutput = false) {
|
|
1475
|
+
return {
|
|
1476
|
+
...run,
|
|
1477
|
+
stdout: showOutput ? run.stdout : run.stdout ? `[redacted ${run.stdout.length} chars]` : undefined,
|
|
1478
|
+
stderr: showOutput ? run.stderr : run.stderr ? `[redacted ${run.stderr.length} chars]` : undefined,
|
|
1479
|
+
error: redact(run.error)
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
function publicWorkflowEvent(event) {
|
|
1483
|
+
return { ...event, payload: redactSensitivePayload(event.payload) };
|
|
1484
|
+
}
|
|
1485
|
+
function publicGoal(goal) {
|
|
1486
|
+
return {
|
|
1487
|
+
...goal,
|
|
1488
|
+
objective: redact(goal.objective, 120)
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
function publicGoalRun(run) {
|
|
1492
|
+
return {
|
|
1493
|
+
...run,
|
|
1494
|
+
evidence: redactSensitivePayload(run.evidence),
|
|
1495
|
+
rawResponse: redactSensitivePayload(run.rawResponse)
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// src/lib/route/todos-cli.ts
|
|
1500
|
+
function defaultLoopsProject() {
|
|
1501
|
+
return process.env.LOOPS_TASK_PROJECT || process.env.LOOPS_DATA_DIR || `${process.env.HOME || homedir2()}/.hasna/loops`;
|
|
1502
|
+
}
|
|
1503
|
+
function runLocalCommand(command, args, opts = {}) {
|
|
1504
|
+
const result = spawnSync2(command, args, {
|
|
1505
|
+
input: opts.input,
|
|
1506
|
+
encoding: "utf8",
|
|
1507
|
+
timeout: opts.timeoutMs ?? 30000,
|
|
1508
|
+
maxBuffer: opts.maxBuffer ?? 8 * 1024 * 1024,
|
|
1509
|
+
env: process.env
|
|
1510
|
+
});
|
|
1511
|
+
return {
|
|
1512
|
+
ok: result.status === 0,
|
|
1513
|
+
status: result.status,
|
|
1514
|
+
stdout: result.stdout || "",
|
|
1515
|
+
stderr: result.stderr || "",
|
|
1516
|
+
error: result.error ? String(result.error.message || result.error) : ""
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
function runLocalCommandWithStdoutFile(command, args, opts = {}) {
|
|
1520
|
+
const tempDir = mkdtempSync(join3(tmpdir(), "loops-command-output-"));
|
|
1521
|
+
const stdoutPath = join3(tempDir, "stdout");
|
|
1522
|
+
const stdoutFd = openSync(stdoutPath, "w");
|
|
1523
|
+
let result;
|
|
1524
|
+
try {
|
|
1525
|
+
result = spawnSync2(command, args, {
|
|
1526
|
+
input: opts.input,
|
|
1527
|
+
encoding: "utf8",
|
|
1528
|
+
timeout: opts.timeoutMs ?? 30000,
|
|
1529
|
+
maxBuffer: opts.maxBuffer ?? 8 * 1024 * 1024,
|
|
1530
|
+
env: process.env,
|
|
1531
|
+
stdio: ["pipe", stdoutFd, "pipe"]
|
|
1532
|
+
});
|
|
1533
|
+
} finally {
|
|
1534
|
+
closeSync(stdoutFd);
|
|
1535
|
+
}
|
|
1536
|
+
try {
|
|
1537
|
+
return {
|
|
1538
|
+
ok: result.status === 0,
|
|
1539
|
+
status: result.status,
|
|
1540
|
+
stdout: readFileSync3(stdoutPath, "utf8"),
|
|
1541
|
+
stderr: typeof result.stderr === "string" ? result.stderr : result.stderr?.toString() || "",
|
|
1542
|
+
error: result.error ? String(result.error.message || result.error) : ""
|
|
1543
|
+
};
|
|
1544
|
+
} finally {
|
|
1545
|
+
rmSync2(tempDir, { recursive: true, force: true });
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
function ensureTodosTaskList(project, slug, name, description) {
|
|
1549
|
+
runLocalCommand("todos", ["--project", project, "task-lists", "--add", name, "--slug", slug, "-d", description]);
|
|
1550
|
+
const list = runLocalCommand("todos", ["--project", project, "--json", "task-lists"]);
|
|
1551
|
+
if (!list.ok)
|
|
1552
|
+
throw new Error(list.stderr || list.error || "failed to list todos task lists");
|
|
1553
|
+
const values = JSON.parse(list.stdout || "[]");
|
|
1554
|
+
const found = values.find((entry) => entry.slug === slug);
|
|
1555
|
+
if (!found)
|
|
1556
|
+
throw new Error(`todos task list not found after ensure: ${slug}`);
|
|
1557
|
+
return found.id;
|
|
1558
|
+
}
|
|
1559
|
+
function todosMutationSummary(result) {
|
|
1560
|
+
return {
|
|
1561
|
+
ok: result.ok,
|
|
1562
|
+
status: result.status,
|
|
1563
|
+
error: result.ok ? undefined : redact(result.stderr || result.error || "todos mutation failed", 320)
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1205
1567
|
// src/lib/store.ts
|
|
1206
1568
|
var DEFAULT_RECOVERY_BATCH_LIMIT = 100;
|
|
1207
1569
|
var DEFAULT_RECOVERY_SCAN_MULTIPLIER = 5;
|
|
1208
1570
|
var LIVE_EXPIRED_RUN_GRACE_MS = 60000;
|
|
1209
|
-
var SCHEMA_USER_VERSION =
|
|
1571
|
+
var SCHEMA_USER_VERSION = 8;
|
|
1210
1572
|
var TERMINAL_RUN_STATUSES = ["succeeded", "failed", "timed_out", "abandoned", "skipped"];
|
|
1211
1573
|
var PRUNE_BATCH_SIZE = 400;
|
|
1212
1574
|
var GENERATED_ROUTE_TEMPLATE_IDS = new Set(["todos-task-worker-verifier", "task-lifecycle", "event-worker-verifier"]);
|
|
1213
1575
|
var GENERATED_ROUTE_KEYS = new Set(["todos-task", "generic-event"]);
|
|
1576
|
+
var TASK_LIFECYCLE_TEMPLATE_ID = "task-lifecycle";
|
|
1214
1577
|
function rowToLoop(row) {
|
|
1215
1578
|
return {
|
|
1216
1579
|
id: row.id,
|
|
@@ -1261,6 +1624,28 @@ function rowToRun(row) {
|
|
|
1261
1624
|
updatedAt: row.updated_at
|
|
1262
1625
|
};
|
|
1263
1626
|
}
|
|
1627
|
+
function rowToRunReceipt(row) {
|
|
1628
|
+
return {
|
|
1629
|
+
loop_id: row.loop_id,
|
|
1630
|
+
run_id: row.run_id,
|
|
1631
|
+
machine: JSON.parse(row.machine_json),
|
|
1632
|
+
repo: row.repo,
|
|
1633
|
+
task_ids: JSON.parse(row.task_ids_json),
|
|
1634
|
+
knowledge_ids: JSON.parse(row.knowledge_ids_json),
|
|
1635
|
+
digest_id: row.digest_id,
|
|
1636
|
+
started_at: row.started_at,
|
|
1637
|
+
finished_at: row.finished_at,
|
|
1638
|
+
status: row.status,
|
|
1639
|
+
exit_code: row.exit_code,
|
|
1640
|
+
summary: JSON.parse(row.summary_json),
|
|
1641
|
+
evidence_paths: JSON.parse(row.evidence_paths_json),
|
|
1642
|
+
created_at: row.created_at,
|
|
1643
|
+
updated_at: row.updated_at
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
function latestRunTime(row) {
|
|
1647
|
+
return row.finished_at ?? row.started_at ?? row.created_at;
|
|
1648
|
+
}
|
|
1264
1649
|
function rowToWorkflow(row) {
|
|
1265
1650
|
return {
|
|
1266
1651
|
id: row.id,
|
|
@@ -1321,6 +1706,7 @@ function rowToWorkflowWorkItem(row) {
|
|
|
1321
1706
|
subjectRef: row.subject_ref,
|
|
1322
1707
|
projectKey: row.project_key ?? undefined,
|
|
1323
1708
|
projectGroup: row.project_group ?? undefined,
|
|
1709
|
+
machineId: row.machine_id ?? undefined,
|
|
1324
1710
|
routeScope: row.route_scope ?? undefined,
|
|
1325
1711
|
priority: row.priority,
|
|
1326
1712
|
status: row.status,
|
|
@@ -1478,6 +1864,7 @@ function scrubbedOrNull(value) {
|
|
|
1478
1864
|
return value == null ? null : scrubSecrets(value);
|
|
1479
1865
|
}
|
|
1480
1866
|
var MAX_PERSISTED_RUN_OUTPUT_CHARS = 64 * 1024;
|
|
1867
|
+
var MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS = 64 * 1024;
|
|
1481
1868
|
function clampPersistedRunOutput(value) {
|
|
1482
1869
|
if (value == null || value.length <= MAX_PERSISTED_RUN_OUTPUT_CHARS)
|
|
1483
1870
|
return value;
|
|
@@ -1492,6 +1879,42 @@ ${tail}`;
|
|
|
1492
1879
|
function persistedRunOutput(value) {
|
|
1493
1880
|
return clampPersistedRunOutput(scrubbedOrNull(value));
|
|
1494
1881
|
}
|
|
1882
|
+
function clampTextToChars(value, maxChars, reason) {
|
|
1883
|
+
if (value.length <= maxChars)
|
|
1884
|
+
return value;
|
|
1885
|
+
const marker = `
|
|
1886
|
+
...[truncated by ${reason}]...
|
|
1887
|
+
`;
|
|
1888
|
+
const budget = Math.max(0, maxChars - marker.length);
|
|
1889
|
+
const headLength = Math.ceil(budget / 2);
|
|
1890
|
+
const tailLength = Math.floor(budget / 2);
|
|
1891
|
+
return `${value.slice(0, headLength)}${marker}${value.slice(value.length - tailLength)}`;
|
|
1892
|
+
}
|
|
1893
|
+
function boundedWorkflowEventPayloadJson(scrubbedJson) {
|
|
1894
|
+
if (scrubbedJson.length <= MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS)
|
|
1895
|
+
return scrubbedJson;
|
|
1896
|
+
const base = {
|
|
1897
|
+
truncated: true,
|
|
1898
|
+
originalChars: scrubbedJson.length,
|
|
1899
|
+
maxChars: MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS,
|
|
1900
|
+
preview: ""
|
|
1901
|
+
};
|
|
1902
|
+
const baseChars = JSON.stringify(base).length;
|
|
1903
|
+
let previewBudget = Math.max(0, MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS - baseChars - 64);
|
|
1904
|
+
while (true) {
|
|
1905
|
+
const preview = clampTextToChars(scrubbedJson, previewBudget, "loops workflow-event payload retention");
|
|
1906
|
+
const bounded = JSON.stringify({ ...base, preview });
|
|
1907
|
+
if (bounded.length <= MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS || previewBudget === 0)
|
|
1908
|
+
return bounded;
|
|
1909
|
+
previewBudget = Math.max(0, previewBudget - (bounded.length - MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS) - 64);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
function persistedWorkflowEventPayload(payload) {
|
|
1913
|
+
if (payload == null)
|
|
1914
|
+
return null;
|
|
1915
|
+
const scrubbed = scrubSecretsDeep(payload);
|
|
1916
|
+
return boundedWorkflowEventPayloadJson(scrubSecrets(JSON.stringify(scrubbed)));
|
|
1917
|
+
}
|
|
1495
1918
|
function chmodIfExists(path, mode) {
|
|
1496
1919
|
try {
|
|
1497
1920
|
if (existsSync(path))
|
|
@@ -1515,7 +1938,7 @@ class Store {
|
|
|
1515
1938
|
const file = path ?? dbPath();
|
|
1516
1939
|
if (file !== ":memory:")
|
|
1517
1940
|
ensurePrivateStorePath(file);
|
|
1518
|
-
this.rootDir = file === ":memory:" ?
|
|
1941
|
+
this.rootDir = file === ":memory:" ? mkdtempSync2(join4(tmpdir2(), "open-loops-store-")) : dirname2(file);
|
|
1519
1942
|
if (file === ":memory:")
|
|
1520
1943
|
this.memoryRootDir = this.rootDir;
|
|
1521
1944
|
this.db = new Database(file);
|
|
@@ -1609,6 +2032,17 @@ class Store {
|
|
|
1609
2032
|
this.addColumnIfMissing("workflow_work_items", "route_scope", "TEXT");
|
|
1610
2033
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_workflow_work_items_scope ON workflow_work_items(route_scope, status)");
|
|
1611
2034
|
}
|
|
2035
|
+
},
|
|
2036
|
+
{
|
|
2037
|
+
id: "0009_run_receipts",
|
|
2038
|
+
apply: () => this.createRunReceiptsSchema()
|
|
2039
|
+
},
|
|
2040
|
+
{
|
|
2041
|
+
id: "0010_work_item_machine_id",
|
|
2042
|
+
apply: () => {
|
|
2043
|
+
this.addColumnIfMissing("workflow_work_items", "machine_id", "TEXT");
|
|
2044
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_workflow_work_items_machine ON workflow_work_items(machine_id, status)");
|
|
2045
|
+
}
|
|
1612
2046
|
}
|
|
1613
2047
|
];
|
|
1614
2048
|
}
|
|
@@ -1670,6 +2104,28 @@ class Store {
|
|
|
1670
2104
|
CREATE INDEX IF NOT EXISTS idx_runs_status_lease ON loop_runs(status, lease_expires_at);
|
|
1671
2105
|
CREATE INDEX IF NOT EXISTS idx_runs_scheduled ON loop_runs(scheduled_for);
|
|
1672
2106
|
|
|
2107
|
+
CREATE TABLE IF NOT EXISTS run_receipts (
|
|
2108
|
+
run_id TEXT PRIMARY KEY,
|
|
2109
|
+
loop_id TEXT NOT NULL,
|
|
2110
|
+
machine_json TEXT NOT NULL,
|
|
2111
|
+
repo TEXT NOT NULL,
|
|
2112
|
+
task_ids_json TEXT NOT NULL,
|
|
2113
|
+
knowledge_ids_json TEXT NOT NULL,
|
|
2114
|
+
digest_id TEXT NOT NULL,
|
|
2115
|
+
started_at TEXT,
|
|
2116
|
+
finished_at TEXT,
|
|
2117
|
+
status TEXT NOT NULL,
|
|
2118
|
+
exit_code INTEGER,
|
|
2119
|
+
summary_json TEXT NOT NULL,
|
|
2120
|
+
evidence_paths_json TEXT NOT NULL,
|
|
2121
|
+
created_at TEXT NOT NULL,
|
|
2122
|
+
updated_at TEXT NOT NULL
|
|
2123
|
+
);
|
|
2124
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_loop ON run_receipts(loop_id, created_at DESC);
|
|
2125
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_repo ON run_receipts(repo, created_at DESC);
|
|
2126
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_digest ON run_receipts(digest_id);
|
|
2127
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_status ON run_receipts(status, created_at DESC);
|
|
2128
|
+
|
|
1673
2129
|
CREATE TABLE IF NOT EXISTS daemon_lease (
|
|
1674
2130
|
id TEXT PRIMARY KEY,
|
|
1675
2131
|
pid INTEGER NOT NULL,
|
|
@@ -1756,6 +2212,7 @@ class Store {
|
|
|
1756
2212
|
subject_ref TEXT NOT NULL,
|
|
1757
2213
|
project_key TEXT,
|
|
1758
2214
|
project_group TEXT,
|
|
2215
|
+
machine_id TEXT,
|
|
1759
2216
|
route_scope TEXT,
|
|
1760
2217
|
priority INTEGER NOT NULL,
|
|
1761
2218
|
status TEXT NOT NULL,
|
|
@@ -1774,10 +2231,10 @@ class Store {
|
|
|
1774
2231
|
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_project ON workflow_work_items(project_key, status);
|
|
1775
2232
|
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_group ON workflow_work_items(project_group, status);
|
|
1776
2233
|
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_invocation ON workflow_work_items(invocation_id);
|
|
1777
|
-
--
|
|
1778
|
-
--
|
|
2234
|
+
-- New-column indexes (route_scope, machine_id, etc.) are created ONLY by
|
|
2235
|
+
-- their additive migrations, never here: this baseline DDL
|
|
1779
2236
|
-- re-runs on EVERY open (0001 is not skip-guarded), and on a pre-0008
|
|
1780
|
-
-- database the CREATE TABLE above is a no-op, so an index on
|
|
2237
|
+
-- database the CREATE TABLE above is a no-op, so an index on a new column
|
|
1781
2238
|
-- here would execute before the column exists and crash the open
|
|
1782
2239
|
-- ("no such column: route_scope"). New columns may be folded into the
|
|
1783
2240
|
-- CREATE TABLE (fresh-db only); their indexes must live in the migration.
|
|
@@ -1888,6 +2345,31 @@ class Store {
|
|
|
1888
2345
|
CREATE INDEX IF NOT EXISTS idx_goal_runs_workflow_run ON goal_runs(workflow_run_id);
|
|
1889
2346
|
`);
|
|
1890
2347
|
}
|
|
2348
|
+
createRunReceiptsSchema() {
|
|
2349
|
+
this.db.exec(`
|
|
2350
|
+
CREATE TABLE IF NOT EXISTS run_receipts (
|
|
2351
|
+
run_id TEXT PRIMARY KEY,
|
|
2352
|
+
loop_id TEXT NOT NULL,
|
|
2353
|
+
machine_json TEXT NOT NULL,
|
|
2354
|
+
repo TEXT NOT NULL,
|
|
2355
|
+
task_ids_json TEXT NOT NULL,
|
|
2356
|
+
knowledge_ids_json TEXT NOT NULL,
|
|
2357
|
+
digest_id TEXT NOT NULL,
|
|
2358
|
+
started_at TEXT,
|
|
2359
|
+
finished_at TEXT,
|
|
2360
|
+
status TEXT NOT NULL,
|
|
2361
|
+
exit_code INTEGER,
|
|
2362
|
+
summary_json TEXT NOT NULL,
|
|
2363
|
+
evidence_paths_json TEXT NOT NULL,
|
|
2364
|
+
created_at TEXT NOT NULL,
|
|
2365
|
+
updated_at TEXT NOT NULL
|
|
2366
|
+
);
|
|
2367
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_loop ON run_receipts(loop_id, created_at DESC);
|
|
2368
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_repo ON run_receipts(repo, created_at DESC);
|
|
2369
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_digest ON run_receipts(digest_id);
|
|
2370
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_status ON run_receipts(status, created_at DESC);
|
|
2371
|
+
`);
|
|
2372
|
+
}
|
|
1891
2373
|
addColumnIfMissing(table, column, definition) {
|
|
1892
2374
|
const columns = this.db.query(`PRAGMA table_info(${table})`).all();
|
|
1893
2375
|
if (columns.some((c) => c.name === column))
|
|
@@ -1999,21 +2481,51 @@ class Store {
|
|
|
1999
2481
|
}
|
|
2000
2482
|
listLoops(opts = {}) {
|
|
2001
2483
|
const limit = opts.limit ?? 200;
|
|
2484
|
+
const offset = Math.max(0, Math.floor(opts.offset ?? 0));
|
|
2485
|
+
if (opts.name != null) {
|
|
2486
|
+
const rows2 = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT ? OFFSET ?").all(opts.name, limit, offset);
|
|
2487
|
+
return this.withLatestRunSummaries(rows2.map(rowToLoop));
|
|
2488
|
+
}
|
|
2002
2489
|
let rows;
|
|
2003
2490
|
if (opts.status && opts.archived) {
|
|
2004
|
-
rows = this.db.query("SELECT * FROM loops WHERE status = ? AND archived_at IS NOT NULL ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit);
|
|
2491
|
+
rows = this.db.query("SELECT * FROM loops WHERE status = ? AND archived_at IS NOT NULL ORDER BY next_run_at ASC LIMIT ? OFFSET ?").all(opts.status, limit, offset);
|
|
2005
2492
|
} else if (opts.status && opts.includeArchived) {
|
|
2006
|
-
rows = this.db.query("SELECT * FROM loops WHERE status = ? ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit);
|
|
2493
|
+
rows = this.db.query("SELECT * FROM loops WHERE status = ? ORDER BY next_run_at ASC LIMIT ? OFFSET ?").all(opts.status, limit, offset);
|
|
2007
2494
|
} else if (opts.status) {
|
|
2008
|
-
rows = this.db.query("SELECT * FROM loops WHERE status = ? AND archived_at IS NULL ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit);
|
|
2495
|
+
rows = this.db.query("SELECT * FROM loops WHERE status = ? AND archived_at IS NULL ORDER BY next_run_at ASC LIMIT ? OFFSET ?").all(opts.status, limit, offset);
|
|
2009
2496
|
} else if (opts.archived) {
|
|
2010
|
-
rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NOT NULL ORDER BY archived_at DESC LIMIT ?").all(limit);
|
|
2497
|
+
rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NOT NULL ORDER BY archived_at DESC LIMIT ? OFFSET ?").all(limit, offset);
|
|
2011
2498
|
} else if (opts.includeArchived) {
|
|
2012
|
-
rows = this.db.query("SELECT * FROM loops ORDER BY status ASC, next_run_at ASC LIMIT ?").all(limit);
|
|
2499
|
+
rows = this.db.query("SELECT * FROM loops ORDER BY status ASC, next_run_at ASC LIMIT ? OFFSET ?").all(limit, offset);
|
|
2013
2500
|
} else {
|
|
2014
|
-
rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NULL ORDER BY status ASC, next_run_at ASC LIMIT ?").all(limit);
|
|
2501
|
+
rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NULL ORDER BY status ASC, next_run_at ASC LIMIT ? OFFSET ?").all(limit, offset);
|
|
2015
2502
|
}
|
|
2016
|
-
return rows.map(rowToLoop);
|
|
2503
|
+
return this.withLatestRunSummaries(rows.map(rowToLoop));
|
|
2504
|
+
}
|
|
2505
|
+
withLatestRunSummaries(loops) {
|
|
2506
|
+
if (loops.length === 0)
|
|
2507
|
+
return loops;
|
|
2508
|
+
const placeholders = loops.map(() => "?").join(",");
|
|
2509
|
+
const rows = this.db.query(`SELECT loop_id, id, status, started_at, finished_at, created_at
|
|
2510
|
+
FROM (
|
|
2511
|
+
SELECT loop_id, id, status, started_at, finished_at, created_at,
|
|
2512
|
+
ROW_NUMBER() OVER (PARTITION BY loop_id ORDER BY created_at DESC, id DESC) AS rn
|
|
2513
|
+
FROM loop_runs
|
|
2514
|
+
WHERE loop_id IN (${placeholders})
|
|
2515
|
+
)
|
|
2516
|
+
WHERE rn = 1`).all(...loops.map((loop) => loop.id));
|
|
2517
|
+
const latestByLoopId = new Map(rows.map((row) => [row.loop_id, row]));
|
|
2518
|
+
return loops.map((loop) => {
|
|
2519
|
+
const latest = latestByLoopId.get(loop.id);
|
|
2520
|
+
if (!latest)
|
|
2521
|
+
return loop;
|
|
2522
|
+
return {
|
|
2523
|
+
...loop,
|
|
2524
|
+
latestRunId: latest.id,
|
|
2525
|
+
latestRunStatus: latest.status,
|
|
2526
|
+
lastRunAt: latestRunTime(latest)
|
|
2527
|
+
};
|
|
2528
|
+
});
|
|
2017
2529
|
}
|
|
2018
2530
|
dueLoops(now, limit = 500) {
|
|
2019
2531
|
const rows = this.db.query(`SELECT * FROM loops
|
|
@@ -2132,6 +2644,45 @@ class Store {
|
|
|
2132
2644
|
throw error;
|
|
2133
2645
|
}
|
|
2134
2646
|
}
|
|
2647
|
+
updateAgentLoopTimeout(idOrName, timeoutMs, opts = {}) {
|
|
2648
|
+
const updated = (opts.now ?? new Date).toISOString();
|
|
2649
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
2650
|
+
try {
|
|
2651
|
+
const current = this.requireUniqueLoop(idOrName);
|
|
2652
|
+
if (current.archivedAt)
|
|
2653
|
+
throw new LoopArchivedError(current.name || current.id);
|
|
2654
|
+
if (current.target.type !== "agent")
|
|
2655
|
+
throw new Error(`loop is not an agent loop: ${idOrName}`);
|
|
2656
|
+
if (this.hasRunningRun(current.id))
|
|
2657
|
+
throw new Error(`refusing to update running loop: ${current.id}`);
|
|
2658
|
+
const target = { ...current.target, timeoutMs };
|
|
2659
|
+
if (timeoutMs === null && target.idleTimeoutMs !== undefined)
|
|
2660
|
+
delete target.idleTimeoutMs;
|
|
2661
|
+
const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
|
|
2662
|
+
WHERE id=$id
|
|
2663
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
2664
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
2665
|
+
))`).run({
|
|
2666
|
+
$id: current.id,
|
|
2667
|
+
$target: JSON.stringify(target),
|
|
2668
|
+
$updated: updated,
|
|
2669
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
2670
|
+
$now: updated
|
|
2671
|
+
});
|
|
2672
|
+
if (res.changes !== 1)
|
|
2673
|
+
throw new Error("daemon lease lost");
|
|
2674
|
+
this.db.exec("COMMIT");
|
|
2675
|
+
const after = this.getLoop(current.id);
|
|
2676
|
+
if (!after)
|
|
2677
|
+
throw new Error(`loop not found after timeout update: ${current.id}`);
|
|
2678
|
+
return after;
|
|
2679
|
+
} catch (error) {
|
|
2680
|
+
try {
|
|
2681
|
+
this.db.exec("ROLLBACK");
|
|
2682
|
+
} catch {}
|
|
2683
|
+
throw error;
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2135
2686
|
createAndRetargetWorkflowLoop(idOrName, workflowInput, opts = {}) {
|
|
2136
2687
|
const normalized = normalizeCreateWorkflowInput(workflowInput);
|
|
2137
2688
|
const updated = (opts.now ?? new Date).toISOString();
|
|
@@ -2462,6 +3013,59 @@ class Store {
|
|
|
2462
3013
|
updated
|
|
2463
3014
|
});
|
|
2464
3015
|
}
|
|
3016
|
+
taskLifecycleTodosPointerContext(workflowRunId) {
|
|
3017
|
+
const run = this.getWorkflowRun(workflowRunId);
|
|
3018
|
+
if (!run || run.status !== "succeeded" || !run.invocationId || !run.workItemId || !run.manifestPath)
|
|
3019
|
+
return;
|
|
3020
|
+
const workItem = this.getWorkflowWorkItem(run.workItemId);
|
|
3021
|
+
if (!workItem || workItem.routeKey !== "todos-task")
|
|
3022
|
+
return;
|
|
3023
|
+
const invocation = this.getWorkflowInvocation(run.invocationId);
|
|
3024
|
+
if (!invocation || invocation.templateId !== TASK_LIFECYCLE_TEMPLATE_ID)
|
|
3025
|
+
return;
|
|
3026
|
+
const projectPath = workItem.projectKey ?? invocation.scope?.projectPath;
|
|
3027
|
+
const taskId = invocation.subjectRef.id ?? workItem.subjectRef;
|
|
3028
|
+
if (!projectPath || !taskId)
|
|
3029
|
+
return;
|
|
3030
|
+
return {
|
|
3031
|
+
projectPath,
|
|
3032
|
+
taskId,
|
|
3033
|
+
invocationId: invocation.id,
|
|
3034
|
+
workflowRunId: run.id,
|
|
3035
|
+
manifestPath: run.manifestPath
|
|
3036
|
+
};
|
|
3037
|
+
}
|
|
3038
|
+
syncSuccessfulTaskLifecycleTodosPointers(workflowRunId) {
|
|
3039
|
+
const context = this.taskLifecycleTodosPointerContext(workflowRunId);
|
|
3040
|
+
if (!context)
|
|
3041
|
+
return;
|
|
3042
|
+
const result = runLocalCommand("todos", [
|
|
3043
|
+
"--project",
|
|
3044
|
+
context.projectPath,
|
|
3045
|
+
"task",
|
|
3046
|
+
"workflow-pointers",
|
|
3047
|
+
context.taskId,
|
|
3048
|
+
"--clear",
|
|
3049
|
+
"--invocation",
|
|
3050
|
+
context.invocationId,
|
|
3051
|
+
"--run",
|
|
3052
|
+
context.workflowRunId,
|
|
3053
|
+
"--manifest",
|
|
3054
|
+
context.manifestPath,
|
|
3055
|
+
"--state",
|
|
3056
|
+
"succeeded",
|
|
3057
|
+
"--actor",
|
|
3058
|
+
"openloops:task-lifecycle"
|
|
3059
|
+
]);
|
|
3060
|
+
this.appendWorkflowEvent(workflowRunId, result.ok ? "todos_workflow_pointers_synced" : "todos_workflow_pointers_sync_failed", undefined, {
|
|
3061
|
+
projectPath: context.projectPath,
|
|
3062
|
+
taskId: context.taskId,
|
|
3063
|
+
invocationId: context.invocationId,
|
|
3064
|
+
workflowRunId: context.workflowRunId,
|
|
3065
|
+
manifestPath: context.manifestPath,
|
|
3066
|
+
mutation: todosMutationSummary(result)
|
|
3067
|
+
});
|
|
3068
|
+
}
|
|
2465
3069
|
createWorkflowInvocation(input) {
|
|
2466
3070
|
const now = nowIso();
|
|
2467
3071
|
const sourceDedupeKey = input.sourceRef.dedupeKey ?? undefined;
|
|
@@ -2572,10 +3176,10 @@ class Store {
|
|
|
2572
3176
|
const id = genId();
|
|
2573
3177
|
const status = input.status ?? "queued";
|
|
2574
3178
|
this.db.query(`INSERT INTO workflow_work_items (id, route_key, idempotency_key, invocation_id, source_type, source_ref,
|
|
2575
|
-
subject_ref, project_key, project_group, route_scope, priority, status, attempts, next_attempt_at, lease_expires_at,
|
|
3179
|
+
subject_ref, project_key, project_group, machine_id, route_scope, priority, status, attempts, next_attempt_at, lease_expires_at,
|
|
2576
3180
|
workflow_id, loop_id, workflow_run_id, last_reason, created_at, updated_at)
|
|
2577
3181
|
VALUES ($id, $routeKey, $idempotencyKey, $invocationId, $sourceType, $sourceRef, $subjectRef,
|
|
2578
|
-
$projectKey, $projectGroup, $routeScope, $priority, $status, 0, $nextAttemptAt, NULL, NULL, NULL, NULL,
|
|
3182
|
+
$projectKey, $projectGroup, $machineId, $routeScope, $priority, $status, 0, $nextAttemptAt, NULL, NULL, NULL, NULL,
|
|
2579
3183
|
$lastReason, $created, $updated)
|
|
2580
3184
|
ON CONFLICT(route_key, idempotency_key) DO UPDATE SET
|
|
2581
3185
|
invocation_id=excluded.invocation_id,
|
|
@@ -2584,6 +3188,10 @@ class Store {
|
|
|
2584
3188
|
subject_ref=excluded.subject_ref,
|
|
2585
3189
|
project_key=excluded.project_key,
|
|
2586
3190
|
project_group=excluded.project_group,
|
|
3191
|
+
machine_id=CASE
|
|
3192
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.machine_id
|
|
3193
|
+
ELSE excluded.machine_id
|
|
3194
|
+
END,
|
|
2587
3195
|
route_scope=excluded.route_scope,
|
|
2588
3196
|
priority=excluded.priority,
|
|
2589
3197
|
status=CASE
|
|
@@ -2626,6 +3234,7 @@ class Store {
|
|
|
2626
3234
|
$subjectRef: input.subjectRef,
|
|
2627
3235
|
$projectKey: input.projectKey ?? null,
|
|
2628
3236
|
$projectGroup: input.projectGroup ?? null,
|
|
3237
|
+
$machineId: input.machineId ?? null,
|
|
2629
3238
|
$routeScope: input.routeScope ?? null,
|
|
2630
3239
|
$priority: input.priority ?? 0,
|
|
2631
3240
|
$status: status,
|
|
@@ -3135,7 +3744,7 @@ class Store {
|
|
|
3135
3744
|
VALUES ($id, $workflowRunId, 1, 'created', NULL, $payload, $created)`).run({
|
|
3136
3745
|
$id: genId(),
|
|
3137
3746
|
$workflowRunId: runId,
|
|
3138
|
-
$payload:
|
|
3747
|
+
$payload: persistedWorkflowEventPayload({
|
|
3139
3748
|
workflowId: input.workflow.id,
|
|
3140
3749
|
workflowName: input.workflow.name,
|
|
3141
3750
|
stepCount: input.workflow.steps.length,
|
|
@@ -3424,6 +4033,8 @@ class Store {
|
|
|
3424
4033
|
const run = this.getWorkflowRun(workflowRunId);
|
|
3425
4034
|
if (!run)
|
|
3426
4035
|
throw new Error(`workflow run not found after finalize: ${workflowRunId}`);
|
|
4036
|
+
if (changed && status === "succeeded")
|
|
4037
|
+
this.syncSuccessfulTaskLifecycleTodosPointers(workflowRunId);
|
|
3427
4038
|
return run;
|
|
3428
4039
|
}
|
|
3429
4040
|
cancelWorkflowRun(workflowRunId, reason = "cancelled by user") {
|
|
@@ -3464,7 +4075,7 @@ class Store {
|
|
|
3464
4075
|
$sequence: sequence,
|
|
3465
4076
|
$eventType: eventType,
|
|
3466
4077
|
$stepId: stepId ?? null,
|
|
3467
|
-
$payload:
|
|
4078
|
+
$payload: persistedWorkflowEventPayload(payload),
|
|
3468
4079
|
$created: now
|
|
3469
4080
|
});
|
|
3470
4081
|
const event = this.db.query("SELECT * FROM workflow_events WHERE id = ?").get(id);
|
|
@@ -3485,6 +4096,17 @@ class Store {
|
|
|
3485
4096
|
const row = this.db.query("SELECT COUNT(*) AS count FROM loop_runs WHERE loop_id = ? AND scheduled_for = ? AND status = 'running'").get(loopId, scheduledFor);
|
|
3486
4097
|
return (row?.count ?? 0) > 0;
|
|
3487
4098
|
}
|
|
4099
|
+
hasBlockingRunningRunForOtherSlot(loopId, scheduledFor, nowIso2) {
|
|
4100
|
+
const rows = this.db.query(`SELECT * FROM loop_runs
|
|
4101
|
+
WHERE loop_id = ? AND scheduled_for <> ? AND status = 'running'`).all(loopId, scheduledFor);
|
|
4102
|
+
return rows.some((row) => {
|
|
4103
|
+
if (!row.lease_expires_at || row.lease_expires_at > nowIso2)
|
|
4104
|
+
return true;
|
|
4105
|
+
if (isRecordedProcessAlive(row.pid, row.process_started_at))
|
|
4106
|
+
return true;
|
|
4107
|
+
return this.hasLiveWorkflowStepProcesses(row.id);
|
|
4108
|
+
});
|
|
4109
|
+
}
|
|
3488
4110
|
markRunPid(id, pid, claimedBy, opts = {}) {
|
|
3489
4111
|
const now = (opts.now ?? new Date).toISOString();
|
|
3490
4112
|
const startedMs = processStartTimeMs(pid);
|
|
@@ -3617,6 +4239,10 @@ class Store {
|
|
|
3617
4239
|
loop = currentLoop;
|
|
3618
4240
|
const leaseExpiresAt = new Date(now.getTime() + loop.leaseMs).toISOString();
|
|
3619
4241
|
const existing = this.getRunBySlot(loop.id, scheduledFor);
|
|
4242
|
+
if (loop.overlap === "skip" && this.hasBlockingRunningRunForOtherSlot(loop.id, scheduledFor, startedAt)) {
|
|
4243
|
+
this.db.exec("COMMIT");
|
|
4244
|
+
return;
|
|
4245
|
+
}
|
|
3620
4246
|
if (existing) {
|
|
3621
4247
|
if (existing.status === "running") {
|
|
3622
4248
|
if (existing.leaseExpiresAt && existing.leaseExpiresAt <= startedAt && isRecordedProcessAlive(existing.pid, existing.processStartedAt)) {
|
|
@@ -3772,18 +4398,93 @@ class Store {
|
|
|
3772
4398
|
}
|
|
3773
4399
|
listRuns(opts = {}) {
|
|
3774
4400
|
const limit = opts.limit ?? 100;
|
|
4401
|
+
const offset = Math.max(0, Math.floor(opts.offset ?? 0));
|
|
3775
4402
|
let rows;
|
|
3776
4403
|
if (opts.loopId && opts.status) {
|
|
3777
|
-
rows = this.db.query("SELECT * FROM loop_runs WHERE loop_id = ? AND status = ? ORDER BY created_at DESC LIMIT ?").all(opts.loopId, opts.status, limit);
|
|
4404
|
+
rows = this.db.query("SELECT * FROM loop_runs WHERE loop_id = ? AND status = ? ORDER BY created_at DESC LIMIT ? OFFSET ?").all(opts.loopId, opts.status, limit, offset);
|
|
3778
4405
|
} else if (opts.loopId) {
|
|
3779
|
-
rows = this.db.query("SELECT * FROM loop_runs WHERE loop_id = ? ORDER BY created_at DESC LIMIT ?").all(opts.loopId, limit);
|
|
4406
|
+
rows = this.db.query("SELECT * FROM loop_runs WHERE loop_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?").all(opts.loopId, limit, offset);
|
|
3780
4407
|
} else if (opts.status) {
|
|
3781
|
-
rows = this.db.query("SELECT * FROM loop_runs WHERE status = ? ORDER BY created_at DESC LIMIT ?").all(opts.status, limit);
|
|
4408
|
+
rows = this.db.query("SELECT * FROM loop_runs WHERE status = ? ORDER BY created_at DESC LIMIT ? OFFSET ?").all(opts.status, limit, offset);
|
|
3782
4409
|
} else {
|
|
3783
|
-
rows = this.db.query("SELECT * FROM loop_runs ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
4410
|
+
rows = this.db.query("SELECT * FROM loop_runs ORDER BY created_at DESC LIMIT ? OFFSET ?").all(limit, offset);
|
|
3784
4411
|
}
|
|
3785
4412
|
return rows.map(rowToRun);
|
|
3786
4413
|
}
|
|
4414
|
+
writeRunReceipt(input, opts = {}) {
|
|
4415
|
+
const inputRunId = typeof input.run_id === "string" && input.run_id.trim() ? input.run_id : undefined;
|
|
4416
|
+
const existing = inputRunId ? this.getRunReceipt(inputRunId) : undefined;
|
|
4417
|
+
const run = inputRunId ? this.getRun(inputRunId) : undefined;
|
|
4418
|
+
const loop = input.loop_id ? this.getLoop(input.loop_id) : run ? this.getLoop(run.loopId) : undefined;
|
|
4419
|
+
const receipt = normalizeRunReceipt(input, { now: opts.now, run, loop, existing });
|
|
4420
|
+
this.db.query(`INSERT INTO run_receipts (run_id, loop_id, machine_json, repo, task_ids_json, knowledge_ids_json, digest_id,
|
|
4421
|
+
started_at, finished_at, status, exit_code, summary_json, evidence_paths_json, created_at, updated_at)
|
|
4422
|
+
VALUES ($runId, $loopId, $machineJson, $repo, $taskIdsJson, $knowledgeIdsJson, $digestId,
|
|
4423
|
+
$startedAt, $finishedAt, $status, $exitCode, $summaryJson, $evidencePathsJson, $createdAt, $updatedAt)
|
|
4424
|
+
ON CONFLICT(run_id) DO UPDATE SET
|
|
4425
|
+
loop_id=excluded.loop_id,
|
|
4426
|
+
machine_json=excluded.machine_json,
|
|
4427
|
+
repo=excluded.repo,
|
|
4428
|
+
task_ids_json=excluded.task_ids_json,
|
|
4429
|
+
knowledge_ids_json=excluded.knowledge_ids_json,
|
|
4430
|
+
digest_id=excluded.digest_id,
|
|
4431
|
+
started_at=excluded.started_at,
|
|
4432
|
+
finished_at=excluded.finished_at,
|
|
4433
|
+
status=excluded.status,
|
|
4434
|
+
exit_code=excluded.exit_code,
|
|
4435
|
+
summary_json=excluded.summary_json,
|
|
4436
|
+
evidence_paths_json=excluded.evidence_paths_json,
|
|
4437
|
+
updated_at=excluded.updated_at`).run({
|
|
4438
|
+
$runId: receipt.run_id,
|
|
4439
|
+
$loopId: receipt.loop_id,
|
|
4440
|
+
$machineJson: JSON.stringify(receipt.machine),
|
|
4441
|
+
$repo: receipt.repo,
|
|
4442
|
+
$taskIdsJson: JSON.stringify(receipt.task_ids),
|
|
4443
|
+
$knowledgeIdsJson: JSON.stringify(receipt.knowledge_ids),
|
|
4444
|
+
$digestId: receipt.digest_id,
|
|
4445
|
+
$startedAt: receipt.started_at,
|
|
4446
|
+
$finishedAt: receipt.finished_at,
|
|
4447
|
+
$status: receipt.status,
|
|
4448
|
+
$exitCode: receipt.exit_code,
|
|
4449
|
+
$summaryJson: JSON.stringify(receipt.summary),
|
|
4450
|
+
$evidencePathsJson: JSON.stringify(receipt.evidence_paths),
|
|
4451
|
+
$createdAt: receipt.created_at,
|
|
4452
|
+
$updatedAt: receipt.updated_at
|
|
4453
|
+
});
|
|
4454
|
+
return this.getRunReceipt(receipt.run_id) ?? receipt;
|
|
4455
|
+
}
|
|
4456
|
+
getRunReceipt(runId) {
|
|
4457
|
+
const row = this.db.query("SELECT * FROM run_receipts WHERE run_id = ?").get(runId);
|
|
4458
|
+
return row ? rowToRunReceipt(row) : undefined;
|
|
4459
|
+
}
|
|
4460
|
+
listRunReceipts(opts = {}) {
|
|
4461
|
+
const limit = opts.limit ?? 100;
|
|
4462
|
+
const filters = [];
|
|
4463
|
+
const params = [];
|
|
4464
|
+
if (opts.loopId) {
|
|
4465
|
+
filters.push("loop_id = ?");
|
|
4466
|
+
params.push(opts.loopId);
|
|
4467
|
+
}
|
|
4468
|
+
if (opts.repo) {
|
|
4469
|
+
filters.push("repo = ?");
|
|
4470
|
+
params.push(opts.repo);
|
|
4471
|
+
}
|
|
4472
|
+
if (opts.status) {
|
|
4473
|
+
filters.push("status = ?");
|
|
4474
|
+
params.push(opts.status);
|
|
4475
|
+
}
|
|
4476
|
+
if (opts.taskId) {
|
|
4477
|
+
filters.push("EXISTS (SELECT 1 FROM json_each(run_receipts.task_ids_json) WHERE value = ?)");
|
|
4478
|
+
params.push(opts.taskId);
|
|
4479
|
+
}
|
|
4480
|
+
if (opts.knowledgeId) {
|
|
4481
|
+
filters.push("EXISTS (SELECT 1 FROM json_each(run_receipts.knowledge_ids_json) WHERE value = ?)");
|
|
4482
|
+
params.push(opts.knowledgeId);
|
|
4483
|
+
}
|
|
4484
|
+
const where = filters.length ? `WHERE ${filters.join(" AND ")}` : "";
|
|
4485
|
+
const rows = this.db.query(`SELECT * FROM run_receipts ${where} ORDER BY created_at DESC LIMIT ?`).all(...params, limit);
|
|
4486
|
+
return rows.map(rowToRunReceipt);
|
|
4487
|
+
}
|
|
3787
4488
|
deferLiveExpiredRun(id, now, opts = {}) {
|
|
3788
4489
|
const updated = now.toISOString();
|
|
3789
4490
|
const deferredUntil = new Date(now.getTime() + LIVE_EXPIRED_RUN_GRACE_MS).toISOString();
|
|
@@ -3942,6 +4643,9 @@ class Store {
|
|
|
3942
4643
|
const runs = includeRuns ? this.db.query("SELECT * FROM loop_runs ORDER BY created_at ASC, id ASC").all().map(rowToRun) : [];
|
|
3943
4644
|
return { schemaVersion: SCHEMA_USER_VERSION, workflows, loops, runs, checks: this.migrationChecks() };
|
|
3944
4645
|
}
|
|
4646
|
+
exportMigrationRunPage(opts) {
|
|
4647
|
+
return this.db.query("SELECT * FROM loop_runs ORDER BY created_at ASC, id ASC LIMIT ? OFFSET ?").all(opts.limit, opts.offset).map(rowToRun);
|
|
4648
|
+
}
|
|
3945
4649
|
countTable(table) {
|
|
3946
4650
|
const row = this.db.query(`SELECT COUNT(*) AS count FROM ${table}`).get();
|
|
3947
4651
|
return row?.count ?? 0;
|
|
@@ -4188,9 +4892,9 @@ class Store {
|
|
|
4188
4892
|
const runDir = dirname2(manifestPath);
|
|
4189
4893
|
try {
|
|
4190
4894
|
if (/^[0-9a-f]{12,64}$/.test(basename2(runDir))) {
|
|
4191
|
-
|
|
4895
|
+
rmSync3(runDir, { recursive: true, force: true });
|
|
4192
4896
|
} else {
|
|
4193
|
-
|
|
4897
|
+
rmSync3(manifestPath, { force: true });
|
|
4194
4898
|
}
|
|
4195
4899
|
} catch {}
|
|
4196
4900
|
}
|
|
@@ -4257,222 +4961,20 @@ class Store {
|
|
|
4257
4961
|
this.db.close();
|
|
4258
4962
|
if (this.memoryRootDir) {
|
|
4259
4963
|
try {
|
|
4260
|
-
|
|
4964
|
+
rmSync3(this.memoryRootDir, { recursive: true, force: true });
|
|
4261
4965
|
} catch {}
|
|
4262
4966
|
this.memoryRootDir = undefined;
|
|
4263
4967
|
}
|
|
4264
4968
|
}
|
|
4265
4969
|
}
|
|
4266
4970
|
|
|
4267
|
-
// src/lib/storage/sqlite.ts
|
|
4268
|
-
class SqliteLoopStorage {
|
|
4269
|
-
store;
|
|
4270
|
-
backend = "sqlite";
|
|
4271
|
-
supportsRemoteRunners = false;
|
|
4272
|
-
constructor(store = new Store) {
|
|
4273
|
-
this.store = store;
|
|
4274
|
-
}
|
|
4275
|
-
async close() {
|
|
4276
|
-
this.store.close();
|
|
4277
|
-
}
|
|
4278
|
-
async call(method, ...args) {
|
|
4279
|
-
return this.store[method](...args);
|
|
4280
|
-
}
|
|
4281
|
-
createLoop(...args) {
|
|
4282
|
-
return this.call("createLoop", ...args);
|
|
4283
|
-
}
|
|
4284
|
-
getLoop(...args) {
|
|
4285
|
-
return this.call("getLoop", ...args);
|
|
4286
|
-
}
|
|
4287
|
-
findLoopByName(...args) {
|
|
4288
|
-
return this.call("findLoopByName", ...args);
|
|
4289
|
-
}
|
|
4290
|
-
requireLoop(...args) {
|
|
4291
|
-
return this.call("requireLoop", ...args);
|
|
4292
|
-
}
|
|
4293
|
-
listLoops(...args) {
|
|
4294
|
-
return this.call("listLoops", ...args);
|
|
4295
|
-
}
|
|
4296
|
-
dueLoops(...args) {
|
|
4297
|
-
return this.call("dueLoops", ...args);
|
|
4298
|
-
}
|
|
4299
|
-
updateLoop(...args) {
|
|
4300
|
-
return this.call("updateLoop", ...args);
|
|
4301
|
-
}
|
|
4302
|
-
renameLoop(...args) {
|
|
4303
|
-
return this.call("renameLoop", ...args);
|
|
4304
|
-
}
|
|
4305
|
-
archiveLoop(...args) {
|
|
4306
|
-
return this.call("archiveLoop", ...args);
|
|
4307
|
-
}
|
|
4308
|
-
unarchiveLoop(...args) {
|
|
4309
|
-
return this.call("unarchiveLoop", ...args);
|
|
4310
|
-
}
|
|
4311
|
-
deleteLoop(...args) {
|
|
4312
|
-
return this.call("deleteLoop", ...args);
|
|
4313
|
-
}
|
|
4314
|
-
createWorkflow(...args) {
|
|
4315
|
-
return this.call("createWorkflow", ...args);
|
|
4316
|
-
}
|
|
4317
|
-
getWorkflow(...args) {
|
|
4318
|
-
return this.call("getWorkflow", ...args);
|
|
4319
|
-
}
|
|
4320
|
-
listWorkflows(...args) {
|
|
4321
|
-
return this.call("listWorkflows", ...args);
|
|
4322
|
-
}
|
|
4323
|
-
countWorkflows(...args) {
|
|
4324
|
-
return this.call("countWorkflows", ...args);
|
|
4325
|
-
}
|
|
4326
|
-
archiveWorkflow(...args) {
|
|
4327
|
-
return this.call("archiveWorkflow", ...args);
|
|
4328
|
-
}
|
|
4329
|
-
createWorkflowInvocation(...args) {
|
|
4330
|
-
return this.call("createWorkflowInvocation", ...args);
|
|
4331
|
-
}
|
|
4332
|
-
getWorkflowInvocation(...args) {
|
|
4333
|
-
return this.call("getWorkflowInvocation", ...args);
|
|
4334
|
-
}
|
|
4335
|
-
listWorkflowInvocations(...args) {
|
|
4336
|
-
return this.call("listWorkflowInvocations", ...args);
|
|
4337
|
-
}
|
|
4338
|
-
upsertWorkflowWorkItem(...args) {
|
|
4339
|
-
return this.call("upsertWorkflowWorkItem", ...args);
|
|
4340
|
-
}
|
|
4341
|
-
getWorkflowWorkItem(...args) {
|
|
4342
|
-
return this.call("getWorkflowWorkItem", ...args);
|
|
4343
|
-
}
|
|
4344
|
-
listWorkflowWorkItems(...args) {
|
|
4345
|
-
return this.call("listWorkflowWorkItems", ...args);
|
|
4346
|
-
}
|
|
4347
|
-
countActiveWorkflowWorkItems(...args) {
|
|
4348
|
-
return this.call("countActiveWorkflowWorkItems", ...args);
|
|
4349
|
-
}
|
|
4350
|
-
admitWorkflowWorkItem(...args) {
|
|
4351
|
-
return this.call("admitWorkflowWorkItem", ...args);
|
|
4352
|
-
}
|
|
4353
|
-
createGoal(...args) {
|
|
4354
|
-
return this.call("createGoal", ...args);
|
|
4355
|
-
}
|
|
4356
|
-
getGoal(...args) {
|
|
4357
|
-
return this.call("getGoal", ...args);
|
|
4358
|
-
}
|
|
4359
|
-
listGoals(...args) {
|
|
4360
|
-
return this.call("listGoals", ...args);
|
|
4361
|
-
}
|
|
4362
|
-
createGoalPlanNodes(...args) {
|
|
4363
|
-
return this.call("createGoalPlanNodes", ...args);
|
|
4364
|
-
}
|
|
4365
|
-
listGoalPlanNodes(...args) {
|
|
4366
|
-
return this.call("listGoalPlanNodes", ...args);
|
|
4367
|
-
}
|
|
4368
|
-
updateGoalStatus(...args) {
|
|
4369
|
-
return this.call("updateGoalStatus", ...args);
|
|
4370
|
-
}
|
|
4371
|
-
updateGoalPlanNode(...args) {
|
|
4372
|
-
return this.call("updateGoalPlanNode", ...args);
|
|
4373
|
-
}
|
|
4374
|
-
recordGoalEvent(...args) {
|
|
4375
|
-
return this.call("recordGoalEvent", ...args);
|
|
4376
|
-
}
|
|
4377
|
-
listGoalRuns(...args) {
|
|
4378
|
-
return this.call("listGoalRuns", ...args);
|
|
4379
|
-
}
|
|
4380
|
-
createWorkflowRun(...args) {
|
|
4381
|
-
return this.call("createWorkflowRun", ...args);
|
|
4382
|
-
}
|
|
4383
|
-
getWorkflowRun(...args) {
|
|
4384
|
-
return this.call("getWorkflowRun", ...args);
|
|
4385
|
-
}
|
|
4386
|
-
listWorkflowRuns(...args) {
|
|
4387
|
-
return this.call("listWorkflowRuns", ...args);
|
|
4388
|
-
}
|
|
4389
|
-
listWorkflowStepRuns(...args) {
|
|
4390
|
-
return this.call("listWorkflowStepRuns", ...args);
|
|
4391
|
-
}
|
|
4392
|
-
getWorkflowStepRun(...args) {
|
|
4393
|
-
return this.call("getWorkflowStepRun", ...args);
|
|
4394
|
-
}
|
|
4395
|
-
startWorkflowStepRun(...args) {
|
|
4396
|
-
return this.call("startWorkflowStepRun", ...args);
|
|
4397
|
-
}
|
|
4398
|
-
recoverWorkflowRun(...args) {
|
|
4399
|
-
return this.call("recoverWorkflowRun", ...args);
|
|
4400
|
-
}
|
|
4401
|
-
finalizeWorkflowStepRun(...args) {
|
|
4402
|
-
return this.call("finalizeWorkflowStepRun", ...args);
|
|
4403
|
-
}
|
|
4404
|
-
finalizeWorkflowRun(...args) {
|
|
4405
|
-
return this.call("finalizeWorkflowRun", ...args);
|
|
4406
|
-
}
|
|
4407
|
-
appendWorkflowEvent(...args) {
|
|
4408
|
-
return this.call("appendWorkflowEvent", ...args);
|
|
4409
|
-
}
|
|
4410
|
-
listWorkflowEvents(...args) {
|
|
4411
|
-
return this.call("listWorkflowEvents", ...args);
|
|
4412
|
-
}
|
|
4413
|
-
recordRunProcess(...args) {
|
|
4414
|
-
return this.call("recordRunProcess", ...args);
|
|
4415
|
-
}
|
|
4416
|
-
createSkippedRun(...args) {
|
|
4417
|
-
return this.call("createSkippedRun", ...args);
|
|
4418
|
-
}
|
|
4419
|
-
getRun(...args) {
|
|
4420
|
-
return this.call("getRun", ...args);
|
|
4421
|
-
}
|
|
4422
|
-
getRunBySlot(...args) {
|
|
4423
|
-
return this.call("getRunBySlot", ...args);
|
|
4424
|
-
}
|
|
4425
|
-
claimRun(...args) {
|
|
4426
|
-
return this.call("claimRun", ...args);
|
|
4427
|
-
}
|
|
4428
|
-
finalizeRun(...args) {
|
|
4429
|
-
return this.call("finalizeRun", ...args);
|
|
4430
|
-
}
|
|
4431
|
-
heartbeatRunLease(...args) {
|
|
4432
|
-
return this.call("heartbeatRunLease", ...args);
|
|
4433
|
-
}
|
|
4434
|
-
listRuns(...args) {
|
|
4435
|
-
return this.call("listRuns", ...args);
|
|
4436
|
-
}
|
|
4437
|
-
recoverExpiredRunLeases(...args) {
|
|
4438
|
-
return this.call("recoverExpiredRunLeases", ...args);
|
|
4439
|
-
}
|
|
4440
|
-
recoverExpiredRunLeasesDetailed(...args) {
|
|
4441
|
-
return this.call("recoverExpiredRunLeasesDetailed", ...args);
|
|
4442
|
-
}
|
|
4443
|
-
countLoops(...args) {
|
|
4444
|
-
return this.call("countLoops", ...args);
|
|
4445
|
-
}
|
|
4446
|
-
countRuns(...args) {
|
|
4447
|
-
return this.call("countRuns", ...args);
|
|
4448
|
-
}
|
|
4449
|
-
pruneHistory(...args) {
|
|
4450
|
-
return this.call("pruneHistory", ...args);
|
|
4451
|
-
}
|
|
4452
|
-
acquireDaemonLease(...args) {
|
|
4453
|
-
return this.call("acquireDaemonLease", ...args);
|
|
4454
|
-
}
|
|
4455
|
-
heartbeatDaemonLease(...args) {
|
|
4456
|
-
return this.call("heartbeatDaemonLease", ...args);
|
|
4457
|
-
}
|
|
4458
|
-
releaseDaemonLease(...args) {
|
|
4459
|
-
return this.call("releaseDaemonLease", ...args);
|
|
4460
|
-
}
|
|
4461
|
-
getDaemonLease(...args) {
|
|
4462
|
-
return this.call("getDaemonLease", ...args);
|
|
4463
|
-
}
|
|
4464
|
-
}
|
|
4465
|
-
function createSqliteLoopStorage(path) {
|
|
4466
|
-
return new SqliteLoopStorage(new Store(path));
|
|
4467
|
-
}
|
|
4468
|
-
|
|
4469
4971
|
// src/lib/storage/postgres-schema.ts
|
|
4470
|
-
import { createHash as
|
|
4972
|
+
import { createHash as createHash3 } from "crypto";
|
|
4471
4973
|
var POSTGRES_MIGRATION_LEDGER_TABLE = "open_loops_schema_migrations";
|
|
4472
4974
|
function checksumStorageSql(sql) {
|
|
4473
4975
|
const normalized = sql.trim().replace(/\r\n/g, `
|
|
4474
4976
|
`);
|
|
4475
|
-
return `sha256:${
|
|
4977
|
+
return `sha256:${createHash3("sha256").update(normalized).digest("hex")}`;
|
|
4476
4978
|
}
|
|
4477
4979
|
function migration(id, sql) {
|
|
4478
4980
|
return Object.freeze({ id, sql: sql.trim(), checksum: checksumStorageSql(sql) });
|
|
@@ -4790,6 +5292,35 @@ CREATE INDEX IF NOT EXISTS idx_audit_events_action ON audit_events(action, creat
|
|
|
4790
5292
|
migration("0004_work_item_route_scope", `
|
|
4791
5293
|
ALTER TABLE workflow_work_items ADD COLUMN IF NOT EXISTS route_scope TEXT;
|
|
4792
5294
|
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_scope ON workflow_work_items(route_scope, status);
|
|
5295
|
+
`),
|
|
5296
|
+
migration("0005_run_receipts", `
|
|
5297
|
+
CREATE TABLE IF NOT EXISTS run_receipts (
|
|
5298
|
+
run_id TEXT PRIMARY KEY,
|
|
5299
|
+
loop_id TEXT NOT NULL,
|
|
5300
|
+
machine_json JSONB NOT NULL,
|
|
5301
|
+
repo TEXT NOT NULL,
|
|
5302
|
+
task_ids_json JSONB NOT NULL,
|
|
5303
|
+
knowledge_ids_json JSONB NOT NULL,
|
|
5304
|
+
digest_id TEXT NOT NULL,
|
|
5305
|
+
started_at TIMESTAMPTZ,
|
|
5306
|
+
finished_at TIMESTAMPTZ,
|
|
5307
|
+
status TEXT NOT NULL,
|
|
5308
|
+
exit_code INTEGER,
|
|
5309
|
+
summary_json JSONB NOT NULL,
|
|
5310
|
+
evidence_paths_json JSONB NOT NULL,
|
|
5311
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
5312
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
5313
|
+
);
|
|
5314
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_loop ON run_receipts(loop_id, created_at DESC);
|
|
5315
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_repo ON run_receipts(repo, created_at DESC);
|
|
5316
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_digest ON run_receipts(digest_id);
|
|
5317
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_status ON run_receipts(status, created_at DESC);
|
|
5318
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_task_ids ON run_receipts USING GIN (task_ids_json);
|
|
5319
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_knowledge_ids ON run_receipts USING GIN (knowledge_ids_json);
|
|
5320
|
+
`),
|
|
5321
|
+
migration("0006_work_item_machine_id", `
|
|
5322
|
+
ALTER TABLE workflow_work_items ADD COLUMN IF NOT EXISTS machine_id TEXT;
|
|
5323
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_machine ON workflow_work_items(machine_id, status);
|
|
4793
5324
|
`)
|
|
4794
5325
|
]);
|
|
4795
5326
|
|
|
@@ -4888,13 +5419,1279 @@ function isMissingLedgerError(error) {
|
|
|
4888
5419
|
const message = error.message.toLowerCase();
|
|
4889
5420
|
return message.includes(POSTGRES_MIGRATION_LEDGER_TABLE.toLowerCase()) && (message.includes("does not exist") || message.includes("no such table") || message.includes("undefined_table"));
|
|
4890
5421
|
}
|
|
5422
|
+
|
|
5423
|
+
// src/lib/storage/sqlite.ts
|
|
5424
|
+
class SqliteLoopStorage {
|
|
5425
|
+
store;
|
|
5426
|
+
backend = "sqlite";
|
|
5427
|
+
supportsRemoteRunners = false;
|
|
5428
|
+
constructor(store = new Store) {
|
|
5429
|
+
this.store = store;
|
|
5430
|
+
}
|
|
5431
|
+
async close() {
|
|
5432
|
+
this.store.close();
|
|
5433
|
+
}
|
|
5434
|
+
async call(method, ...args) {
|
|
5435
|
+
return this.store[method](...args);
|
|
5436
|
+
}
|
|
5437
|
+
createLoop(...args) {
|
|
5438
|
+
return this.call("createLoop", ...args);
|
|
5439
|
+
}
|
|
5440
|
+
getLoop(...args) {
|
|
5441
|
+
return this.call("getLoop", ...args);
|
|
5442
|
+
}
|
|
5443
|
+
findLoopByName(...args) {
|
|
5444
|
+
return this.call("findLoopByName", ...args);
|
|
5445
|
+
}
|
|
5446
|
+
requireLoop(...args) {
|
|
5447
|
+
return this.call("requireLoop", ...args);
|
|
5448
|
+
}
|
|
5449
|
+
listLoops(...args) {
|
|
5450
|
+
return this.call("listLoops", ...args);
|
|
5451
|
+
}
|
|
5452
|
+
dueLoops(...args) {
|
|
5453
|
+
return this.call("dueLoops", ...args);
|
|
5454
|
+
}
|
|
5455
|
+
updateLoop(...args) {
|
|
5456
|
+
return this.call("updateLoop", ...args);
|
|
5457
|
+
}
|
|
5458
|
+
renameLoop(...args) {
|
|
5459
|
+
return this.call("renameLoop", ...args);
|
|
5460
|
+
}
|
|
5461
|
+
archiveLoop(...args) {
|
|
5462
|
+
return this.call("archiveLoop", ...args);
|
|
5463
|
+
}
|
|
5464
|
+
unarchiveLoop(...args) {
|
|
5465
|
+
return this.call("unarchiveLoop", ...args);
|
|
5466
|
+
}
|
|
5467
|
+
deleteLoop(...args) {
|
|
5468
|
+
return this.call("deleteLoop", ...args);
|
|
5469
|
+
}
|
|
5470
|
+
upsertMigrationLoop(...args) {
|
|
5471
|
+
return this.call("upsertMigrationLoop", ...args);
|
|
5472
|
+
}
|
|
5473
|
+
upsertMigrationRun(...args) {
|
|
5474
|
+
return this.call("upsertMigrationRun", ...args);
|
|
5475
|
+
}
|
|
5476
|
+
upsertMigrationWorkflow(...args) {
|
|
5477
|
+
return this.call("upsertMigrationWorkflow", ...args);
|
|
5478
|
+
}
|
|
5479
|
+
createWorkflow(...args) {
|
|
5480
|
+
return this.call("createWorkflow", ...args);
|
|
5481
|
+
}
|
|
5482
|
+
getWorkflow(...args) {
|
|
5483
|
+
return this.call("getWorkflow", ...args);
|
|
5484
|
+
}
|
|
5485
|
+
listWorkflows(...args) {
|
|
5486
|
+
return this.call("listWorkflows", ...args);
|
|
5487
|
+
}
|
|
5488
|
+
countWorkflows(...args) {
|
|
5489
|
+
return this.call("countWorkflows", ...args);
|
|
5490
|
+
}
|
|
5491
|
+
archiveWorkflow(...args) {
|
|
5492
|
+
return this.call("archiveWorkflow", ...args);
|
|
5493
|
+
}
|
|
5494
|
+
createWorkflowInvocation(...args) {
|
|
5495
|
+
return this.call("createWorkflowInvocation", ...args);
|
|
5496
|
+
}
|
|
5497
|
+
getWorkflowInvocation(...args) {
|
|
5498
|
+
return this.call("getWorkflowInvocation", ...args);
|
|
5499
|
+
}
|
|
5500
|
+
listWorkflowInvocations(...args) {
|
|
5501
|
+
return this.call("listWorkflowInvocations", ...args);
|
|
5502
|
+
}
|
|
5503
|
+
upsertWorkflowWorkItem(...args) {
|
|
5504
|
+
return this.call("upsertWorkflowWorkItem", ...args);
|
|
5505
|
+
}
|
|
5506
|
+
getWorkflowWorkItem(...args) {
|
|
5507
|
+
return this.call("getWorkflowWorkItem", ...args);
|
|
5508
|
+
}
|
|
5509
|
+
listWorkflowWorkItems(...args) {
|
|
5510
|
+
return this.call("listWorkflowWorkItems", ...args);
|
|
5511
|
+
}
|
|
5512
|
+
countActiveWorkflowWorkItems(...args) {
|
|
5513
|
+
return this.call("countActiveWorkflowWorkItems", ...args);
|
|
5514
|
+
}
|
|
5515
|
+
admitWorkflowWorkItem(...args) {
|
|
5516
|
+
return this.call("admitWorkflowWorkItem", ...args);
|
|
5517
|
+
}
|
|
5518
|
+
createGoal(...args) {
|
|
5519
|
+
return this.call("createGoal", ...args);
|
|
5520
|
+
}
|
|
5521
|
+
getGoal(...args) {
|
|
5522
|
+
return this.call("getGoal", ...args);
|
|
5523
|
+
}
|
|
5524
|
+
listGoals(...args) {
|
|
5525
|
+
return this.call("listGoals", ...args);
|
|
5526
|
+
}
|
|
5527
|
+
createGoalPlanNodes(...args) {
|
|
5528
|
+
return this.call("createGoalPlanNodes", ...args);
|
|
5529
|
+
}
|
|
5530
|
+
listGoalPlanNodes(...args) {
|
|
5531
|
+
return this.call("listGoalPlanNodes", ...args);
|
|
5532
|
+
}
|
|
5533
|
+
updateGoalStatus(...args) {
|
|
5534
|
+
return this.call("updateGoalStatus", ...args);
|
|
5535
|
+
}
|
|
5536
|
+
updateGoalPlanNode(...args) {
|
|
5537
|
+
return this.call("updateGoalPlanNode", ...args);
|
|
5538
|
+
}
|
|
5539
|
+
recordGoalEvent(...args) {
|
|
5540
|
+
return this.call("recordGoalEvent", ...args);
|
|
5541
|
+
}
|
|
5542
|
+
listGoalRuns(...args) {
|
|
5543
|
+
return this.call("listGoalRuns", ...args);
|
|
5544
|
+
}
|
|
5545
|
+
createWorkflowRun(...args) {
|
|
5546
|
+
return this.call("createWorkflowRun", ...args);
|
|
5547
|
+
}
|
|
5548
|
+
getWorkflowRun(...args) {
|
|
5549
|
+
return this.call("getWorkflowRun", ...args);
|
|
5550
|
+
}
|
|
5551
|
+
listWorkflowRuns(...args) {
|
|
5552
|
+
return this.call("listWorkflowRuns", ...args);
|
|
5553
|
+
}
|
|
5554
|
+
listWorkflowStepRuns(...args) {
|
|
5555
|
+
return this.call("listWorkflowStepRuns", ...args);
|
|
5556
|
+
}
|
|
5557
|
+
getWorkflowStepRun(...args) {
|
|
5558
|
+
return this.call("getWorkflowStepRun", ...args);
|
|
5559
|
+
}
|
|
5560
|
+
startWorkflowStepRun(...args) {
|
|
5561
|
+
return this.call("startWorkflowStepRun", ...args);
|
|
5562
|
+
}
|
|
5563
|
+
recoverWorkflowRun(...args) {
|
|
5564
|
+
return this.call("recoverWorkflowRun", ...args);
|
|
5565
|
+
}
|
|
5566
|
+
finalizeWorkflowStepRun(...args) {
|
|
5567
|
+
return this.call("finalizeWorkflowStepRun", ...args);
|
|
5568
|
+
}
|
|
5569
|
+
finalizeWorkflowRun(...args) {
|
|
5570
|
+
return this.call("finalizeWorkflowRun", ...args);
|
|
5571
|
+
}
|
|
5572
|
+
appendWorkflowEvent(...args) {
|
|
5573
|
+
return this.call("appendWorkflowEvent", ...args);
|
|
5574
|
+
}
|
|
5575
|
+
listWorkflowEvents(...args) {
|
|
5576
|
+
return this.call("listWorkflowEvents", ...args);
|
|
5577
|
+
}
|
|
5578
|
+
recordRunProcess(...args) {
|
|
5579
|
+
return this.call("recordRunProcess", ...args);
|
|
5580
|
+
}
|
|
5581
|
+
createSkippedRun(...args) {
|
|
5582
|
+
return this.call("createSkippedRun", ...args);
|
|
5583
|
+
}
|
|
5584
|
+
getRun(...args) {
|
|
5585
|
+
return this.call("getRun", ...args);
|
|
5586
|
+
}
|
|
5587
|
+
getRunBySlot(...args) {
|
|
5588
|
+
return this.call("getRunBySlot", ...args);
|
|
5589
|
+
}
|
|
5590
|
+
claimRun(...args) {
|
|
5591
|
+
return this.call("claimRun", ...args);
|
|
5592
|
+
}
|
|
5593
|
+
finalizeRun(...args) {
|
|
5594
|
+
return this.call("finalizeRun", ...args);
|
|
5595
|
+
}
|
|
5596
|
+
heartbeatRunLease(...args) {
|
|
5597
|
+
return this.call("heartbeatRunLease", ...args);
|
|
5598
|
+
}
|
|
5599
|
+
listRuns(...args) {
|
|
5600
|
+
return this.call("listRuns", ...args);
|
|
5601
|
+
}
|
|
5602
|
+
writeRunReceipt(...args) {
|
|
5603
|
+
return this.call("writeRunReceipt", ...args);
|
|
5604
|
+
}
|
|
5605
|
+
getRunReceipt(...args) {
|
|
5606
|
+
return this.call("getRunReceipt", ...args);
|
|
5607
|
+
}
|
|
5608
|
+
listRunReceipts(...args) {
|
|
5609
|
+
return this.call("listRunReceipts", ...args);
|
|
5610
|
+
}
|
|
5611
|
+
recoverExpiredRunLeases(...args) {
|
|
5612
|
+
return this.call("recoverExpiredRunLeases", ...args);
|
|
5613
|
+
}
|
|
5614
|
+
recoverExpiredRunLeasesDetailed(...args) {
|
|
5615
|
+
return this.call("recoverExpiredRunLeasesDetailed", ...args);
|
|
5616
|
+
}
|
|
5617
|
+
countLoops(...args) {
|
|
5618
|
+
return this.call("countLoops", ...args);
|
|
5619
|
+
}
|
|
5620
|
+
countRuns(...args) {
|
|
5621
|
+
return this.call("countRuns", ...args);
|
|
5622
|
+
}
|
|
5623
|
+
pruneHistory(...args) {
|
|
5624
|
+
return this.call("pruneHistory", ...args);
|
|
5625
|
+
}
|
|
5626
|
+
acquireDaemonLease(...args) {
|
|
5627
|
+
return this.call("acquireDaemonLease", ...args);
|
|
5628
|
+
}
|
|
5629
|
+
heartbeatDaemonLease(...args) {
|
|
5630
|
+
return this.call("heartbeatDaemonLease", ...args);
|
|
5631
|
+
}
|
|
5632
|
+
releaseDaemonLease(...args) {
|
|
5633
|
+
return this.call("releaseDaemonLease", ...args);
|
|
5634
|
+
}
|
|
5635
|
+
getDaemonLease(...args) {
|
|
5636
|
+
return this.call("getDaemonLease", ...args);
|
|
5637
|
+
}
|
|
5638
|
+
}
|
|
5639
|
+
function createSqliteLoopStorage(path) {
|
|
5640
|
+
return new SqliteLoopStorage(new Store(path));
|
|
5641
|
+
}
|
|
5642
|
+
// src/lib/storage/postgres-loop-storage.ts
|
|
5643
|
+
import pgLib from "pg";
|
|
5644
|
+
var { types: pgTypes } = pgLib;
|
|
5645
|
+
pgTypes.setTypeParser(3802, (v) => v);
|
|
5646
|
+
pgTypes.setTypeParser(114, (v) => v);
|
|
5647
|
+
var toIso = (v) => v == null ? null : new Date(v).toISOString();
|
|
5648
|
+
pgTypes.setTypeParser(1184, (v) => toIso(v));
|
|
5649
|
+
pgTypes.setTypeParser(1114, (v) => toIso(v));
|
|
5650
|
+
|
|
5651
|
+
class NotImplementedError extends Error {
|
|
5652
|
+
code = "not_implemented";
|
|
5653
|
+
constructor(method) {
|
|
5654
|
+
super(`PostgresLoopStorage.${method} is not implemented on the Postgres backend yet. ` + `This path must not silently no-op; port the sqlite Store.${method} logic.`);
|
|
5655
|
+
this.name = "NotImplementedError";
|
|
5656
|
+
}
|
|
5657
|
+
}
|
|
5658
|
+
var TERMINAL_RUN_STATUSES2 = ["succeeded", "failed", "timed_out", "abandoned", "skipped"];
|
|
5659
|
+
var PRUNE_BATCH_SIZE2 = 400;
|
|
5660
|
+
function isUniqueViolation(error) {
|
|
5661
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "23505";
|
|
5662
|
+
}
|
|
5663
|
+
var DEFAULT_RECOVERY_BATCH_LIMIT2 = 100;
|
|
5664
|
+
var DEFAULT_RECOVERY_SCAN_MULTIPLIER2 = 5;
|
|
5665
|
+
|
|
5666
|
+
class PostgresLoopStorage {
|
|
5667
|
+
client;
|
|
5668
|
+
backend = "postgres";
|
|
5669
|
+
supportsRemoteRunners = true;
|
|
5670
|
+
constructor(client) {
|
|
5671
|
+
this.client = client;
|
|
5672
|
+
}
|
|
5673
|
+
async close() {
|
|
5674
|
+
await this.client.close();
|
|
5675
|
+
}
|
|
5676
|
+
async assertDaemonLeaseFence(c, opts, now) {
|
|
5677
|
+
if (!opts.daemonLeaseId)
|
|
5678
|
+
return;
|
|
5679
|
+
const row = await c.get("SELECT id FROM daemon_lease WHERE id = $1 AND expires_at > $2", [opts.daemonLeaseId, now]);
|
|
5680
|
+
if (!row)
|
|
5681
|
+
throw new Error("daemon lease lost");
|
|
5682
|
+
}
|
|
5683
|
+
async loadLoop(c, id) {
|
|
5684
|
+
const row = await c.get("SELECT * FROM loops WHERE id = $1", [id]);
|
|
5685
|
+
return row ? rowToLoop(row) : undefined;
|
|
5686
|
+
}
|
|
5687
|
+
async loadRun(c, id) {
|
|
5688
|
+
const row = await c.get("SELECT * FROM loop_runs WHERE id = $1", [id]);
|
|
5689
|
+
return row ? rowToRun(row) : undefined;
|
|
5690
|
+
}
|
|
5691
|
+
async loadRunBySlot(c, loopId, scheduledFor) {
|
|
5692
|
+
const row = await c.get("SELECT * FROM loop_runs WHERE loop_id = $1 AND scheduled_for = $2", [loopId, scheduledFor]);
|
|
5693
|
+
return row ? rowToRun(row) : undefined;
|
|
5694
|
+
}
|
|
5695
|
+
async loadDaemonLease(c) {
|
|
5696
|
+
const row = await c.get("SELECT * FROM daemon_lease LIMIT 1", []);
|
|
5697
|
+
return row ? rowToLease(row) : undefined;
|
|
5698
|
+
}
|
|
5699
|
+
async setWorkItemsForLoop(c, loopId, status, reason, updated, statuses = ["admitted", "running"]) {
|
|
5700
|
+
const placeholders = statuses.map((_, i) => `$${i + 5}`).join(",");
|
|
5701
|
+
await c.execute(`UPDATE workflow_work_items
|
|
5702
|
+
SET status=$1, lease_expires_at=NULL, last_reason=COALESCE($2, last_reason), updated_at=$3
|
|
5703
|
+
WHERE loop_id = $4 AND status IN (${placeholders})`, [status, reason ?? null, updated, loopId, ...statuses]);
|
|
5704
|
+
}
|
|
5705
|
+
async setWorkItemsForWorkflowRun(c, workflowRunId, status, reason, updated, statuses = ["admitted", "running"]) {
|
|
5706
|
+
const placeholders = statuses.map((_, i) => `$${i + 5}`).join(",");
|
|
5707
|
+
await c.execute(`UPDATE workflow_work_items
|
|
5708
|
+
SET status=$1, lease_expires_at=NULL, last_reason=COALESCE($2, last_reason), updated_at=$3
|
|
5709
|
+
WHERE workflow_run_id = $4 AND status IN (${placeholders})`, [status, reason ?? null, updated, workflowRunId, ...statuses]);
|
|
5710
|
+
}
|
|
5711
|
+
async cascadeWorkItemsForLoopRun(c, run, reason, updated) {
|
|
5712
|
+
const loop = await this.loadLoop(c, run.loopId);
|
|
5713
|
+
const status = workItemStatusForLoopRun(run.status, run.attempt, loop?.maxAttempts);
|
|
5714
|
+
if (!status)
|
|
5715
|
+
return;
|
|
5716
|
+
const statuses = status === "admitted" ? ["admitted", "running", "failed"] : ["admitted", "running"];
|
|
5717
|
+
const nextReason = status === "admitted" ? reason ? `attempt failed; retry pending: ${reason}` : "attempt failed; retry pending" : reason;
|
|
5718
|
+
await this.setWorkItemsForLoop(c, run.loopId, status, nextReason, updated, statuses);
|
|
5719
|
+
}
|
|
5720
|
+
async createLoop(...args) {
|
|
5721
|
+
const [input, from = new Date] = args;
|
|
5722
|
+
const now = nowIso();
|
|
5723
|
+
const target = input.target.type === "workflow" ? input.target : normalizeCreateWorkflowInput({
|
|
5724
|
+
name: "loop-target-validation",
|
|
5725
|
+
steps: [{ id: "target", target: input.target }]
|
|
5726
|
+
}).steps[0].target;
|
|
5727
|
+
if (input.goal && target.type === "workflow") {
|
|
5728
|
+
const workflow = await this.loadWorkflow(this.client, target.workflowId);
|
|
5729
|
+
if (workflow?.goal) {
|
|
5730
|
+
throw new Error(`workflow loop cannot define a loop-level goal when workflow ${workflow.name} already has a top-level goal; remove one goal wrapper`);
|
|
5731
|
+
}
|
|
5732
|
+
}
|
|
5733
|
+
const loop = {
|
|
5734
|
+
id: genId(),
|
|
5735
|
+
name: input.name,
|
|
5736
|
+
description: input.description,
|
|
5737
|
+
status: "active",
|
|
5738
|
+
schedule: input.schedule,
|
|
5739
|
+
target,
|
|
5740
|
+
goal: input.goal,
|
|
5741
|
+
machine: input.machine,
|
|
5742
|
+
nextRunAt: initialNextRun(input.schedule, from),
|
|
5743
|
+
catchUp: input.catchUp ?? "latest",
|
|
5744
|
+
catchUpLimit: input.catchUpLimit ?? 50,
|
|
5745
|
+
overlap: input.overlap ?? "skip",
|
|
5746
|
+
maxAttempts: input.maxAttempts ?? 1,
|
|
5747
|
+
retryDelayMs: input.retryDelayMs ?? 60000,
|
|
5748
|
+
leaseMs: input.leaseMs ?? 30 * 60000,
|
|
5749
|
+
expiresAt: input.expiresAt,
|
|
5750
|
+
createdAt: now,
|
|
5751
|
+
updatedAt: now
|
|
5752
|
+
};
|
|
5753
|
+
await this.client.execute(`INSERT INTO loops (id, name, description, status, schedule_json, target_json, machine_json, next_run_at, retry_scheduled_for,
|
|
5754
|
+
goal_json, catch_up, catch_up_limit, overlap, max_attempts, retry_delay_ms, lease_ms, expires_at, created_at, updated_at)
|
|
5755
|
+
VALUES ($1,$2,$3,$4,$5::jsonb,$6::jsonb,$7::jsonb,$8,NULL,$9::jsonb,$10,$11,$12,$13,$14,$15,$16,$17,$18)`, [
|
|
5756
|
+
loop.id,
|
|
5757
|
+
loop.name,
|
|
5758
|
+
loop.description ?? null,
|
|
5759
|
+
loop.status,
|
|
5760
|
+
JSON.stringify(loop.schedule),
|
|
5761
|
+
JSON.stringify(loop.target),
|
|
5762
|
+
loop.machine ? JSON.stringify(loop.machine) : null,
|
|
5763
|
+
loop.nextRunAt ?? null,
|
|
5764
|
+
loop.goal ? JSON.stringify(loop.goal) : null,
|
|
5765
|
+
loop.catchUp,
|
|
5766
|
+
loop.catchUpLimit,
|
|
5767
|
+
loop.overlap,
|
|
5768
|
+
loop.maxAttempts,
|
|
5769
|
+
loop.retryDelayMs,
|
|
5770
|
+
loop.leaseMs,
|
|
5771
|
+
loop.expiresAt ?? null,
|
|
5772
|
+
loop.createdAt,
|
|
5773
|
+
loop.updatedAt
|
|
5774
|
+
]);
|
|
5775
|
+
return loop;
|
|
5776
|
+
}
|
|
5777
|
+
async getLoop(...args) {
|
|
5778
|
+
return this.loadLoop(this.client, args[0]);
|
|
5779
|
+
}
|
|
5780
|
+
async findLoopByName(...args) {
|
|
5781
|
+
const row = await this.client.get("SELECT * FROM loops WHERE name = $1 ORDER BY created_at DESC LIMIT 1", [args[0]]);
|
|
5782
|
+
return row ? rowToLoop(row) : undefined;
|
|
5783
|
+
}
|
|
5784
|
+
async requireLoop(...args) {
|
|
5785
|
+
const idOrName = args[0];
|
|
5786
|
+
const found = await this.getLoop(idOrName) ?? await this.findLoopByName(idOrName);
|
|
5787
|
+
if (!found)
|
|
5788
|
+
throw new LoopNotFoundError(idOrName);
|
|
5789
|
+
return found;
|
|
5790
|
+
}
|
|
5791
|
+
async listLoops(...args) {
|
|
5792
|
+
const opts = args[0] ?? {};
|
|
5793
|
+
const limit = opts.limit ?? 200;
|
|
5794
|
+
const offset = Math.max(0, Math.floor(opts.offset ?? 0));
|
|
5795
|
+
let rows;
|
|
5796
|
+
if (opts.name != null) {
|
|
5797
|
+
rows = await this.client.many("SELECT * FROM loops WHERE name = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3", [opts.name, limit, offset]);
|
|
5798
|
+
} else if (opts.status && opts.archived) {
|
|
5799
|
+
rows = await this.client.many("SELECT * FROM loops WHERE status = $1 AND archived_at IS NOT NULL ORDER BY next_run_at ASC LIMIT $2 OFFSET $3", [opts.status, limit, offset]);
|
|
5800
|
+
} else if (opts.status && opts.includeArchived) {
|
|
5801
|
+
rows = await this.client.many("SELECT * FROM loops WHERE status = $1 ORDER BY next_run_at ASC LIMIT $2 OFFSET $3", [opts.status, limit, offset]);
|
|
5802
|
+
} else if (opts.status) {
|
|
5803
|
+
rows = await this.client.many("SELECT * FROM loops WHERE status = $1 AND archived_at IS NULL ORDER BY next_run_at ASC LIMIT $2 OFFSET $3", [opts.status, limit, offset]);
|
|
5804
|
+
} else if (opts.archived) {
|
|
5805
|
+
rows = await this.client.many("SELECT * FROM loops WHERE archived_at IS NOT NULL ORDER BY archived_at DESC LIMIT $1 OFFSET $2", [limit, offset]);
|
|
5806
|
+
} else if (opts.includeArchived) {
|
|
5807
|
+
rows = await this.client.many("SELECT * FROM loops ORDER BY status ASC, next_run_at ASC LIMIT $1 OFFSET $2", [limit, offset]);
|
|
5808
|
+
} else {
|
|
5809
|
+
rows = await this.client.many("SELECT * FROM loops WHERE archived_at IS NULL ORDER BY status ASC, next_run_at ASC LIMIT $1 OFFSET $2", [limit, offset]);
|
|
5810
|
+
}
|
|
5811
|
+
return rows.map(rowToLoop);
|
|
5812
|
+
}
|
|
5813
|
+
async dueLoops(...args) {
|
|
5814
|
+
const [now, limit = 500] = args;
|
|
5815
|
+
const rows = await this.client.many(`SELECT * FROM loops
|
|
5816
|
+
WHERE status = 'active' AND archived_at IS NULL AND next_run_at IS NOT NULL AND next_run_at <= $1
|
|
5817
|
+
ORDER BY next_run_at ASC LIMIT $2`, [now.toISOString(), limit]);
|
|
5818
|
+
return rows.map(rowToLoop);
|
|
5819
|
+
}
|
|
5820
|
+
async updateLoop(...args) {
|
|
5821
|
+
const [id, patch, opts = {}] = args;
|
|
5822
|
+
const updated = (opts.now ?? new Date).toISOString();
|
|
5823
|
+
return this.client.transaction(async (c) => {
|
|
5824
|
+
const current = await this.loadLoop(c, id);
|
|
5825
|
+
if (!current)
|
|
5826
|
+
throw new LoopNotFoundError(id);
|
|
5827
|
+
if (current.archivedAt)
|
|
5828
|
+
throw new LoopArchivedError(current.name || id);
|
|
5829
|
+
const merged = { ...current, ...patch, updatedAt: updated };
|
|
5830
|
+
const res = await c.query(`UPDATE loops SET status=$1, next_run_at=$2, retry_scheduled_for=$3, expires_at=$4, updated_at=$5
|
|
5831
|
+
WHERE id=$6
|
|
5832
|
+
AND ($7::text IS NULL OR EXISTS (SELECT 1 FROM daemon_lease WHERE id=$7 AND expires_at > $8))`, [
|
|
5833
|
+
merged.status,
|
|
5834
|
+
merged.nextRunAt ?? null,
|
|
5835
|
+
merged.retryScheduledFor ?? null,
|
|
5836
|
+
merged.expiresAt ?? null,
|
|
5837
|
+
merged.updatedAt,
|
|
5838
|
+
id,
|
|
5839
|
+
opts.daemonLeaseId ?? null,
|
|
5840
|
+
updated
|
|
5841
|
+
]);
|
|
5842
|
+
if (res.rowCount !== 1)
|
|
5843
|
+
throw new Error("daemon lease lost");
|
|
5844
|
+
if (patch.status && patch.status !== "active") {
|
|
5845
|
+
const status = patch.status === "paused" ? "deferred" : "cancelled";
|
|
5846
|
+
await this.setWorkItemsForLoop(c, id, status, `loop ${patch.status}`, updated);
|
|
5847
|
+
}
|
|
5848
|
+
const after = await this.loadLoop(c, id);
|
|
5849
|
+
if (!after)
|
|
5850
|
+
throw new Error(`loop not found after update: ${id}`);
|
|
5851
|
+
return after;
|
|
5852
|
+
});
|
|
5853
|
+
}
|
|
5854
|
+
async renameLoop(...args) {
|
|
5855
|
+
const [id, name, opts = {}] = args;
|
|
5856
|
+
const current = await this.getLoop(id);
|
|
5857
|
+
if (!current)
|
|
5858
|
+
throw new LoopNotFoundError(id);
|
|
5859
|
+
const trimmed = name.trim();
|
|
5860
|
+
if (!trimmed)
|
|
5861
|
+
throw new ValidationError("loop name must not be empty");
|
|
5862
|
+
const updated = (opts.now ?? new Date).toISOString();
|
|
5863
|
+
await this.client.execute(`UPDATE loops SET name=$1, updated_at=$2
|
|
5864
|
+
WHERE id=$3 AND ($4::text IS NULL OR EXISTS (SELECT 1 FROM daemon_lease WHERE id=$4 AND expires_at > $5))`, [trimmed, updated, id, opts.daemonLeaseId ?? null, updated]);
|
|
5865
|
+
const after = await this.getLoop(id);
|
|
5866
|
+
if (!after)
|
|
5867
|
+
throw new Error(`loop not found after rename: ${id}`);
|
|
5868
|
+
return after;
|
|
5869
|
+
}
|
|
5870
|
+
async archiveLoop(...args) {
|
|
5871
|
+
const idOrName = args[0];
|
|
5872
|
+
return this.client.transaction(async (c) => {
|
|
5873
|
+
const loop = await this.requireLoopIn(c, idOrName);
|
|
5874
|
+
if (loop.archivedAt)
|
|
5875
|
+
return loop;
|
|
5876
|
+
const updated = nowIso();
|
|
5877
|
+
const archivedStatus = loop.status === "active" ? "paused" : loop.status;
|
|
5878
|
+
await c.execute(`UPDATE loops SET status=$1, archived_at=$2, archived_from_status=$3, updated_at=$4 WHERE id=$5`, [archivedStatus, updated, loop.status, updated, loop.id]);
|
|
5879
|
+
await this.setWorkItemsForLoop(c, loop.id, "deferred", "loop archived", updated);
|
|
5880
|
+
const archived = await this.loadLoop(c, loop.id);
|
|
5881
|
+
if (!archived)
|
|
5882
|
+
throw new Error(`loop not found after archive: ${loop.id}`);
|
|
5883
|
+
return archived;
|
|
5884
|
+
});
|
|
5885
|
+
}
|
|
5886
|
+
async unarchiveLoop(...args) {
|
|
5887
|
+
const idOrName = args[0];
|
|
5888
|
+
const loop = await this.requireLoop(idOrName);
|
|
5889
|
+
if (!loop.archivedAt)
|
|
5890
|
+
return loop;
|
|
5891
|
+
const updated = nowIso();
|
|
5892
|
+
const restoredStatus = loop.archivedFromStatus ?? loop.status;
|
|
5893
|
+
await this.client.execute(`UPDATE loops SET status=$1, archived_at=NULL, archived_from_status=NULL, updated_at=$2 WHERE id=$3`, [restoredStatus, updated, loop.id]);
|
|
5894
|
+
const unarchived = await this.getLoop(loop.id);
|
|
5895
|
+
if (!unarchived)
|
|
5896
|
+
throw new Error(`loop not found after unarchive: ${loop.id}`);
|
|
5897
|
+
return unarchived;
|
|
5898
|
+
}
|
|
5899
|
+
async deleteLoop(...args) {
|
|
5900
|
+
const idOrName = args[0];
|
|
5901
|
+
return this.client.transaction(async (c) => {
|
|
5902
|
+
const loop = await this.requireLoopIn(c, idOrName);
|
|
5903
|
+
await this.setWorkItemsForLoop(c, loop.id, "cancelled", "loop deleted", nowIso());
|
|
5904
|
+
const res = await c.query(`DELETE FROM loops WHERE id = $1`, [loop.id]);
|
|
5905
|
+
return res.rowCount > 0;
|
|
5906
|
+
});
|
|
5907
|
+
}
|
|
5908
|
+
async requireLoopIn(c, idOrName) {
|
|
5909
|
+
const byId = await this.loadLoop(c, idOrName);
|
|
5910
|
+
if (byId)
|
|
5911
|
+
return byId;
|
|
5912
|
+
const row = await c.get("SELECT * FROM loops WHERE name = $1 ORDER BY created_at DESC LIMIT 1", [idOrName]);
|
|
5913
|
+
if (!row)
|
|
5914
|
+
throw new LoopNotFoundError(idOrName);
|
|
5915
|
+
return rowToLoop(row);
|
|
5916
|
+
}
|
|
5917
|
+
async countLoops(...args) {
|
|
5918
|
+
const [status, opts = {}] = args;
|
|
5919
|
+
let sql;
|
|
5920
|
+
const params = [];
|
|
5921
|
+
if (status && opts.archived) {
|
|
5922
|
+
sql = "SELECT COUNT(*)::int AS count FROM loops WHERE status = $1 AND archived_at IS NOT NULL";
|
|
5923
|
+
params.push(status);
|
|
5924
|
+
} else if (status && opts.includeArchived) {
|
|
5925
|
+
sql = "SELECT COUNT(*)::int AS count FROM loops WHERE status = $1";
|
|
5926
|
+
params.push(status);
|
|
5927
|
+
} else if (status) {
|
|
5928
|
+
sql = "SELECT COUNT(*)::int AS count FROM loops WHERE status = $1 AND archived_at IS NULL";
|
|
5929
|
+
params.push(status);
|
|
5930
|
+
} else if (opts.archived) {
|
|
5931
|
+
sql = "SELECT COUNT(*)::int AS count FROM loops WHERE archived_at IS NOT NULL";
|
|
5932
|
+
} else if (opts.includeArchived) {
|
|
5933
|
+
sql = "SELECT COUNT(*)::int AS count FROM loops";
|
|
5934
|
+
} else {
|
|
5935
|
+
sql = "SELECT COUNT(*)::int AS count FROM loops WHERE archived_at IS NULL";
|
|
5936
|
+
}
|
|
5937
|
+
const row = await this.client.get(sql, params);
|
|
5938
|
+
return row?.count ?? 0;
|
|
5939
|
+
}
|
|
5940
|
+
async upsertMigrationWorkflow(...args) {
|
|
5941
|
+
const [workflow, opts = {}] = args;
|
|
5942
|
+
const existing = await this.getWorkflow(workflow.id);
|
|
5943
|
+
if (existing && !opts.replace)
|
|
5944
|
+
return existing;
|
|
5945
|
+
try {
|
|
5946
|
+
await this.client.execute(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
|
|
5947
|
+
VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7::jsonb,$8,$9)
|
|
5948
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
5949
|
+
name=EXCLUDED.name,
|
|
5950
|
+
description=EXCLUDED.description,
|
|
5951
|
+
version=EXCLUDED.version,
|
|
5952
|
+
status=EXCLUDED.status,
|
|
5953
|
+
goal_json=EXCLUDED.goal_json,
|
|
5954
|
+
steps_json=EXCLUDED.steps_json,
|
|
5955
|
+
created_at=EXCLUDED.created_at,
|
|
5956
|
+
updated_at=EXCLUDED.updated_at`, [
|
|
5957
|
+
workflow.id,
|
|
5958
|
+
workflow.name,
|
|
5959
|
+
workflow.description ?? null,
|
|
5960
|
+
workflow.version,
|
|
5961
|
+
workflow.status,
|
|
5962
|
+
workflow.goal ? JSON.stringify(workflow.goal) : null,
|
|
5963
|
+
JSON.stringify(workflow.steps),
|
|
5964
|
+
workflow.createdAt,
|
|
5965
|
+
workflow.updatedAt
|
|
5966
|
+
]);
|
|
5967
|
+
} catch (error) {
|
|
5968
|
+
if (isUniqueViolation(error)) {
|
|
5969
|
+
const owner = await this.client.get("SELECT * FROM workflow_specs WHERE name = $1 AND status = 'active' LIMIT 1", [workflow.name]);
|
|
5970
|
+
if (owner)
|
|
5971
|
+
return rowToWorkflow(owner);
|
|
5972
|
+
}
|
|
5973
|
+
throw error;
|
|
5974
|
+
}
|
|
5975
|
+
const imported = await this.getWorkflow(workflow.id);
|
|
5976
|
+
if (!imported)
|
|
5977
|
+
throw new Error(`workflow not found after migration import: ${workflow.id}`);
|
|
5978
|
+
return imported;
|
|
5979
|
+
}
|
|
5980
|
+
async upsertMigrationLoop(...args) {
|
|
5981
|
+
const [loop, opts = {}] = args;
|
|
5982
|
+
const existing = await this.loadLoop(this.client, loop.id);
|
|
5983
|
+
if (existing && !opts.replace)
|
|
5984
|
+
return existing;
|
|
5985
|
+
await this.client.execute(`INSERT INTO loops (id, name, description, status, archived_at, archived_from_status, schedule_json, target_json,
|
|
5986
|
+
goal_json, machine_json, next_run_at, retry_scheduled_for, catch_up, catch_up_limit, overlap, max_attempts,
|
|
5987
|
+
retry_delay_ms, lease_ms, expires_at, created_at, updated_at)
|
|
5988
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb,$8::jsonb,$9::jsonb,$10::jsonb,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21)
|
|
5989
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
5990
|
+
name=EXCLUDED.name,
|
|
5991
|
+
description=EXCLUDED.description,
|
|
5992
|
+
status=EXCLUDED.status,
|
|
5993
|
+
archived_at=EXCLUDED.archived_at,
|
|
5994
|
+
archived_from_status=EXCLUDED.archived_from_status,
|
|
5995
|
+
schedule_json=EXCLUDED.schedule_json,
|
|
5996
|
+
target_json=EXCLUDED.target_json,
|
|
5997
|
+
goal_json=EXCLUDED.goal_json,
|
|
5998
|
+
machine_json=EXCLUDED.machine_json,
|
|
5999
|
+
next_run_at=EXCLUDED.next_run_at,
|
|
6000
|
+
retry_scheduled_for=EXCLUDED.retry_scheduled_for,
|
|
6001
|
+
catch_up=EXCLUDED.catch_up,
|
|
6002
|
+
catch_up_limit=EXCLUDED.catch_up_limit,
|
|
6003
|
+
overlap=EXCLUDED.overlap,
|
|
6004
|
+
max_attempts=EXCLUDED.max_attempts,
|
|
6005
|
+
retry_delay_ms=EXCLUDED.retry_delay_ms,
|
|
6006
|
+
lease_ms=EXCLUDED.lease_ms,
|
|
6007
|
+
expires_at=EXCLUDED.expires_at,
|
|
6008
|
+
created_at=EXCLUDED.created_at,
|
|
6009
|
+
updated_at=EXCLUDED.updated_at`, [
|
|
6010
|
+
loop.id,
|
|
6011
|
+
loop.name,
|
|
6012
|
+
loop.description ?? null,
|
|
6013
|
+
loop.status,
|
|
6014
|
+
loop.archivedAt ?? null,
|
|
6015
|
+
loop.archivedFromStatus ?? null,
|
|
6016
|
+
JSON.stringify(loop.schedule),
|
|
6017
|
+
JSON.stringify(loop.target),
|
|
6018
|
+
loop.goal ? JSON.stringify(loop.goal) : null,
|
|
6019
|
+
loop.machine ? JSON.stringify(loop.machine) : null,
|
|
6020
|
+
loop.nextRunAt ?? null,
|
|
6021
|
+
loop.retryScheduledFor ?? null,
|
|
6022
|
+
loop.catchUp,
|
|
6023
|
+
loop.catchUpLimit,
|
|
6024
|
+
loop.overlap,
|
|
6025
|
+
loop.maxAttempts,
|
|
6026
|
+
loop.retryDelayMs,
|
|
6027
|
+
loop.leaseMs,
|
|
6028
|
+
loop.expiresAt ?? null,
|
|
6029
|
+
loop.createdAt,
|
|
6030
|
+
loop.updatedAt
|
|
6031
|
+
]);
|
|
6032
|
+
const imported = await this.loadLoop(this.client, loop.id);
|
|
6033
|
+
if (!imported)
|
|
6034
|
+
throw new Error(`loop not found after migration import: ${loop.id}`);
|
|
6035
|
+
return imported;
|
|
6036
|
+
}
|
|
6037
|
+
async upsertMigrationRun(...args) {
|
|
6038
|
+
const [run, opts = {}] = args;
|
|
6039
|
+
if (run.status === "running")
|
|
6040
|
+
throw new ValidationError(`cannot import running run ${run.id}`);
|
|
6041
|
+
const existing = await this.loadRun(this.client, run.id);
|
|
6042
|
+
if (existing && !opts.replace)
|
|
6043
|
+
return existing;
|
|
6044
|
+
try {
|
|
6045
|
+
await this.client.execute(`INSERT INTO loop_runs (id, loop_id, loop_name, scheduled_for, attempt, status, started_at, finished_at,
|
|
6046
|
+
claimed_by, claim_token, lease_expires_at, pid, pgid, process_started_at, exit_code, duration_ms,
|
|
6047
|
+
stdout, stderr, error, goal_run_id, created_at, updated_at)
|
|
6048
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,NULL,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21)
|
|
6049
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
6050
|
+
loop_id=EXCLUDED.loop_id,
|
|
6051
|
+
loop_name=EXCLUDED.loop_name,
|
|
6052
|
+
scheduled_for=EXCLUDED.scheduled_for,
|
|
6053
|
+
attempt=EXCLUDED.attempt,
|
|
6054
|
+
status=EXCLUDED.status,
|
|
6055
|
+
started_at=EXCLUDED.started_at,
|
|
6056
|
+
finished_at=EXCLUDED.finished_at,
|
|
6057
|
+
claimed_by=EXCLUDED.claimed_by,
|
|
6058
|
+
claim_token=NULL,
|
|
6059
|
+
lease_expires_at=EXCLUDED.lease_expires_at,
|
|
6060
|
+
pid=EXCLUDED.pid,
|
|
6061
|
+
pgid=EXCLUDED.pgid,
|
|
6062
|
+
process_started_at=EXCLUDED.process_started_at,
|
|
6063
|
+
exit_code=EXCLUDED.exit_code,
|
|
6064
|
+
duration_ms=EXCLUDED.duration_ms,
|
|
6065
|
+
stdout=EXCLUDED.stdout,
|
|
6066
|
+
stderr=EXCLUDED.stderr,
|
|
6067
|
+
error=EXCLUDED.error,
|
|
6068
|
+
goal_run_id=EXCLUDED.goal_run_id,
|
|
6069
|
+
created_at=EXCLUDED.created_at,
|
|
6070
|
+
updated_at=EXCLUDED.updated_at`, [
|
|
6071
|
+
run.id,
|
|
6072
|
+
run.loopId,
|
|
6073
|
+
run.loopName,
|
|
6074
|
+
run.scheduledFor,
|
|
6075
|
+
run.attempt,
|
|
6076
|
+
run.status,
|
|
6077
|
+
run.startedAt ?? null,
|
|
6078
|
+
run.finishedAt ?? null,
|
|
6079
|
+
run.claimedBy ?? null,
|
|
6080
|
+
run.leaseExpiresAt ?? null,
|
|
6081
|
+
run.pid ?? null,
|
|
6082
|
+
run.pgid ?? null,
|
|
6083
|
+
run.processStartedAt ?? null,
|
|
6084
|
+
run.exitCode ?? null,
|
|
6085
|
+
run.durationMs ?? null,
|
|
6086
|
+
persistedRunOutput(run.stdout),
|
|
6087
|
+
persistedRunOutput(run.stderr),
|
|
6088
|
+
scrubbedOrNull(run.error),
|
|
6089
|
+
run.goalRunId ?? null,
|
|
6090
|
+
run.createdAt,
|
|
6091
|
+
run.updatedAt
|
|
6092
|
+
]);
|
|
6093
|
+
} catch (error) {
|
|
6094
|
+
if (isUniqueViolation(error)) {
|
|
6095
|
+
const slot = await this.loadRunBySlot(this.client, run.loopId, run.scheduledFor);
|
|
6096
|
+
if (slot)
|
|
6097
|
+
return slot;
|
|
6098
|
+
}
|
|
6099
|
+
throw error;
|
|
6100
|
+
}
|
|
6101
|
+
const imported = await this.loadRun(this.client, run.id);
|
|
6102
|
+
if (!imported)
|
|
6103
|
+
throw new Error(`run not found after migration import: ${run.id}`);
|
|
6104
|
+
return imported;
|
|
6105
|
+
}
|
|
6106
|
+
async createSkippedRun(...args) {
|
|
6107
|
+
const [loop, scheduledFor, reason, opts = {}] = args;
|
|
6108
|
+
const now = nowIso();
|
|
6109
|
+
const id = genId();
|
|
6110
|
+
return this.client.transaction(async (c) => {
|
|
6111
|
+
await this.assertDaemonLeaseFence(c, opts, now);
|
|
6112
|
+
await c.execute(`INSERT INTO loop_runs (id, loop_id, loop_name, scheduled_for, attempt, status, started_at, finished_at,
|
|
6113
|
+
claimed_by, lease_expires_at, pid, exit_code, duration_ms, stdout, stderr, error, created_at, updated_at)
|
|
6114
|
+
VALUES ($1,$2,$3,$4,1,'skipped',NULL,$5,NULL,NULL,NULL,NULL,NULL,NULL,NULL,$6,$7,$7)
|
|
6115
|
+
ON CONFLICT (loop_id, scheduled_for) DO NOTHING`, [id, loop.id, loop.name, scheduledFor, now, reason, now]);
|
|
6116
|
+
const run = await this.loadRunBySlot(c, loop.id, scheduledFor);
|
|
6117
|
+
if (run)
|
|
6118
|
+
return run;
|
|
6119
|
+
return {
|
|
6120
|
+
id,
|
|
6121
|
+
loopId: loop.id,
|
|
6122
|
+
loopName: loop.name,
|
|
6123
|
+
scheduledFor,
|
|
6124
|
+
attempt: 1,
|
|
6125
|
+
status: "skipped",
|
|
6126
|
+
finishedAt: now,
|
|
6127
|
+
error: reason,
|
|
6128
|
+
createdAt: now,
|
|
6129
|
+
updatedAt: now
|
|
6130
|
+
};
|
|
6131
|
+
});
|
|
6132
|
+
}
|
|
6133
|
+
async getRun(...args) {
|
|
6134
|
+
return this.loadRun(this.client, args[0]);
|
|
6135
|
+
}
|
|
6136
|
+
async getRunBySlot(...args) {
|
|
6137
|
+
return this.loadRunBySlot(this.client, args[0], args[1]);
|
|
6138
|
+
}
|
|
6139
|
+
async claimRun(...args) {
|
|
6140
|
+
const [loopArg, scheduledFor, runnerId, now = new Date, opts = {}] = args;
|
|
6141
|
+
const startedAt = now.toISOString();
|
|
6142
|
+
const claimToken = opts.claimToken ?? genId();
|
|
6143
|
+
return this.client.transaction(async (c) => {
|
|
6144
|
+
await this.assertDaemonLeaseFence(c, opts, startedAt);
|
|
6145
|
+
const loop = await this.loadLoop(c, loopArg.id);
|
|
6146
|
+
if (!loop || loop.archivedAt)
|
|
6147
|
+
return;
|
|
6148
|
+
const leaseExpiresAt = new Date(now.getTime() + loop.leaseMs).toISOString();
|
|
6149
|
+
if (loop.overlap === "skip") {
|
|
6150
|
+
const blocking = await c.get(`SELECT id FROM loop_runs
|
|
6151
|
+
WHERE loop_id=$1 AND scheduled_for<>$2 AND status='running'
|
|
6152
|
+
AND lease_expires_at IS NOT NULL AND lease_expires_at > $3
|
|
6153
|
+
LIMIT 1`, [loop.id, scheduledFor, startedAt]);
|
|
6154
|
+
if (blocking)
|
|
6155
|
+
return;
|
|
6156
|
+
}
|
|
6157
|
+
const existing = await c.get("SELECT * FROM loop_runs WHERE loop_id=$1 AND scheduled_for=$2 FOR UPDATE", [loop.id, scheduledFor]);
|
|
6158
|
+
if (existing) {
|
|
6159
|
+
if (existing.status === "running") {
|
|
6160
|
+
if (existing.lease_expires_at && existing.lease_expires_at > startedAt) {
|
|
6161
|
+
return;
|
|
6162
|
+
}
|
|
6163
|
+
const res3 = await c.query(`UPDATE loop_runs SET status='running', started_at=$2, finished_at=NULL, claimed_by=$3, claim_token=$4,
|
|
6164
|
+
lease_expires_at=$5, pid=NULL, pgid=NULL, process_started_at=NULL, exit_code=NULL, duration_ms=NULL,
|
|
6165
|
+
stdout=NULL, stderr=NULL, error=NULL, updated_at=$2
|
|
6166
|
+
WHERE id=$1 AND status='running' AND lease_expires_at <= $6`, [existing.id, startedAt, runnerId, claimToken, leaseExpiresAt, startedAt]);
|
|
6167
|
+
if (res3.rowCount !== 1)
|
|
6168
|
+
return;
|
|
6169
|
+
const run3 = await this.loadRun(c, existing.id);
|
|
6170
|
+
return run3 ? { run: run3, loop, claimToken } : undefined;
|
|
6171
|
+
}
|
|
6172
|
+
if (existing.status === "succeeded" || existing.status === "skipped")
|
|
6173
|
+
return;
|
|
6174
|
+
const attempt = existing.attempt + 1;
|
|
6175
|
+
const res2 = await c.query(`UPDATE loop_runs SET attempt=$2, status='running', started_at=$3, finished_at=NULL, claimed_by=$4, claim_token=$5,
|
|
6176
|
+
lease_expires_at=$6, pid=NULL, pgid=NULL, process_started_at=NULL, exit_code=NULL, duration_ms=NULL,
|
|
6177
|
+
stdout=NULL, stderr=NULL, error=NULL, updated_at=$3
|
|
6178
|
+
WHERE id=$1 AND status IN ('failed','timed_out','abandoned') AND attempt < $7`, [existing.id, attempt, startedAt, runnerId, claimToken, leaseExpiresAt, loop.maxAttempts]);
|
|
6179
|
+
if (res2.rowCount !== 1)
|
|
6180
|
+
return;
|
|
6181
|
+
const run2 = await this.loadRun(c, existing.id);
|
|
6182
|
+
return run2 ? { run: run2, loop, claimToken } : undefined;
|
|
6183
|
+
}
|
|
6184
|
+
const id = genId();
|
|
6185
|
+
const res = await c.query(`INSERT INTO loop_runs (id, loop_id, loop_name, scheduled_for, attempt, status, started_at, finished_at,
|
|
6186
|
+
claimed_by, claim_token, lease_expires_at, pid, exit_code, duration_ms, stdout, stderr, error, created_at, updated_at)
|
|
6187
|
+
VALUES ($1,$2,$3,$4,1,'running',$5,NULL,$6,$7,$8,NULL,NULL,NULL,NULL,NULL,NULL,$5,$5)
|
|
6188
|
+
ON CONFLICT (loop_id, scheduled_for) DO NOTHING`, [id, loop.id, loop.name, scheduledFor, startedAt, runnerId, claimToken, leaseExpiresAt]);
|
|
6189
|
+
if (res.rowCount !== 1)
|
|
6190
|
+
return;
|
|
6191
|
+
const run = await this.loadRun(c, id);
|
|
6192
|
+
return run ? { run, loop, claimToken } : undefined;
|
|
6193
|
+
});
|
|
6194
|
+
}
|
|
6195
|
+
async finalizeRun(...args) {
|
|
6196
|
+
const [id, patch, opts = {}] = args;
|
|
6197
|
+
const finishedAt = patch.finishedAt ?? nowIso();
|
|
6198
|
+
const error = patch.error === undefined ? undefined : persistedRunOutput(patch.error) ?? undefined;
|
|
6199
|
+
const nowStr = (opts.now ?? new Date).toISOString();
|
|
6200
|
+
return this.client.transaction(async (c) => {
|
|
6201
|
+
let res;
|
|
6202
|
+
if (opts.claimedBy) {
|
|
6203
|
+
res = await c.query(`UPDATE loop_runs SET status=$2, finished_at=$3, claim_token=NULL, lease_expires_at=NULL, pid=$4, exit_code=$5,
|
|
6204
|
+
duration_ms=$6, stdout=$7, stderr=$8, error=$9, updated_at=$3
|
|
6205
|
+
WHERE id=$1 AND status='running' AND claimed_by=$10 AND lease_expires_at > $11
|
|
6206
|
+
AND ($12::text IS NULL OR claim_token=$12)
|
|
6207
|
+
AND ($13::text IS NULL OR EXISTS (SELECT 1 FROM daemon_lease WHERE id=$13 AND expires_at > $11))`, [
|
|
6208
|
+
id,
|
|
6209
|
+
patch.status,
|
|
6210
|
+
finishedAt,
|
|
6211
|
+
patch.pid ?? null,
|
|
6212
|
+
patch.exitCode ?? null,
|
|
6213
|
+
patch.durationMs ?? null,
|
|
6214
|
+
persistedRunOutput(patch.stdout),
|
|
6215
|
+
persistedRunOutput(patch.stderr),
|
|
6216
|
+
error ?? null,
|
|
6217
|
+
opts.claimedBy,
|
|
6218
|
+
nowStr,
|
|
6219
|
+
opts.claimToken ?? null,
|
|
6220
|
+
opts.daemonLeaseId ?? null
|
|
6221
|
+
]);
|
|
6222
|
+
} else {
|
|
6223
|
+
res = await c.query(`UPDATE loop_runs SET status=$2, finished_at=$3, claim_token=NULL, lease_expires_at=NULL, pid=$4, exit_code=$5,
|
|
6224
|
+
duration_ms=$6, stdout=$7, stderr=$8, error=$9, updated_at=$3
|
|
6225
|
+
WHERE id=$1 AND status='running'`, [
|
|
6226
|
+
id,
|
|
6227
|
+
patch.status,
|
|
6228
|
+
finishedAt,
|
|
6229
|
+
patch.pid ?? null,
|
|
6230
|
+
patch.exitCode ?? null,
|
|
6231
|
+
patch.durationMs ?? null,
|
|
6232
|
+
persistedRunOutput(patch.stdout),
|
|
6233
|
+
persistedRunOutput(patch.stderr),
|
|
6234
|
+
error ?? null
|
|
6235
|
+
]);
|
|
6236
|
+
}
|
|
6237
|
+
const run = await this.loadRun(c, id);
|
|
6238
|
+
if (!run)
|
|
6239
|
+
throw new Error(`run not found after finalize: ${id}`);
|
|
6240
|
+
if (opts.claimedBy && res.rowCount !== 1)
|
|
6241
|
+
return run;
|
|
6242
|
+
if (res.rowCount === 1) {
|
|
6243
|
+
await this.cascadeWorkItemsForLoopRun(c, run, error, finishedAt);
|
|
6244
|
+
}
|
|
6245
|
+
return run;
|
|
6246
|
+
});
|
|
6247
|
+
}
|
|
6248
|
+
async heartbeatRunLease(...args) {
|
|
6249
|
+
const [id, claimedBy, leaseMs, now = new Date, opts = {}] = args;
|
|
6250
|
+
const nowStr = now.toISOString();
|
|
6251
|
+
const expiresAt = new Date(now.getTime() + leaseMs).toISOString();
|
|
6252
|
+
const res = await this.client.query(`UPDATE loop_runs SET lease_expires_at=$2, updated_at=$3
|
|
6253
|
+
WHERE id=$1 AND status='running' AND claimed_by=$4 AND lease_expires_at > $5
|
|
6254
|
+
AND ($6::text IS NULL OR claim_token=$6)
|
|
6255
|
+
AND ($7::text IS NULL OR EXISTS (SELECT 1 FROM daemon_lease WHERE id=$7 AND expires_at > $5))`, [id, expiresAt, nowStr, claimedBy, nowStr, opts.claimToken ?? null, opts.daemonLeaseId ?? null]);
|
|
6256
|
+
if (res.rowCount !== 1)
|
|
6257
|
+
return;
|
|
6258
|
+
return this.getRun(id);
|
|
6259
|
+
}
|
|
6260
|
+
async recordRunProcess(...args) {
|
|
6261
|
+
const [runId, info, opts = {}] = args;
|
|
6262
|
+
const now = (opts.now ?? new Date).toISOString();
|
|
6263
|
+
const res = await this.client.query(`UPDATE loop_runs SET pid=$2, pgid=$3, process_started_at=$4, updated_at=$5
|
|
6264
|
+
WHERE id=$1 AND status='running'
|
|
6265
|
+
AND ($6::text IS NULL OR EXISTS (SELECT 1 FROM daemon_lease WHERE id=$6 AND expires_at > $7))`, [runId, info.pid, info.pgid ?? null, info.processStartedAt ?? now, now, opts.daemonLeaseId ?? null, now]);
|
|
6266
|
+
if (res.rowCount !== 1)
|
|
6267
|
+
return;
|
|
6268
|
+
return this.getRun(runId);
|
|
6269
|
+
}
|
|
6270
|
+
async listRuns(...args) {
|
|
6271
|
+
const opts = args[0] ?? {};
|
|
6272
|
+
const limit = opts.limit ?? 100;
|
|
6273
|
+
const offset = Math.max(0, Math.floor(opts.offset ?? 0));
|
|
6274
|
+
let rows;
|
|
6275
|
+
if (opts.loopId && opts.status) {
|
|
6276
|
+
rows = await this.client.many("SELECT * FROM loop_runs WHERE loop_id = $1 AND status = $2 ORDER BY created_at DESC LIMIT $3 OFFSET $4", [opts.loopId, opts.status, limit, offset]);
|
|
6277
|
+
} else if (opts.loopId) {
|
|
6278
|
+
rows = await this.client.many("SELECT * FROM loop_runs WHERE loop_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3", [opts.loopId, limit, offset]);
|
|
6279
|
+
} else if (opts.status) {
|
|
6280
|
+
rows = await this.client.many("SELECT * FROM loop_runs WHERE status = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3", [opts.status, limit, offset]);
|
|
6281
|
+
} else {
|
|
6282
|
+
rows = await this.client.many("SELECT * FROM loop_runs ORDER BY created_at DESC LIMIT $1 OFFSET $2", [limit, offset]);
|
|
6283
|
+
}
|
|
6284
|
+
return rows.map(rowToRun);
|
|
6285
|
+
}
|
|
6286
|
+
async writeRunReceipt(...args) {
|
|
6287
|
+
const [input, opts = {}] = args;
|
|
6288
|
+
const inputRunId = typeof input.run_id === "string" && input.run_id.trim() ? input.run_id : undefined;
|
|
6289
|
+
const existing = inputRunId ? await this.getRunReceipt(inputRunId) : undefined;
|
|
6290
|
+
const run = inputRunId ? await this.getRun(inputRunId) : undefined;
|
|
6291
|
+
const loop = input.loop_id ? await this.getLoop(input.loop_id) : run ? await this.getLoop(run.loopId) : undefined;
|
|
6292
|
+
const receipt = normalizeRunReceipt(input, { now: opts.now, run, loop, existing });
|
|
6293
|
+
await this.client.execute(`INSERT INTO run_receipts (run_id, loop_id, machine_json, repo, task_ids_json, knowledge_ids_json, digest_id,
|
|
6294
|
+
started_at, finished_at, status, exit_code, summary_json, evidence_paths_json, created_at, updated_at)
|
|
6295
|
+
VALUES ($1,$2,$3::jsonb,$4,$5::jsonb,$6::jsonb,$7,$8,$9,$10,$11,$12::jsonb,$13::jsonb,$14,$15)
|
|
6296
|
+
ON CONFLICT(run_id) DO UPDATE SET
|
|
6297
|
+
loop_id=EXCLUDED.loop_id,
|
|
6298
|
+
machine_json=EXCLUDED.machine_json,
|
|
6299
|
+
repo=EXCLUDED.repo,
|
|
6300
|
+
task_ids_json=EXCLUDED.task_ids_json,
|
|
6301
|
+
knowledge_ids_json=EXCLUDED.knowledge_ids_json,
|
|
6302
|
+
digest_id=EXCLUDED.digest_id,
|
|
6303
|
+
started_at=EXCLUDED.started_at,
|
|
6304
|
+
finished_at=EXCLUDED.finished_at,
|
|
6305
|
+
status=EXCLUDED.status,
|
|
6306
|
+
exit_code=EXCLUDED.exit_code,
|
|
6307
|
+
summary_json=EXCLUDED.summary_json,
|
|
6308
|
+
evidence_paths_json=EXCLUDED.evidence_paths_json,
|
|
6309
|
+
updated_at=EXCLUDED.updated_at`, [
|
|
6310
|
+
receipt.run_id,
|
|
6311
|
+
receipt.loop_id,
|
|
6312
|
+
JSON.stringify(receipt.machine),
|
|
6313
|
+
receipt.repo,
|
|
6314
|
+
JSON.stringify(receipt.task_ids),
|
|
6315
|
+
JSON.stringify(receipt.knowledge_ids),
|
|
6316
|
+
receipt.digest_id,
|
|
6317
|
+
receipt.started_at,
|
|
6318
|
+
receipt.finished_at,
|
|
6319
|
+
receipt.status,
|
|
6320
|
+
receipt.exit_code,
|
|
6321
|
+
JSON.stringify(receipt.summary),
|
|
6322
|
+
JSON.stringify(receipt.evidence_paths),
|
|
6323
|
+
receipt.created_at,
|
|
6324
|
+
receipt.updated_at
|
|
6325
|
+
]);
|
|
6326
|
+
return await this.getRunReceipt(receipt.run_id) ?? receipt;
|
|
6327
|
+
}
|
|
6328
|
+
async getRunReceipt(...args) {
|
|
6329
|
+
const row = await this.client.get("SELECT * FROM run_receipts WHERE run_id = $1", [args[0]]);
|
|
6330
|
+
return row ? rowToRunReceipt(row) : undefined;
|
|
6331
|
+
}
|
|
6332
|
+
async listRunReceipts(...args) {
|
|
6333
|
+
const opts = args[0] ?? {};
|
|
6334
|
+
const limit = opts.limit ?? 100;
|
|
6335
|
+
const filters = [];
|
|
6336
|
+
const params = [];
|
|
6337
|
+
const next = () => `$${params.length + 1}`;
|
|
6338
|
+
if (opts.loopId) {
|
|
6339
|
+
const slot = next();
|
|
6340
|
+
filters.push(`loop_id = ${slot}`);
|
|
6341
|
+
params.push(opts.loopId);
|
|
6342
|
+
}
|
|
6343
|
+
if (opts.repo) {
|
|
6344
|
+
const slot = next();
|
|
6345
|
+
filters.push(`repo = ${slot}`);
|
|
6346
|
+
params.push(opts.repo);
|
|
6347
|
+
}
|
|
6348
|
+
if (opts.status) {
|
|
6349
|
+
const slot = next();
|
|
6350
|
+
filters.push(`status = ${slot}`);
|
|
6351
|
+
params.push(opts.status);
|
|
6352
|
+
}
|
|
6353
|
+
if (opts.taskId) {
|
|
6354
|
+
const slot = next();
|
|
6355
|
+
filters.push(`task_ids_json ? ${slot}`);
|
|
6356
|
+
params.push(opts.taskId);
|
|
6357
|
+
}
|
|
6358
|
+
if (opts.knowledgeId) {
|
|
6359
|
+
const slot = next();
|
|
6360
|
+
filters.push(`knowledge_ids_json ? ${slot}`);
|
|
6361
|
+
params.push(opts.knowledgeId);
|
|
6362
|
+
}
|
|
6363
|
+
const limitSlot = next();
|
|
6364
|
+
params.push(limit);
|
|
6365
|
+
const where = filters.length ? `WHERE ${filters.join(" AND ")}` : "";
|
|
6366
|
+
const rows = await this.client.many(`SELECT * FROM run_receipts ${where} ORDER BY created_at DESC LIMIT ${limitSlot}`, params);
|
|
6367
|
+
return rows.map(rowToRunReceipt);
|
|
6368
|
+
}
|
|
6369
|
+
async countRuns(...args) {
|
|
6370
|
+
const status = args[0];
|
|
6371
|
+
const row = status ? await this.client.get("SELECT COUNT(*)::int AS count FROM loop_runs WHERE status = $1", [status]) : await this.client.get("SELECT COUNT(*)::int AS count FROM loop_runs", []);
|
|
6372
|
+
return row?.count ?? 0;
|
|
6373
|
+
}
|
|
6374
|
+
async recoverExpiredRunLeases(...args) {
|
|
6375
|
+
const detailed = await this.recoverExpiredRunLeasesDetailed(...args);
|
|
6376
|
+
return detailed.abandoned;
|
|
6377
|
+
}
|
|
6378
|
+
async recoverExpiredRunLeasesDetailed(...args) {
|
|
6379
|
+
const [now = new Date, opts = {}] = args;
|
|
6380
|
+
const limit = Math.max(1, Math.min(1000, Math.floor(opts.limit ?? DEFAULT_RECOVERY_BATCH_LIMIT2)));
|
|
6381
|
+
const scanLimit = Math.max(limit, Math.min(5000, Math.floor(opts.scanLimit ?? limit * DEFAULT_RECOVERY_SCAN_MULTIPLIER2)));
|
|
6382
|
+
const finished = now.toISOString();
|
|
6383
|
+
const rows = await this.client.many(`SELECT * FROM loop_runs WHERE status='running' AND lease_expires_at <= $1 ORDER BY lease_expires_at ASC LIMIT $2`, [finished, scanLimit]);
|
|
6384
|
+
const recovered = [];
|
|
6385
|
+
for (const row of rows) {
|
|
6386
|
+
if (recovered.length >= limit)
|
|
6387
|
+
break;
|
|
6388
|
+
const run = await this.client.transaction(async (c) => {
|
|
6389
|
+
const res = await c.query(`UPDATE loop_runs SET status='abandoned', finished_at=$2, lease_expires_at=NULL,
|
|
6390
|
+
error='run lease expired before completion', updated_at=$2
|
|
6391
|
+
WHERE id=$1 AND status='running' AND lease_expires_at <= $3
|
|
6392
|
+
AND ($4::text IS NULL OR EXISTS (SELECT 1 FROM daemon_lease WHERE id=$4 AND expires_at > $3))`, [row.id, finished, finished, opts.daemonLeaseId ?? null]);
|
|
6393
|
+
if (res.rowCount !== 1)
|
|
6394
|
+
return;
|
|
6395
|
+
const workflowRows = await c.many("SELECT * FROM workflow_runs WHERE loop_run_id = $1 AND status NOT IN ('succeeded','failed','timed_out','cancelled')", [row.id]);
|
|
6396
|
+
for (const wf of workflowRows) {
|
|
6397
|
+
const wfRes = await c.query(`UPDATE workflow_runs SET status='failed', finished_at=$2,
|
|
6398
|
+
error='parent loop run lease expired before completion', updated_at=$2
|
|
6399
|
+
WHERE id=$1 AND status NOT IN ('succeeded','failed','timed_out','cancelled')`, [wf.id, finished]);
|
|
6400
|
+
if (wfRes.rowCount !== 1)
|
|
6401
|
+
continue;
|
|
6402
|
+
await c.execute(`UPDATE workflow_step_runs SET status='skipped', finished_at=$2, pid=NULL,
|
|
6403
|
+
error='parent loop run lease expired before completion', updated_at=$2
|
|
6404
|
+
WHERE workflow_run_id=$1 AND status IN ('pending','running')`, [wf.id, finished]);
|
|
6405
|
+
await this.setWorkItemsForWorkflowRun(c, wf.id, "failed", "parent loop run lease expired before completion", finished);
|
|
6406
|
+
}
|
|
6407
|
+
const loop = await this.loadLoop(c, row.loop_id);
|
|
6408
|
+
const itemStatus = workItemStatusForLoopRun("abandoned", row.attempt, loop?.maxAttempts);
|
|
6409
|
+
if (itemStatus) {
|
|
6410
|
+
const statuses = itemStatus === "admitted" ? ["admitted", "running", "failed"] : ["admitted", "running"];
|
|
6411
|
+
const reason = itemStatus === "admitted" ? "run lease expired before completion; retry pending" : "run lease expired before completion";
|
|
6412
|
+
await this.setWorkItemsForLoop(c, row.loop_id, itemStatus, reason, finished, statuses);
|
|
6413
|
+
}
|
|
6414
|
+
return this.loadRun(c, row.id);
|
|
6415
|
+
});
|
|
6416
|
+
if (run)
|
|
6417
|
+
recovered.push(run);
|
|
6418
|
+
}
|
|
6419
|
+
return { abandoned: recovered, deferred: [] };
|
|
6420
|
+
}
|
|
6421
|
+
async pruneHistory(...args) {
|
|
6422
|
+
const opts = args[0];
|
|
6423
|
+
const { maxAgeDays, keepPerLoop } = opts;
|
|
6424
|
+
if (maxAgeDays === undefined && keepPerLoop === undefined) {
|
|
6425
|
+
throw new ValidationError("pruneHistory requires maxAgeDays and/or keepPerLoop");
|
|
6426
|
+
}
|
|
6427
|
+
if (maxAgeDays !== undefined && (!Number.isFinite(maxAgeDays) || maxAgeDays < 0)) {
|
|
6428
|
+
throw new ValidationError(`pruneHistory maxAgeDays must be a non-negative number: ${maxAgeDays}`);
|
|
6429
|
+
}
|
|
6430
|
+
if (keepPerLoop !== undefined && (!Number.isInteger(keepPerLoop) || keepPerLoop < 0)) {
|
|
6431
|
+
throw new ValidationError(`pruneHistory keepPerLoop must be a non-negative integer: ${keepPerLoop}`);
|
|
6432
|
+
}
|
|
6433
|
+
const now = opts.now ?? new Date;
|
|
6434
|
+
const dryRun = opts.dryRun ?? false;
|
|
6435
|
+
const cutoff = maxAgeDays === undefined ? undefined : new Date(now.getTime() - maxAgeDays * 86400000).toISOString();
|
|
6436
|
+
const terminal = TERMINAL_RUN_STATUSES2.map((s) => `'${s}'`).join(",");
|
|
6437
|
+
const candidateIds = (await this.client.many(`WITH ranked AS (
|
|
6438
|
+
SELECT id, status, created_at,
|
|
6439
|
+
ROW_NUMBER() OVER (PARTITION BY loop_id ORDER BY created_at DESC, id DESC) AS recency
|
|
6440
|
+
FROM loop_runs
|
|
6441
|
+
)
|
|
6442
|
+
SELECT id FROM ranked
|
|
6443
|
+
WHERE status IN (${terminal})
|
|
6444
|
+
AND ($1::timestamptz IS NULL OR created_at < $1::timestamptz)
|
|
6445
|
+
AND ($2::int IS NULL OR recency > $2)`, [cutoff ?? null, keepPerLoop ?? null])).map((r) => r.id);
|
|
6446
|
+
const summary = {
|
|
6447
|
+
dryRun,
|
|
6448
|
+
cutoff,
|
|
6449
|
+
keepPerLoop,
|
|
6450
|
+
loopRuns: dryRun ? candidateIds.length : 0,
|
|
6451
|
+
workflowRuns: 0,
|
|
6452
|
+
goalRuns: 0
|
|
6453
|
+
};
|
|
6454
|
+
for (let offset = 0;offset < candidateIds.length; offset += PRUNE_BATCH_SIZE2) {
|
|
6455
|
+
const batch = candidateIds.slice(offset, offset + PRUNE_BATCH_SIZE2);
|
|
6456
|
+
if (dryRun) {
|
|
6457
|
+
const workflowRunIds = (await this.client.many(`SELECT id FROM workflow_runs WHERE loop_run_id = ANY($1)`, [batch])).map((r) => r.id);
|
|
6458
|
+
summary.workflowRuns += workflowRunIds.length;
|
|
6459
|
+
const gr = await this.client.get(`SELECT COUNT(*)::int AS count FROM goal_runs WHERE loop_run_id = ANY($1) OR workflow_run_id = ANY($2)`, [batch, workflowRunIds]);
|
|
6460
|
+
summary.goalRuns += gr?.count ?? 0;
|
|
6461
|
+
continue;
|
|
6462
|
+
}
|
|
6463
|
+
await this.client.transaction(async (c) => {
|
|
6464
|
+
const confirmed = (await c.many(`SELECT id FROM loop_runs WHERE id = ANY($1) AND status IN (${terminal})`, [batch])).map((r) => r.id);
|
|
6465
|
+
if (confirmed.length === 0)
|
|
6466
|
+
return;
|
|
6467
|
+
const workflowRunIds = (await c.many(`SELECT id FROM workflow_runs WHERE loop_run_id = ANY($1)`, [confirmed])).map((r) => r.id);
|
|
6468
|
+
summary.loopRuns += confirmed.length;
|
|
6469
|
+
summary.workflowRuns += workflowRunIds.length;
|
|
6470
|
+
const gr = await c.query(`DELETE FROM goal_runs WHERE loop_run_id = ANY($1) OR workflow_run_id = ANY($2)`, [confirmed, workflowRunIds]);
|
|
6471
|
+
summary.goalRuns += gr.rowCount;
|
|
6472
|
+
if (workflowRunIds.length > 0) {
|
|
6473
|
+
await c.execute(`DELETE FROM workflow_runs WHERE id = ANY($1)`, [workflowRunIds]);
|
|
6474
|
+
}
|
|
6475
|
+
await c.execute(`DELETE FROM loop_runs WHERE id = ANY($1) AND status IN (${terminal})`, [confirmed]);
|
|
6476
|
+
});
|
|
6477
|
+
}
|
|
6478
|
+
return summary;
|
|
6479
|
+
}
|
|
6480
|
+
async acquireDaemonLease(...args) {
|
|
6481
|
+
const input = args[0];
|
|
6482
|
+
const now = input.now ?? new Date;
|
|
6483
|
+
const expiresAt = new Date(now.getTime() + input.ttlMs).toISOString();
|
|
6484
|
+
return this.client.transaction(async (c) => {
|
|
6485
|
+
const existing = await c.get("SELECT * FROM daemon_lease LIMIT 1 FOR UPDATE", []);
|
|
6486
|
+
if (existing && existing.expires_at > now.toISOString() && existing.id !== input.id) {
|
|
6487
|
+
return;
|
|
6488
|
+
}
|
|
6489
|
+
await c.execute("DELETE FROM daemon_lease", []);
|
|
6490
|
+
await c.execute(`INSERT INTO daemon_lease (id, pid, hostname, heartbeat_at, expires_at, created_at, updated_at)
|
|
6491
|
+
VALUES ($1,$2,$3,$4,$5,$4,$4)`, [input.id, input.pid, input.hostname, now.toISOString(), expiresAt]);
|
|
6492
|
+
return this.loadDaemonLease(c);
|
|
6493
|
+
});
|
|
6494
|
+
}
|
|
6495
|
+
async heartbeatDaemonLease(...args) {
|
|
6496
|
+
const [id, ttlMs, now = new Date] = args;
|
|
6497
|
+
const expiresAt = new Date(now.getTime() + ttlMs).toISOString();
|
|
6498
|
+
const res = await this.client.query(`UPDATE daemon_lease SET heartbeat_at=$2, expires_at=$3, updated_at=$2 WHERE id=$1 AND expires_at > $4`, [id, now.toISOString(), expiresAt, now.toISOString()]);
|
|
6499
|
+
if (res.rowCount !== 1)
|
|
6500
|
+
return;
|
|
6501
|
+
return this.getDaemonLease();
|
|
6502
|
+
}
|
|
6503
|
+
async releaseDaemonLease(...args) {
|
|
6504
|
+
await this.client.execute("DELETE FROM daemon_lease WHERE id = $1", [args[0]]);
|
|
6505
|
+
}
|
|
6506
|
+
async getDaemonLease(...args) {
|
|
6507
|
+
return this.loadDaemonLease(this.client);
|
|
6508
|
+
}
|
|
6509
|
+
async loadWorkflow(c, id) {
|
|
6510
|
+
const row = await c.get("SELECT * FROM workflow_specs WHERE id = $1", [id]);
|
|
6511
|
+
return row ? rowToWorkflow(row) : undefined;
|
|
6512
|
+
}
|
|
6513
|
+
async getWorkflow(...args) {
|
|
6514
|
+
return this.loadWorkflow(this.client, args[0]);
|
|
6515
|
+
}
|
|
6516
|
+
async listWorkflows(...args) {
|
|
6517
|
+
const opts = args[0] ?? {};
|
|
6518
|
+
const limit = opts.limit ?? 200;
|
|
6519
|
+
const offset = opts.offset ?? 0;
|
|
6520
|
+
const rows = opts.status ? await this.client.many("SELECT * FROM workflow_specs WHERE status = $1 ORDER BY status ASC, name ASC LIMIT $2 OFFSET $3", [opts.status, limit, offset]) : await this.client.many("SELECT * FROM workflow_specs ORDER BY status ASC, name ASC LIMIT $1 OFFSET $2", [limit, offset]);
|
|
6521
|
+
return rows.map(rowToWorkflow);
|
|
6522
|
+
}
|
|
6523
|
+
async countWorkflows(...args) {
|
|
6524
|
+
const opts = args[0] ?? {};
|
|
6525
|
+
const row = opts.status ? await this.client.get("SELECT COUNT(*)::int AS count FROM workflow_specs WHERE status = $1", [opts.status]) : await this.client.get("SELECT COUNT(*)::int AS count FROM workflow_specs", []);
|
|
6526
|
+
return row?.count ?? 0;
|
|
6527
|
+
}
|
|
6528
|
+
async getWorkflowInvocation(...args) {
|
|
6529
|
+
const row = await this.client.get("SELECT * FROM workflow_invocations WHERE id = $1", [args[0]]);
|
|
6530
|
+
return row ? rowToWorkflowInvocation(row) : undefined;
|
|
6531
|
+
}
|
|
6532
|
+
async listWorkflowInvocations(...args) {
|
|
6533
|
+
const opts = args[0] ?? {};
|
|
6534
|
+
const rows = await this.client.many("SELECT * FROM workflow_invocations ORDER BY created_at DESC LIMIT $1", [opts.limit ?? 100]);
|
|
6535
|
+
return rows.map(rowToWorkflowInvocation);
|
|
6536
|
+
}
|
|
6537
|
+
async getWorkflowWorkItem(...args) {
|
|
6538
|
+
const row = await this.client.get("SELECT * FROM workflow_work_items WHERE id = $1", [args[0]]);
|
|
6539
|
+
return row ? rowToWorkflowWorkItem(row) : undefined;
|
|
6540
|
+
}
|
|
6541
|
+
async listWorkflowWorkItems(...args) {
|
|
6542
|
+
const opts = args[0] ?? {};
|
|
6543
|
+
const limit = opts.limit ?? 100;
|
|
6544
|
+
let rows;
|
|
6545
|
+
if (opts.status && opts.routeKey) {
|
|
6546
|
+
rows = await this.client.many("SELECT * FROM workflow_work_items WHERE status = $1 AND route_key = $2 ORDER BY priority DESC, created_at ASC LIMIT $3", [opts.status, opts.routeKey, limit]);
|
|
6547
|
+
} else if (opts.status) {
|
|
6548
|
+
rows = await this.client.many("SELECT * FROM workflow_work_items WHERE status = $1 ORDER BY priority DESC, created_at ASC LIMIT $2", [opts.status, limit]);
|
|
6549
|
+
} else if (opts.routeKey) {
|
|
6550
|
+
rows = await this.client.many("SELECT * FROM workflow_work_items WHERE route_key = $1 ORDER BY priority DESC, created_at ASC LIMIT $2", [opts.routeKey, limit]);
|
|
6551
|
+
} else {
|
|
6552
|
+
rows = await this.client.many("SELECT * FROM workflow_work_items ORDER BY priority DESC, created_at ASC LIMIT $1", [limit]);
|
|
6553
|
+
}
|
|
6554
|
+
return rows.map(rowToWorkflowWorkItem);
|
|
6555
|
+
}
|
|
6556
|
+
async countActiveWorkflowWorkItems(...args) {
|
|
6557
|
+
const a = args[0] ?? {};
|
|
6558
|
+
const active = "('admitted','running')";
|
|
6559
|
+
const scoped = async (col, val) => {
|
|
6560
|
+
const row = await this.client.get(`SELECT COUNT(*)::int AS count FROM workflow_work_items WHERE status IN ${active} AND ${col} = $1`, [val]);
|
|
6561
|
+
return row?.count ?? 0;
|
|
6562
|
+
};
|
|
6563
|
+
const routeScope = a.routeScope?.trim() || undefined;
|
|
6564
|
+
const global = routeScope ? await scoped("route_scope", routeScope) : (await this.client.get(`SELECT COUNT(*)::int AS count FROM workflow_work_items WHERE status IN ${active}`, []))?.count ?? 0;
|
|
6565
|
+
const project = a.projectKey ? await scoped("project_key", a.projectKey) : 0;
|
|
6566
|
+
const projectGroup = a.projectGroup ? await scoped("project_group", a.projectGroup) : undefined;
|
|
6567
|
+
return {
|
|
6568
|
+
global,
|
|
6569
|
+
project,
|
|
6570
|
+
...projectGroup !== undefined ? { projectGroup } : {}
|
|
6571
|
+
};
|
|
6572
|
+
}
|
|
6573
|
+
async getWorkflowRun(...args) {
|
|
6574
|
+
const row = await this.client.get("SELECT * FROM workflow_runs WHERE id = $1", [args[0]]);
|
|
6575
|
+
return row ? rowToWorkflowRun(row) : undefined;
|
|
6576
|
+
}
|
|
6577
|
+
async listWorkflowRuns(...args) {
|
|
6578
|
+
const opts = args[0] ?? {};
|
|
6579
|
+
const limit = opts.limit ?? 100;
|
|
6580
|
+
let rows;
|
|
6581
|
+
if (opts.workflowId && opts.loopRunId) {
|
|
6582
|
+
rows = await this.client.many("SELECT * FROM workflow_runs WHERE workflow_id = $1 AND loop_run_id = $2 ORDER BY created_at DESC LIMIT $3", [opts.workflowId, opts.loopRunId, limit]);
|
|
6583
|
+
} else if (opts.workflowId) {
|
|
6584
|
+
rows = await this.client.many("SELECT * FROM workflow_runs WHERE workflow_id = $1 ORDER BY created_at DESC LIMIT $2", [opts.workflowId, limit]);
|
|
6585
|
+
} else if (opts.loopRunId) {
|
|
6586
|
+
rows = await this.client.many("SELECT * FROM workflow_runs WHERE loop_run_id = $1 ORDER BY created_at DESC LIMIT $2", [opts.loopRunId, limit]);
|
|
6587
|
+
} else {
|
|
6588
|
+
rows = await this.client.many("SELECT * FROM workflow_runs ORDER BY created_at DESC LIMIT $1", [limit]);
|
|
6589
|
+
}
|
|
6590
|
+
return rows.map(rowToWorkflowRun);
|
|
6591
|
+
}
|
|
6592
|
+
async listWorkflowStepRuns(...args) {
|
|
6593
|
+
const rows = await this.client.many("SELECT * FROM workflow_step_runs WHERE workflow_run_id = $1 ORDER BY sequence ASC", [args[0]]);
|
|
6594
|
+
return rows.map(rowToWorkflowStepRun);
|
|
6595
|
+
}
|
|
6596
|
+
async getWorkflowStepRun(...args) {
|
|
6597
|
+
const row = await this.client.get("SELECT * FROM workflow_step_runs WHERE workflow_run_id = $1 AND step_id = $2", [args[0], args[1]]);
|
|
6598
|
+
return row ? rowToWorkflowStepRun(row) : undefined;
|
|
6599
|
+
}
|
|
6600
|
+
async listWorkflowEvents(...args) {
|
|
6601
|
+
const [workflowRunId, limit = 200] = args;
|
|
6602
|
+
const rows = await this.client.many("SELECT * FROM workflow_events WHERE workflow_run_id = $1 ORDER BY sequence ASC LIMIT $2", [workflowRunId, limit]);
|
|
6603
|
+
return rows.map(rowToWorkflowEvent);
|
|
6604
|
+
}
|
|
6605
|
+
async getGoal(...args) {
|
|
6606
|
+
const row = await this.client.get("SELECT * FROM goals WHERE id = $1", [args[0]]);
|
|
6607
|
+
return row ? rowToGoal(row) : undefined;
|
|
6608
|
+
}
|
|
6609
|
+
async listGoals(...args) {
|
|
6610
|
+
const opts = args[0] ?? {};
|
|
6611
|
+
const limit = opts.limit ?? 100;
|
|
6612
|
+
const rows = opts.status ? await this.client.many("SELECT * FROM goals WHERE status = $1 ORDER BY updated_at DESC LIMIT $2", [opts.status, limit]) : await this.client.many("SELECT * FROM goals ORDER BY updated_at DESC LIMIT $1", [limit]);
|
|
6613
|
+
return rows.map(rowToGoal);
|
|
6614
|
+
}
|
|
6615
|
+
async listGoalPlanNodes(...args) {
|
|
6616
|
+
const idOrPlan = args[0];
|
|
6617
|
+
const rows = await this.client.many("SELECT * FROM goal_plan_nodes WHERE goal_id = $1 OR plan_id = $1 ORDER BY sequence ASC", [idOrPlan]);
|
|
6618
|
+
return rows.map((r) => rowToGoalPlanNode({ ...r, ready: r.ready ? 1 : 0 }));
|
|
6619
|
+
}
|
|
6620
|
+
async listGoalRuns(...args) {
|
|
6621
|
+
const opts = args[0] ?? {};
|
|
6622
|
+
const limit = opts.limit ?? 100;
|
|
6623
|
+
let rows;
|
|
6624
|
+
if (opts.runId) {
|
|
6625
|
+
rows = await this.client.many("SELECT * FROM goal_runs WHERE id = $1", [opts.runId]);
|
|
6626
|
+
} else if (opts.goalId) {
|
|
6627
|
+
rows = await this.client.many("SELECT * FROM goal_runs WHERE goal_id = $1 ORDER BY created_at ASC LIMIT $2", [opts.goalId, limit]);
|
|
6628
|
+
} else {
|
|
6629
|
+
rows = await this.client.many("SELECT * FROM goal_runs ORDER BY created_at DESC LIMIT $1", [limit]);
|
|
6630
|
+
}
|
|
6631
|
+
return rows.map(rowToGoalRun);
|
|
6632
|
+
}
|
|
6633
|
+
createWorkflow() {
|
|
6634
|
+
throw new NotImplementedError("createWorkflow");
|
|
6635
|
+
}
|
|
6636
|
+
archiveWorkflow() {
|
|
6637
|
+
throw new NotImplementedError("archiveWorkflow");
|
|
6638
|
+
}
|
|
6639
|
+
createWorkflowInvocation() {
|
|
6640
|
+
throw new NotImplementedError("createWorkflowInvocation");
|
|
6641
|
+
}
|
|
6642
|
+
upsertWorkflowWorkItem() {
|
|
6643
|
+
throw new NotImplementedError("upsertWorkflowWorkItem");
|
|
6644
|
+
}
|
|
6645
|
+
admitWorkflowWorkItem() {
|
|
6646
|
+
throw new NotImplementedError("admitWorkflowWorkItem");
|
|
6647
|
+
}
|
|
6648
|
+
createGoal() {
|
|
6649
|
+
throw new NotImplementedError("createGoal");
|
|
6650
|
+
}
|
|
6651
|
+
createGoalPlanNodes() {
|
|
6652
|
+
throw new NotImplementedError("createGoalPlanNodes");
|
|
6653
|
+
}
|
|
6654
|
+
updateGoalStatus() {
|
|
6655
|
+
throw new NotImplementedError("updateGoalStatus");
|
|
6656
|
+
}
|
|
6657
|
+
updateGoalPlanNode() {
|
|
6658
|
+
throw new NotImplementedError("updateGoalPlanNode");
|
|
6659
|
+
}
|
|
6660
|
+
recordGoalEvent() {
|
|
6661
|
+
throw new NotImplementedError("recordGoalEvent");
|
|
6662
|
+
}
|
|
6663
|
+
createWorkflowRun() {
|
|
6664
|
+
throw new NotImplementedError("createWorkflowRun");
|
|
6665
|
+
}
|
|
6666
|
+
startWorkflowStepRun() {
|
|
6667
|
+
throw new NotImplementedError("startWorkflowStepRun");
|
|
6668
|
+
}
|
|
6669
|
+
recoverWorkflowRun() {
|
|
6670
|
+
throw new NotImplementedError("recoverWorkflowRun");
|
|
6671
|
+
}
|
|
6672
|
+
finalizeWorkflowStepRun() {
|
|
6673
|
+
throw new NotImplementedError("finalizeWorkflowStepRun");
|
|
6674
|
+
}
|
|
6675
|
+
finalizeWorkflowRun() {
|
|
6676
|
+
throw new NotImplementedError("finalizeWorkflowRun");
|
|
6677
|
+
}
|
|
6678
|
+
appendWorkflowEvent() {
|
|
6679
|
+
throw new NotImplementedError("appendWorkflowEvent");
|
|
6680
|
+
}
|
|
6681
|
+
}
|
|
6682
|
+
function createPostgresLoopStorage(client) {
|
|
6683
|
+
return new PostgresLoopStorage(client);
|
|
6684
|
+
}
|
|
4891
6685
|
export {
|
|
4892
6686
|
createSqliteLoopStorage,
|
|
4893
6687
|
createPostgresStorage,
|
|
6688
|
+
createPostgresLoopStorage,
|
|
4894
6689
|
checksumStorageSql,
|
|
4895
6690
|
Store,
|
|
4896
6691
|
SqliteLoopStorage,
|
|
4897
6692
|
PostgresStorage,
|
|
6693
|
+
PostgresLoopStorage,
|
|
4898
6694
|
POSTGRES_STORAGE_MIGRATIONS,
|
|
4899
|
-
POSTGRES_MIGRATION_LEDGER_TABLE
|
|
6695
|
+
POSTGRES_MIGRATION_LEDGER_TABLE,
|
|
6696
|
+
NotImplementedError
|
|
4900
6697
|
};
|