@hasna/loops 0.4.14 → 0.4.23
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 +25 -0
- package/README.md +119 -29
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +825 -7
- package/dist/cli/index.js +5423 -2619
- package/dist/daemon/index.js +478 -84
- package/dist/generated/storage-kit/index.d.ts +1 -1
- package/dist/index.js +2521 -226
- 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/migration.d.ts +40 -0
- package/dist/lib/mode.js +3 -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/storage/contract.d.ts +7 -1
- package/dist/lib/storage/index.js +710 -31
- package/dist/lib/storage/postgres-loop-storage.d.ts +6 -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 +412 -18
- package/dist/lib/store/index.d.ts +268 -0
- package/dist/lib/store.d.ts +48 -1
- package/dist/lib/store.js +396 -18
- 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 +1658 -168
- package/dist/runner/index.js +76 -9
- package/dist/sdk/http.d.ts +67 -0
- package/dist/sdk/http.js +21 -0
- package/dist/sdk/index.d.ts +50 -17
- package/dist/sdk/index.js +1509 -132
- package/dist/serve/index.js +1598 -122
- package/dist/types.d.ts +49 -0
- package/docs/AUTOMATION_RUNTIME_DESIGN.md +442 -1
- package/docs/CUTOVER-RUNBOOK.md +73 -56
- package/docs/DEPLOYMENT_MODES.md +35 -18
- package/docs/RUNTIME_BOUNDARY.md +203 -0
- package/docs/USAGE.md +145 -27
- package/package.json +3 -3
package/dist/serve/index.js
CHANGED
|
@@ -1204,6 +1204,175 @@ function discardWorkflowRunManifest(staged) {
|
|
|
1204
1204
|
} catch {}
|
|
1205
1205
|
}
|
|
1206
1206
|
|
|
1207
|
+
// src/lib/run-receipts.ts
|
|
1208
|
+
import { createHash as createHash2 } from "crypto";
|
|
1209
|
+
import { hostname } from "os";
|
|
1210
|
+
|
|
1211
|
+
// src/lib/run-envelope.ts
|
|
1212
|
+
var ENVELOPE_EXCERPT_CHARS = 2048;
|
|
1213
|
+
var BLOCKED_STEP_ERROR_PREFIX = "blocked:";
|
|
1214
|
+
function boundedExcerpt(text, limit = ENVELOPE_EXCERPT_CHARS) {
|
|
1215
|
+
if (!text)
|
|
1216
|
+
return;
|
|
1217
|
+
const scrubbed = scrubSecrets(text);
|
|
1218
|
+
if (scrubbed.length <= limit * 2)
|
|
1219
|
+
return scrubbed;
|
|
1220
|
+
const omitted = scrubbed.length - limit * 2;
|
|
1221
|
+
return `${scrubbed.slice(0, limit)}
|
|
1222
|
+
[... ${omitted} chars omitted ...]
|
|
1223
|
+
${scrubbed.slice(-limit)}`;
|
|
1224
|
+
}
|
|
1225
|
+
function summarizeOutput(stdout, stderr) {
|
|
1226
|
+
return {
|
|
1227
|
+
stdoutBytes: stdout ? Buffer.byteLength(stdout, "utf8") : 0,
|
|
1228
|
+
stderrBytes: stderr ? Buffer.byteLength(stderr, "utf8") : 0,
|
|
1229
|
+
stdoutExcerpt: boundedExcerpt(stdout),
|
|
1230
|
+
stderrExcerpt: boundedExcerpt(stderr)
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
function summarizeExecutorResult(result) {
|
|
1234
|
+
return {
|
|
1235
|
+
...summarizeOutput(result.stdout, result.stderr),
|
|
1236
|
+
status: result.status,
|
|
1237
|
+
exitCode: result.exitCode,
|
|
1238
|
+
durationMs: result.durationMs,
|
|
1239
|
+
error: boundedExcerpt(result.error)
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
function isBlockedStepRun(step) {
|
|
1243
|
+
return step?.status === "skipped" && (step.error?.startsWith(BLOCKED_STEP_ERROR_PREFIX) ?? false);
|
|
1244
|
+
}
|
|
1245
|
+
function summarizeWorkflowStepRun(step) {
|
|
1246
|
+
return {
|
|
1247
|
+
...summarizeOutput(step.stdout, step.stderr),
|
|
1248
|
+
stepId: step.stepId,
|
|
1249
|
+
status: step.status,
|
|
1250
|
+
exitCode: step.exitCode,
|
|
1251
|
+
durationMs: step.durationMs,
|
|
1252
|
+
error: boundedExcerpt(step.error),
|
|
1253
|
+
blocked: isBlockedStepRun(step)
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
function workflowRunEnvelope(run, steps) {
|
|
1257
|
+
return JSON.stringify({ workflowRun: run, steps: steps.map(summarizeWorkflowStepRun) }, null, 2);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// src/lib/run-receipts.ts
|
|
1261
|
+
var RUN_RECEIPT_SUMMARY_TEXT_CHARS = 4096;
|
|
1262
|
+
var RUN_RECEIPT_MAX_IDS = 100;
|
|
1263
|
+
var RUN_RECEIPT_MAX_EVIDENCE_PATHS = 100;
|
|
1264
|
+
var RUN_RECEIPT_MAX_PATH_CHARS = 1024;
|
|
1265
|
+
function canonicalJson(value) {
|
|
1266
|
+
if (Array.isArray(value))
|
|
1267
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
1268
|
+
if (value && typeof value === "object") {
|
|
1269
|
+
return `{${Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
|
|
1270
|
+
}
|
|
1271
|
+
return JSON.stringify(value);
|
|
1272
|
+
}
|
|
1273
|
+
function digestReceipt(receipt) {
|
|
1274
|
+
return `sha256:${createHash2("sha256").update(canonicalJson(receipt)).digest("hex")}`;
|
|
1275
|
+
}
|
|
1276
|
+
function isoOrNull(value, label) {
|
|
1277
|
+
if (value === undefined || value === null || value === "")
|
|
1278
|
+
return null;
|
|
1279
|
+
const date = new Date(value);
|
|
1280
|
+
if (Number.isNaN(date.getTime()))
|
|
1281
|
+
throw new Error(`${label} must be a valid ISO date/time`);
|
|
1282
|
+
return date.toISOString();
|
|
1283
|
+
}
|
|
1284
|
+
function nonEmpty(value, label) {
|
|
1285
|
+
const trimmed = value?.trim();
|
|
1286
|
+
if (!trimmed)
|
|
1287
|
+
throw new Error(`${label} must be non-empty`);
|
|
1288
|
+
return trimmed;
|
|
1289
|
+
}
|
|
1290
|
+
function normalizedStringArray(values, opts) {
|
|
1291
|
+
const out = [];
|
|
1292
|
+
const seen = new Set;
|
|
1293
|
+
for (const raw of values ?? []) {
|
|
1294
|
+
const value = String(raw).trim();
|
|
1295
|
+
if (!value || seen.has(value))
|
|
1296
|
+
continue;
|
|
1297
|
+
seen.add(value);
|
|
1298
|
+
out.push(opts.itemMax ? value.slice(0, opts.itemMax) : value);
|
|
1299
|
+
if (out.length >= opts.max)
|
|
1300
|
+
break;
|
|
1301
|
+
}
|
|
1302
|
+
if ((values?.length ?? 0) > opts.max) {
|
|
1303
|
+
out.push(`[truncated ${values.length - opts.max} ${opts.label}]`);
|
|
1304
|
+
}
|
|
1305
|
+
return out;
|
|
1306
|
+
}
|
|
1307
|
+
function receiptMachine(input, opts) {
|
|
1308
|
+
if (typeof input.machine === "string")
|
|
1309
|
+
return nonEmpty(input.machine, "machine");
|
|
1310
|
+
if (input.machine && typeof input.machine === "object")
|
|
1311
|
+
return input.machine;
|
|
1312
|
+
if (opts.loop?.machine)
|
|
1313
|
+
return { ...opts.loop.machine };
|
|
1314
|
+
if (typeof opts.defaultMachine === "string")
|
|
1315
|
+
return nonEmpty(opts.defaultMachine, "machine");
|
|
1316
|
+
if (opts.defaultMachine && typeof opts.defaultMachine === "object")
|
|
1317
|
+
return opts.defaultMachine;
|
|
1318
|
+
return hostname();
|
|
1319
|
+
}
|
|
1320
|
+
function normalizeSummary(input, run) {
|
|
1321
|
+
const stdout = input.stdout ?? run?.stdout;
|
|
1322
|
+
const stderr = input.stderr ?? run?.stderr;
|
|
1323
|
+
const output = summarizeOutput(stdout, stderr);
|
|
1324
|
+
const summaryInput = input.summary;
|
|
1325
|
+
const provided = typeof summaryInput === "object" && summaryInput !== null ? summaryInput : {};
|
|
1326
|
+
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;
|
|
1327
|
+
return {
|
|
1328
|
+
text,
|
|
1329
|
+
stdout_bytes: Number.isFinite(provided.stdout_bytes) ? Number(provided.stdout_bytes) : output.stdoutBytes,
|
|
1330
|
+
stderr_bytes: Number.isFinite(provided.stderr_bytes) ? Number(provided.stderr_bytes) : output.stderrBytes,
|
|
1331
|
+
stdout_excerpt: typeof provided.stdout_excerpt === "string" ? boundedExcerpt(provided.stdout_excerpt) : output.stdoutExcerpt,
|
|
1332
|
+
stderr_excerpt: typeof provided.stderr_excerpt === "string" ? boundedExcerpt(provided.stderr_excerpt) : output.stderrExcerpt,
|
|
1333
|
+
error: typeof provided.error === "string" ? boundedExcerpt(provided.error) : boundedExcerpt(input.error ?? run?.error),
|
|
1334
|
+
duration_ms: Number.isFinite(provided.duration_ms) ? Number(provided.duration_ms) : input.duration_ms ?? run?.durationMs
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
function normalizeRunReceipt(input, opts = {}) {
|
|
1338
|
+
const now = (opts.now ?? new Date).toISOString();
|
|
1339
|
+
const run = opts.run;
|
|
1340
|
+
const loopId = input.loop_id ?? run?.loopId ?? opts.loop?.id;
|
|
1341
|
+
const normalized = {
|
|
1342
|
+
loop_id: nonEmpty(loopId, "loop_id"),
|
|
1343
|
+
run_id: nonEmpty(input.run_id ?? run?.id, "run_id"),
|
|
1344
|
+
machine: receiptMachine(input, opts),
|
|
1345
|
+
repo: nonEmpty(input.repo ?? opts.defaultRepo ?? targetRepo(opts.loop) ?? process.cwd(), "repo"),
|
|
1346
|
+
task_ids: normalizedStringArray(input.task_ids, { max: RUN_RECEIPT_MAX_IDS, label: "task_ids" }),
|
|
1347
|
+
knowledge_ids: normalizedStringArray(input.knowledge_ids, { max: RUN_RECEIPT_MAX_IDS, label: "knowledge_ids" }),
|
|
1348
|
+
started_at: isoOrNull(input.started_at ?? run?.startedAt, "started_at"),
|
|
1349
|
+
finished_at: isoOrNull(input.finished_at ?? run?.finishedAt, "finished_at"),
|
|
1350
|
+
status: nonEmpty(input.status ?? run?.status, "status"),
|
|
1351
|
+
exit_code: input.exit_code === undefined ? run?.exitCode ?? null : input.exit_code,
|
|
1352
|
+
summary: normalizeSummary(input, run),
|
|
1353
|
+
evidence_paths: normalizedStringArray(input.evidence_paths, {
|
|
1354
|
+
max: RUN_RECEIPT_MAX_EVIDENCE_PATHS,
|
|
1355
|
+
label: "evidence_paths",
|
|
1356
|
+
itemMax: RUN_RECEIPT_MAX_PATH_CHARS
|
|
1357
|
+
})
|
|
1358
|
+
};
|
|
1359
|
+
return {
|
|
1360
|
+
...normalized,
|
|
1361
|
+
digest_id: input.digest_id?.trim() || digestReceipt(normalized),
|
|
1362
|
+
created_at: opts.existing?.created_at ?? now,
|
|
1363
|
+
updated_at: now
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
function targetRepo(loop) {
|
|
1367
|
+
if (!loop)
|
|
1368
|
+
return;
|
|
1369
|
+
if (loop.target.type === "command")
|
|
1370
|
+
return loop.target.cwd;
|
|
1371
|
+
if (loop.target.type === "agent")
|
|
1372
|
+
return loop.target.cwd;
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1207
1376
|
// src/lib/route/todos-cli.ts
|
|
1208
1377
|
import { closeSync, mkdtempSync, openSync, readFileSync as readFileSync3, rmSync as rmSync2 } from "fs";
|
|
1209
1378
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
@@ -1275,6 +1444,9 @@ function publicRun(run, showOutput = false, opts = {}) {
|
|
|
1275
1444
|
error: opts.redactError ? redact(run.error) : run.error
|
|
1276
1445
|
};
|
|
1277
1446
|
}
|
|
1447
|
+
function publicRunReceipt(receipt) {
|
|
1448
|
+
return { ...receipt };
|
|
1449
|
+
}
|
|
1278
1450
|
function publicExecutorResult(result, showOutput = false) {
|
|
1279
1451
|
return {
|
|
1280
1452
|
...result,
|
|
@@ -1398,7 +1570,7 @@ function todosMutationSummary(result) {
|
|
|
1398
1570
|
var DEFAULT_RECOVERY_BATCH_LIMIT = 100;
|
|
1399
1571
|
var DEFAULT_RECOVERY_SCAN_MULTIPLIER = 5;
|
|
1400
1572
|
var LIVE_EXPIRED_RUN_GRACE_MS = 60000;
|
|
1401
|
-
var SCHEMA_USER_VERSION =
|
|
1573
|
+
var SCHEMA_USER_VERSION = 8;
|
|
1402
1574
|
var TERMINAL_RUN_STATUSES = ["succeeded", "failed", "timed_out", "abandoned", "skipped"];
|
|
1403
1575
|
var PRUNE_BATCH_SIZE = 400;
|
|
1404
1576
|
var GENERATED_ROUTE_TEMPLATE_IDS = new Set(["todos-task-worker-verifier", "task-lifecycle", "event-worker-verifier"]);
|
|
@@ -1454,6 +1626,25 @@ function rowToRun(row) {
|
|
|
1454
1626
|
updatedAt: row.updated_at
|
|
1455
1627
|
};
|
|
1456
1628
|
}
|
|
1629
|
+
function rowToRunReceipt(row) {
|
|
1630
|
+
return {
|
|
1631
|
+
loop_id: row.loop_id,
|
|
1632
|
+
run_id: row.run_id,
|
|
1633
|
+
machine: JSON.parse(row.machine_json),
|
|
1634
|
+
repo: row.repo,
|
|
1635
|
+
task_ids: JSON.parse(row.task_ids_json),
|
|
1636
|
+
knowledge_ids: JSON.parse(row.knowledge_ids_json),
|
|
1637
|
+
digest_id: row.digest_id,
|
|
1638
|
+
started_at: row.started_at,
|
|
1639
|
+
finished_at: row.finished_at,
|
|
1640
|
+
status: row.status,
|
|
1641
|
+
exit_code: row.exit_code,
|
|
1642
|
+
summary: JSON.parse(row.summary_json),
|
|
1643
|
+
evidence_paths: JSON.parse(row.evidence_paths_json),
|
|
1644
|
+
created_at: row.created_at,
|
|
1645
|
+
updated_at: row.updated_at
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1457
1648
|
function latestRunTime(row) {
|
|
1458
1649
|
return row.finished_at ?? row.started_at ?? row.created_at;
|
|
1459
1650
|
}
|
|
@@ -1517,6 +1708,7 @@ function rowToWorkflowWorkItem(row) {
|
|
|
1517
1708
|
subjectRef: row.subject_ref,
|
|
1518
1709
|
projectKey: row.project_key ?? undefined,
|
|
1519
1710
|
projectGroup: row.project_group ?? undefined,
|
|
1711
|
+
machineId: row.machine_id ?? undefined,
|
|
1520
1712
|
routeScope: row.route_scope ?? undefined,
|
|
1521
1713
|
priority: row.priority,
|
|
1522
1714
|
status: row.status,
|
|
@@ -1674,6 +1866,7 @@ function scrubbedOrNull(value) {
|
|
|
1674
1866
|
return value == null ? null : scrubSecrets(value);
|
|
1675
1867
|
}
|
|
1676
1868
|
var MAX_PERSISTED_RUN_OUTPUT_CHARS = 64 * 1024;
|
|
1869
|
+
var MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS = 64 * 1024;
|
|
1677
1870
|
function clampPersistedRunOutput(value) {
|
|
1678
1871
|
if (value == null || value.length <= MAX_PERSISTED_RUN_OUTPUT_CHARS)
|
|
1679
1872
|
return value;
|
|
@@ -1688,6 +1881,42 @@ ${tail}`;
|
|
|
1688
1881
|
function persistedRunOutput(value) {
|
|
1689
1882
|
return clampPersistedRunOutput(scrubbedOrNull(value));
|
|
1690
1883
|
}
|
|
1884
|
+
function clampTextToChars(value, maxChars, reason) {
|
|
1885
|
+
if (value.length <= maxChars)
|
|
1886
|
+
return value;
|
|
1887
|
+
const marker = `
|
|
1888
|
+
...[truncated by ${reason}]...
|
|
1889
|
+
`;
|
|
1890
|
+
const budget = Math.max(0, maxChars - marker.length);
|
|
1891
|
+
const headLength = Math.ceil(budget / 2);
|
|
1892
|
+
const tailLength = Math.floor(budget / 2);
|
|
1893
|
+
return `${value.slice(0, headLength)}${marker}${value.slice(value.length - tailLength)}`;
|
|
1894
|
+
}
|
|
1895
|
+
function boundedWorkflowEventPayloadJson(scrubbedJson) {
|
|
1896
|
+
if (scrubbedJson.length <= MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS)
|
|
1897
|
+
return scrubbedJson;
|
|
1898
|
+
const base = {
|
|
1899
|
+
truncated: true,
|
|
1900
|
+
originalChars: scrubbedJson.length,
|
|
1901
|
+
maxChars: MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS,
|
|
1902
|
+
preview: ""
|
|
1903
|
+
};
|
|
1904
|
+
const baseChars = JSON.stringify(base).length;
|
|
1905
|
+
let previewBudget = Math.max(0, MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS - baseChars - 64);
|
|
1906
|
+
while (true) {
|
|
1907
|
+
const preview = clampTextToChars(scrubbedJson, previewBudget, "loops workflow-event payload retention");
|
|
1908
|
+
const bounded = JSON.stringify({ ...base, preview });
|
|
1909
|
+
if (bounded.length <= MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS || previewBudget === 0)
|
|
1910
|
+
return bounded;
|
|
1911
|
+
previewBudget = Math.max(0, previewBudget - (bounded.length - MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS) - 64);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
function persistedWorkflowEventPayload(payload) {
|
|
1915
|
+
if (payload == null)
|
|
1916
|
+
return null;
|
|
1917
|
+
const scrubbed = scrubSecretsDeep(payload);
|
|
1918
|
+
return boundedWorkflowEventPayloadJson(scrubSecrets(JSON.stringify(scrubbed)));
|
|
1919
|
+
}
|
|
1691
1920
|
function chmodIfExists(path, mode) {
|
|
1692
1921
|
try {
|
|
1693
1922
|
if (existsSync(path))
|
|
@@ -1805,6 +2034,17 @@ class Store {
|
|
|
1805
2034
|
this.addColumnIfMissing("workflow_work_items", "route_scope", "TEXT");
|
|
1806
2035
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_workflow_work_items_scope ON workflow_work_items(route_scope, status)");
|
|
1807
2036
|
}
|
|
2037
|
+
},
|
|
2038
|
+
{
|
|
2039
|
+
id: "0009_run_receipts",
|
|
2040
|
+
apply: () => this.createRunReceiptsSchema()
|
|
2041
|
+
},
|
|
2042
|
+
{
|
|
2043
|
+
id: "0010_work_item_machine_id",
|
|
2044
|
+
apply: () => {
|
|
2045
|
+
this.addColumnIfMissing("workflow_work_items", "machine_id", "TEXT");
|
|
2046
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_workflow_work_items_machine ON workflow_work_items(machine_id, status)");
|
|
2047
|
+
}
|
|
1808
2048
|
}
|
|
1809
2049
|
];
|
|
1810
2050
|
}
|
|
@@ -1866,6 +2106,28 @@ class Store {
|
|
|
1866
2106
|
CREATE INDEX IF NOT EXISTS idx_runs_status_lease ON loop_runs(status, lease_expires_at);
|
|
1867
2107
|
CREATE INDEX IF NOT EXISTS idx_runs_scheduled ON loop_runs(scheduled_for);
|
|
1868
2108
|
|
|
2109
|
+
CREATE TABLE IF NOT EXISTS run_receipts (
|
|
2110
|
+
run_id TEXT PRIMARY KEY,
|
|
2111
|
+
loop_id TEXT NOT NULL,
|
|
2112
|
+
machine_json TEXT NOT NULL,
|
|
2113
|
+
repo TEXT NOT NULL,
|
|
2114
|
+
task_ids_json TEXT NOT NULL,
|
|
2115
|
+
knowledge_ids_json TEXT NOT NULL,
|
|
2116
|
+
digest_id TEXT NOT NULL,
|
|
2117
|
+
started_at TEXT,
|
|
2118
|
+
finished_at TEXT,
|
|
2119
|
+
status TEXT NOT NULL,
|
|
2120
|
+
exit_code INTEGER,
|
|
2121
|
+
summary_json TEXT NOT NULL,
|
|
2122
|
+
evidence_paths_json TEXT NOT NULL,
|
|
2123
|
+
created_at TEXT NOT NULL,
|
|
2124
|
+
updated_at TEXT NOT NULL
|
|
2125
|
+
);
|
|
2126
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_loop ON run_receipts(loop_id, created_at DESC);
|
|
2127
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_repo ON run_receipts(repo, created_at DESC);
|
|
2128
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_digest ON run_receipts(digest_id);
|
|
2129
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_status ON run_receipts(status, created_at DESC);
|
|
2130
|
+
|
|
1869
2131
|
CREATE TABLE IF NOT EXISTS daemon_lease (
|
|
1870
2132
|
id TEXT PRIMARY KEY,
|
|
1871
2133
|
pid INTEGER NOT NULL,
|
|
@@ -1952,6 +2214,7 @@ class Store {
|
|
|
1952
2214
|
subject_ref TEXT NOT NULL,
|
|
1953
2215
|
project_key TEXT,
|
|
1954
2216
|
project_group TEXT,
|
|
2217
|
+
machine_id TEXT,
|
|
1955
2218
|
route_scope TEXT,
|
|
1956
2219
|
priority INTEGER NOT NULL,
|
|
1957
2220
|
status TEXT NOT NULL,
|
|
@@ -1970,10 +2233,10 @@ class Store {
|
|
|
1970
2233
|
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_project ON workflow_work_items(project_key, status);
|
|
1971
2234
|
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_group ON workflow_work_items(project_group, status);
|
|
1972
2235
|
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_invocation ON workflow_work_items(invocation_id);
|
|
1973
|
-
--
|
|
1974
|
-
--
|
|
2236
|
+
-- New-column indexes (route_scope, machine_id, etc.) are created ONLY by
|
|
2237
|
+
-- their additive migrations, never here: this baseline DDL
|
|
1975
2238
|
-- re-runs on EVERY open (0001 is not skip-guarded), and on a pre-0008
|
|
1976
|
-
-- database the CREATE TABLE above is a no-op, so an index on
|
|
2239
|
+
-- database the CREATE TABLE above is a no-op, so an index on a new column
|
|
1977
2240
|
-- here would execute before the column exists and crash the open
|
|
1978
2241
|
-- ("no such column: route_scope"). New columns may be folded into the
|
|
1979
2242
|
-- CREATE TABLE (fresh-db only); their indexes must live in the migration.
|
|
@@ -2084,6 +2347,31 @@ class Store {
|
|
|
2084
2347
|
CREATE INDEX IF NOT EXISTS idx_goal_runs_workflow_run ON goal_runs(workflow_run_id);
|
|
2085
2348
|
`);
|
|
2086
2349
|
}
|
|
2350
|
+
createRunReceiptsSchema() {
|
|
2351
|
+
this.db.exec(`
|
|
2352
|
+
CREATE TABLE IF NOT EXISTS run_receipts (
|
|
2353
|
+
run_id TEXT PRIMARY KEY,
|
|
2354
|
+
loop_id TEXT NOT NULL,
|
|
2355
|
+
machine_json TEXT NOT NULL,
|
|
2356
|
+
repo TEXT NOT NULL,
|
|
2357
|
+
task_ids_json TEXT NOT NULL,
|
|
2358
|
+
knowledge_ids_json TEXT NOT NULL,
|
|
2359
|
+
digest_id TEXT NOT NULL,
|
|
2360
|
+
started_at TEXT,
|
|
2361
|
+
finished_at TEXT,
|
|
2362
|
+
status TEXT NOT NULL,
|
|
2363
|
+
exit_code INTEGER,
|
|
2364
|
+
summary_json TEXT NOT NULL,
|
|
2365
|
+
evidence_paths_json TEXT NOT NULL,
|
|
2366
|
+
created_at TEXT NOT NULL,
|
|
2367
|
+
updated_at TEXT NOT NULL
|
|
2368
|
+
);
|
|
2369
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_loop ON run_receipts(loop_id, created_at DESC);
|
|
2370
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_repo ON run_receipts(repo, created_at DESC);
|
|
2371
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_digest ON run_receipts(digest_id);
|
|
2372
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_status ON run_receipts(status, created_at DESC);
|
|
2373
|
+
`);
|
|
2374
|
+
}
|
|
2087
2375
|
addColumnIfMissing(table, column, definition) {
|
|
2088
2376
|
const columns = this.db.query(`PRAGMA table_info(${table})`).all();
|
|
2089
2377
|
if (columns.some((c) => c.name === column))
|
|
@@ -2195,19 +2483,24 @@ class Store {
|
|
|
2195
2483
|
}
|
|
2196
2484
|
listLoops(opts = {}) {
|
|
2197
2485
|
const limit = opts.limit ?? 200;
|
|
2486
|
+
const offset = Math.max(0, Math.floor(opts.offset ?? 0));
|
|
2487
|
+
if (opts.name != null) {
|
|
2488
|
+
const rows2 = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT ? OFFSET ?").all(opts.name, limit, offset);
|
|
2489
|
+
return this.withLatestRunSummaries(rows2.map(rowToLoop));
|
|
2490
|
+
}
|
|
2198
2491
|
let rows;
|
|
2199
2492
|
if (opts.status && opts.archived) {
|
|
2200
|
-
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);
|
|
2493
|
+
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);
|
|
2201
2494
|
} else if (opts.status && opts.includeArchived) {
|
|
2202
|
-
rows = this.db.query("SELECT * FROM loops WHERE status = ? ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit);
|
|
2495
|
+
rows = this.db.query("SELECT * FROM loops WHERE status = ? ORDER BY next_run_at ASC LIMIT ? OFFSET ?").all(opts.status, limit, offset);
|
|
2203
2496
|
} else if (opts.status) {
|
|
2204
|
-
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);
|
|
2497
|
+
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);
|
|
2205
2498
|
} else if (opts.archived) {
|
|
2206
|
-
rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NOT NULL ORDER BY archived_at DESC LIMIT ?").all(limit);
|
|
2499
|
+
rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NOT NULL ORDER BY archived_at DESC LIMIT ? OFFSET ?").all(limit, offset);
|
|
2207
2500
|
} else if (opts.includeArchived) {
|
|
2208
|
-
rows = this.db.query("SELECT * FROM loops ORDER BY status ASC, next_run_at ASC LIMIT ?").all(limit);
|
|
2501
|
+
rows = this.db.query("SELECT * FROM loops ORDER BY status ASC, next_run_at ASC LIMIT ? OFFSET ?").all(limit, offset);
|
|
2209
2502
|
} else {
|
|
2210
|
-
rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NULL ORDER BY status ASC, next_run_at ASC LIMIT ?").all(limit);
|
|
2503
|
+
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);
|
|
2211
2504
|
}
|
|
2212
2505
|
return this.withLatestRunSummaries(rows.map(rowToLoop));
|
|
2213
2506
|
}
|
|
@@ -2885,10 +3178,10 @@ class Store {
|
|
|
2885
3178
|
const id = genId();
|
|
2886
3179
|
const status = input.status ?? "queued";
|
|
2887
3180
|
this.db.query(`INSERT INTO workflow_work_items (id, route_key, idempotency_key, invocation_id, source_type, source_ref,
|
|
2888
|
-
subject_ref, project_key, project_group, route_scope, priority, status, attempts, next_attempt_at, lease_expires_at,
|
|
3181
|
+
subject_ref, project_key, project_group, machine_id, route_scope, priority, status, attempts, next_attempt_at, lease_expires_at,
|
|
2889
3182
|
workflow_id, loop_id, workflow_run_id, last_reason, created_at, updated_at)
|
|
2890
3183
|
VALUES ($id, $routeKey, $idempotencyKey, $invocationId, $sourceType, $sourceRef, $subjectRef,
|
|
2891
|
-
$projectKey, $projectGroup, $routeScope, $priority, $status, 0, $nextAttemptAt, NULL, NULL, NULL, NULL,
|
|
3184
|
+
$projectKey, $projectGroup, $machineId, $routeScope, $priority, $status, 0, $nextAttemptAt, NULL, NULL, NULL, NULL,
|
|
2892
3185
|
$lastReason, $created, $updated)
|
|
2893
3186
|
ON CONFLICT(route_key, idempotency_key) DO UPDATE SET
|
|
2894
3187
|
invocation_id=excluded.invocation_id,
|
|
@@ -2897,6 +3190,10 @@ class Store {
|
|
|
2897
3190
|
subject_ref=excluded.subject_ref,
|
|
2898
3191
|
project_key=excluded.project_key,
|
|
2899
3192
|
project_group=excluded.project_group,
|
|
3193
|
+
machine_id=CASE
|
|
3194
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.machine_id
|
|
3195
|
+
ELSE excluded.machine_id
|
|
3196
|
+
END,
|
|
2900
3197
|
route_scope=excluded.route_scope,
|
|
2901
3198
|
priority=excluded.priority,
|
|
2902
3199
|
status=CASE
|
|
@@ -2939,6 +3236,7 @@ class Store {
|
|
|
2939
3236
|
$subjectRef: input.subjectRef,
|
|
2940
3237
|
$projectKey: input.projectKey ?? null,
|
|
2941
3238
|
$projectGroup: input.projectGroup ?? null,
|
|
3239
|
+
$machineId: input.machineId ?? null,
|
|
2942
3240
|
$routeScope: input.routeScope ?? null,
|
|
2943
3241
|
$priority: input.priority ?? 0,
|
|
2944
3242
|
$status: status,
|
|
@@ -3448,7 +3746,7 @@ class Store {
|
|
|
3448
3746
|
VALUES ($id, $workflowRunId, 1, 'created', NULL, $payload, $created)`).run({
|
|
3449
3747
|
$id: genId(),
|
|
3450
3748
|
$workflowRunId: runId,
|
|
3451
|
-
$payload:
|
|
3749
|
+
$payload: persistedWorkflowEventPayload({
|
|
3452
3750
|
workflowId: input.workflow.id,
|
|
3453
3751
|
workflowName: input.workflow.name,
|
|
3454
3752
|
stepCount: input.workflow.steps.length,
|
|
@@ -3779,7 +4077,7 @@ class Store {
|
|
|
3779
4077
|
$sequence: sequence,
|
|
3780
4078
|
$eventType: eventType,
|
|
3781
4079
|
$stepId: stepId ?? null,
|
|
3782
|
-
$payload:
|
|
4080
|
+
$payload: persistedWorkflowEventPayload(payload),
|
|
3783
4081
|
$created: now
|
|
3784
4082
|
});
|
|
3785
4083
|
const event = this.db.query("SELECT * FROM workflow_events WHERE id = ?").get(id);
|
|
@@ -4102,18 +4400,93 @@ class Store {
|
|
|
4102
4400
|
}
|
|
4103
4401
|
listRuns(opts = {}) {
|
|
4104
4402
|
const limit = opts.limit ?? 100;
|
|
4403
|
+
const offset = Math.max(0, Math.floor(opts.offset ?? 0));
|
|
4105
4404
|
let rows;
|
|
4106
4405
|
if (opts.loopId && opts.status) {
|
|
4107
|
-
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);
|
|
4406
|
+
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);
|
|
4108
4407
|
} else if (opts.loopId) {
|
|
4109
|
-
rows = this.db.query("SELECT * FROM loop_runs WHERE loop_id = ? ORDER BY created_at DESC LIMIT ?").all(opts.loopId, limit);
|
|
4408
|
+
rows = this.db.query("SELECT * FROM loop_runs WHERE loop_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?").all(opts.loopId, limit, offset);
|
|
4110
4409
|
} else if (opts.status) {
|
|
4111
|
-
rows = this.db.query("SELECT * FROM loop_runs WHERE status = ? ORDER BY created_at DESC LIMIT ?").all(opts.status, limit);
|
|
4410
|
+
rows = this.db.query("SELECT * FROM loop_runs WHERE status = ? ORDER BY created_at DESC LIMIT ? OFFSET ?").all(opts.status, limit, offset);
|
|
4112
4411
|
} else {
|
|
4113
|
-
rows = this.db.query("SELECT * FROM loop_runs ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
4412
|
+
rows = this.db.query("SELECT * FROM loop_runs ORDER BY created_at DESC LIMIT ? OFFSET ?").all(limit, offset);
|
|
4114
4413
|
}
|
|
4115
4414
|
return rows.map(rowToRun);
|
|
4116
4415
|
}
|
|
4416
|
+
writeRunReceipt(input, opts = {}) {
|
|
4417
|
+
const inputRunId = typeof input.run_id === "string" && input.run_id.trim() ? input.run_id : undefined;
|
|
4418
|
+
const existing = inputRunId ? this.getRunReceipt(inputRunId) : undefined;
|
|
4419
|
+
const run = inputRunId ? this.getRun(inputRunId) : undefined;
|
|
4420
|
+
const loop = input.loop_id ? this.getLoop(input.loop_id) : run ? this.getLoop(run.loopId) : undefined;
|
|
4421
|
+
const receipt = normalizeRunReceipt(input, { now: opts.now, run, loop, existing });
|
|
4422
|
+
this.db.query(`INSERT INTO run_receipts (run_id, loop_id, machine_json, repo, task_ids_json, knowledge_ids_json, digest_id,
|
|
4423
|
+
started_at, finished_at, status, exit_code, summary_json, evidence_paths_json, created_at, updated_at)
|
|
4424
|
+
VALUES ($runId, $loopId, $machineJson, $repo, $taskIdsJson, $knowledgeIdsJson, $digestId,
|
|
4425
|
+
$startedAt, $finishedAt, $status, $exitCode, $summaryJson, $evidencePathsJson, $createdAt, $updatedAt)
|
|
4426
|
+
ON CONFLICT(run_id) DO UPDATE SET
|
|
4427
|
+
loop_id=excluded.loop_id,
|
|
4428
|
+
machine_json=excluded.machine_json,
|
|
4429
|
+
repo=excluded.repo,
|
|
4430
|
+
task_ids_json=excluded.task_ids_json,
|
|
4431
|
+
knowledge_ids_json=excluded.knowledge_ids_json,
|
|
4432
|
+
digest_id=excluded.digest_id,
|
|
4433
|
+
started_at=excluded.started_at,
|
|
4434
|
+
finished_at=excluded.finished_at,
|
|
4435
|
+
status=excluded.status,
|
|
4436
|
+
exit_code=excluded.exit_code,
|
|
4437
|
+
summary_json=excluded.summary_json,
|
|
4438
|
+
evidence_paths_json=excluded.evidence_paths_json,
|
|
4439
|
+
updated_at=excluded.updated_at`).run({
|
|
4440
|
+
$runId: receipt.run_id,
|
|
4441
|
+
$loopId: receipt.loop_id,
|
|
4442
|
+
$machineJson: JSON.stringify(receipt.machine),
|
|
4443
|
+
$repo: receipt.repo,
|
|
4444
|
+
$taskIdsJson: JSON.stringify(receipt.task_ids),
|
|
4445
|
+
$knowledgeIdsJson: JSON.stringify(receipt.knowledge_ids),
|
|
4446
|
+
$digestId: receipt.digest_id,
|
|
4447
|
+
$startedAt: receipt.started_at,
|
|
4448
|
+
$finishedAt: receipt.finished_at,
|
|
4449
|
+
$status: receipt.status,
|
|
4450
|
+
$exitCode: receipt.exit_code,
|
|
4451
|
+
$summaryJson: JSON.stringify(receipt.summary),
|
|
4452
|
+
$evidencePathsJson: JSON.stringify(receipt.evidence_paths),
|
|
4453
|
+
$createdAt: receipt.created_at,
|
|
4454
|
+
$updatedAt: receipt.updated_at
|
|
4455
|
+
});
|
|
4456
|
+
return this.getRunReceipt(receipt.run_id) ?? receipt;
|
|
4457
|
+
}
|
|
4458
|
+
getRunReceipt(runId) {
|
|
4459
|
+
const row = this.db.query("SELECT * FROM run_receipts WHERE run_id = ?").get(runId);
|
|
4460
|
+
return row ? rowToRunReceipt(row) : undefined;
|
|
4461
|
+
}
|
|
4462
|
+
listRunReceipts(opts = {}) {
|
|
4463
|
+
const limit = opts.limit ?? 100;
|
|
4464
|
+
const filters = [];
|
|
4465
|
+
const params = [];
|
|
4466
|
+
if (opts.loopId) {
|
|
4467
|
+
filters.push("loop_id = ?");
|
|
4468
|
+
params.push(opts.loopId);
|
|
4469
|
+
}
|
|
4470
|
+
if (opts.repo) {
|
|
4471
|
+
filters.push("repo = ?");
|
|
4472
|
+
params.push(opts.repo);
|
|
4473
|
+
}
|
|
4474
|
+
if (opts.status) {
|
|
4475
|
+
filters.push("status = ?");
|
|
4476
|
+
params.push(opts.status);
|
|
4477
|
+
}
|
|
4478
|
+
if (opts.taskId) {
|
|
4479
|
+
filters.push("EXISTS (SELECT 1 FROM json_each(run_receipts.task_ids_json) WHERE value = ?)");
|
|
4480
|
+
params.push(opts.taskId);
|
|
4481
|
+
}
|
|
4482
|
+
if (opts.knowledgeId) {
|
|
4483
|
+
filters.push("EXISTS (SELECT 1 FROM json_each(run_receipts.knowledge_ids_json) WHERE value = ?)");
|
|
4484
|
+
params.push(opts.knowledgeId);
|
|
4485
|
+
}
|
|
4486
|
+
const where = filters.length ? `WHERE ${filters.join(" AND ")}` : "";
|
|
4487
|
+
const rows = this.db.query(`SELECT * FROM run_receipts ${where} ORDER BY created_at DESC LIMIT ?`).all(...params, limit);
|
|
4488
|
+
return rows.map(rowToRunReceipt);
|
|
4489
|
+
}
|
|
4117
4490
|
deferLiveExpiredRun(id, now, opts = {}) {
|
|
4118
4491
|
const updated = now.toISOString();
|
|
4119
4492
|
const deferredUntil = new Date(now.getTime() + LIVE_EXPIRED_RUN_GRACE_MS).toISOString();
|
|
@@ -4272,6 +4645,9 @@ class Store {
|
|
|
4272
4645
|
const runs = includeRuns ? this.db.query("SELECT * FROM loop_runs ORDER BY created_at ASC, id ASC").all().map(rowToRun) : [];
|
|
4273
4646
|
return { schemaVersion: SCHEMA_USER_VERSION, workflows, loops, runs, checks: this.migrationChecks() };
|
|
4274
4647
|
}
|
|
4648
|
+
exportMigrationRunPage(opts) {
|
|
4649
|
+
return this.db.query("SELECT * FROM loop_runs ORDER BY created_at ASC, id ASC LIMIT ? OFFSET ?").all(opts.limit, opts.offset).map(rowToRun);
|
|
4650
|
+
}
|
|
4275
4651
|
countTable(table) {
|
|
4276
4652
|
const row = this.db.query(`SELECT COUNT(*) AS count FROM ${table}`).get();
|
|
4277
4653
|
return row?.count ?? 0;
|
|
@@ -4596,7 +4972,7 @@ class Store {
|
|
|
4596
4972
|
// package.json
|
|
4597
4973
|
var package_default = {
|
|
4598
4974
|
name: "@hasna/loops",
|
|
4599
|
-
version: "0.4.
|
|
4975
|
+
version: "0.4.23",
|
|
4600
4976
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4601
4977
|
type: "module",
|
|
4602
4978
|
main: "dist/index.js",
|
|
@@ -4706,7 +5082,6 @@ var package_default = {
|
|
|
4706
5082
|
bun: ">=1.0.0"
|
|
4707
5083
|
},
|
|
4708
5084
|
dependencies: {
|
|
4709
|
-
"@hasna/contracts": "^0.4.1",
|
|
4710
5085
|
"@hasna/events": "^0.1.9",
|
|
4711
5086
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
4712
5087
|
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
@@ -4716,7 +5091,8 @@ var package_default = {
|
|
|
4716
5091
|
zod: "4.4.3"
|
|
4717
5092
|
},
|
|
4718
5093
|
optionalDependencies: {
|
|
4719
|
-
"@hasna/machines": "0.0.49"
|
|
5094
|
+
"@hasna/machines": "0.0.49",
|
|
5095
|
+
"@hasna/contracts": "^0.4.2"
|
|
4720
5096
|
},
|
|
4721
5097
|
devDependencies: {
|
|
4722
5098
|
"@types/bun": "latest",
|
|
@@ -5093,6 +5469,50 @@ var loops_default = {
|
|
|
5093
5469
|
}
|
|
5094
5470
|
}
|
|
5095
5471
|
},
|
|
5472
|
+
"/v1/loops/count": {
|
|
5473
|
+
get: {
|
|
5474
|
+
operationId: "countLoops",
|
|
5475
|
+
summary: "Count loops (total-row verification)",
|
|
5476
|
+
parameters: [
|
|
5477
|
+
{
|
|
5478
|
+
name: "status",
|
|
5479
|
+
in: "query",
|
|
5480
|
+
required: false,
|
|
5481
|
+
schema: {
|
|
5482
|
+
type: "string"
|
|
5483
|
+
}
|
|
5484
|
+
},
|
|
5485
|
+
{
|
|
5486
|
+
name: "includeArchived",
|
|
5487
|
+
in: "query",
|
|
5488
|
+
required: false,
|
|
5489
|
+
schema: {
|
|
5490
|
+
type: "boolean"
|
|
5491
|
+
}
|
|
5492
|
+
},
|
|
5493
|
+
{
|
|
5494
|
+
name: "archived",
|
|
5495
|
+
in: "query",
|
|
5496
|
+
required: false,
|
|
5497
|
+
schema: {
|
|
5498
|
+
type: "boolean"
|
|
5499
|
+
}
|
|
5500
|
+
}
|
|
5501
|
+
],
|
|
5502
|
+
responses: {
|
|
5503
|
+
"200": {
|
|
5504
|
+
description: "count",
|
|
5505
|
+
content: {
|
|
5506
|
+
"application/json": {
|
|
5507
|
+
schema: {
|
|
5508
|
+
$ref: "#/components/schemas/CountResponse"
|
|
5509
|
+
}
|
|
5510
|
+
}
|
|
5511
|
+
}
|
|
5512
|
+
}
|
|
5513
|
+
}
|
|
5514
|
+
}
|
|
5515
|
+
},
|
|
5096
5516
|
"/v1/loops/{id}": {
|
|
5097
5517
|
get: {
|
|
5098
5518
|
operationId: "getLoop",
|
|
@@ -5283,6 +5703,34 @@ var loops_default = {
|
|
|
5283
5703
|
}
|
|
5284
5704
|
}
|
|
5285
5705
|
},
|
|
5706
|
+
"/v1/runs/count": {
|
|
5707
|
+
get: {
|
|
5708
|
+
operationId: "countRuns",
|
|
5709
|
+
summary: "Count runs (total-row verification)",
|
|
5710
|
+
parameters: [
|
|
5711
|
+
{
|
|
5712
|
+
name: "status",
|
|
5713
|
+
in: "query",
|
|
5714
|
+
required: false,
|
|
5715
|
+
schema: {
|
|
5716
|
+
type: "string"
|
|
5717
|
+
}
|
|
5718
|
+
}
|
|
5719
|
+
],
|
|
5720
|
+
responses: {
|
|
5721
|
+
"200": {
|
|
5722
|
+
description: "count",
|
|
5723
|
+
content: {
|
|
5724
|
+
"application/json": {
|
|
5725
|
+
schema: {
|
|
5726
|
+
$ref: "#/components/schemas/CountResponse"
|
|
5727
|
+
}
|
|
5728
|
+
}
|
|
5729
|
+
}
|
|
5730
|
+
}
|
|
5731
|
+
}
|
|
5732
|
+
}
|
|
5733
|
+
},
|
|
5286
5734
|
"/v1/runs/{id}": {
|
|
5287
5735
|
get: {
|
|
5288
5736
|
operationId: "getRun",
|
|
@@ -5310,54 +5758,205 @@ var loops_default = {
|
|
|
5310
5758
|
}
|
|
5311
5759
|
}
|
|
5312
5760
|
}
|
|
5313
|
-
}
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
type: "string"
|
|
5328
|
-
},
|
|
5329
|
-
service: {
|
|
5330
|
-
type: "string"
|
|
5331
|
-
},
|
|
5332
|
-
detail: {
|
|
5333
|
-
type: "string"
|
|
5761
|
+
},
|
|
5762
|
+
"/v1/import": {
|
|
5763
|
+
post: {
|
|
5764
|
+
operationId: "importRows",
|
|
5765
|
+
summary: "Bulk id-preserving import (self-hosted backfill)",
|
|
5766
|
+
description: "Upsert full workflow/loop/run rows by id (idempotent; ON CONFLICT(id) DO UPDATE). Preserves id, status, archived state, and timestamps. Applied FK-safe: workflows, then loops, then runs. Volatile running runs are skipped and reported.",
|
|
5767
|
+
requestBody: {
|
|
5768
|
+
required: true,
|
|
5769
|
+
content: {
|
|
5770
|
+
"application/json": {
|
|
5771
|
+
schema: {
|
|
5772
|
+
$ref: "#/components/schemas/ImportInput"
|
|
5773
|
+
}
|
|
5774
|
+
}
|
|
5334
5775
|
}
|
|
5335
5776
|
},
|
|
5336
|
-
|
|
5337
|
-
"
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5777
|
+
responses: {
|
|
5778
|
+
"200": {
|
|
5779
|
+
description: "import counts",
|
|
5780
|
+
content: {
|
|
5781
|
+
"application/json": {
|
|
5782
|
+
schema: {
|
|
5783
|
+
$ref: "#/components/schemas/ImportResponse"
|
|
5784
|
+
}
|
|
5785
|
+
}
|
|
5786
|
+
}
|
|
5787
|
+
}
|
|
5788
|
+
}
|
|
5789
|
+
}
|
|
5790
|
+
},
|
|
5791
|
+
"/v1/receipts": {
|
|
5792
|
+
get: {
|
|
5793
|
+
operationId: "listRunReceipts",
|
|
5794
|
+
summary: "List run receipts",
|
|
5795
|
+
parameters: [
|
|
5796
|
+
{
|
|
5797
|
+
name: "loopId",
|
|
5798
|
+
in: "query",
|
|
5799
|
+
required: false,
|
|
5800
|
+
schema: {
|
|
5801
|
+
type: "string"
|
|
5802
|
+
}
|
|
5350
5803
|
},
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5804
|
+
{
|
|
5805
|
+
name: "repo",
|
|
5806
|
+
in: "query",
|
|
5807
|
+
required: false,
|
|
5808
|
+
schema: {
|
|
5809
|
+
type: "string"
|
|
5810
|
+
}
|
|
5354
5811
|
},
|
|
5355
|
-
|
|
5356
|
-
|
|
5812
|
+
{
|
|
5813
|
+
name: "taskId",
|
|
5814
|
+
in: "query",
|
|
5815
|
+
required: false,
|
|
5816
|
+
schema: {
|
|
5817
|
+
type: "string"
|
|
5818
|
+
}
|
|
5357
5819
|
},
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5820
|
+
{
|
|
5821
|
+
name: "knowledgeId",
|
|
5822
|
+
in: "query",
|
|
5823
|
+
required: false,
|
|
5824
|
+
schema: {
|
|
5825
|
+
type: "string"
|
|
5826
|
+
}
|
|
5827
|
+
},
|
|
5828
|
+
{
|
|
5829
|
+
name: "status",
|
|
5830
|
+
in: "query",
|
|
5831
|
+
required: false,
|
|
5832
|
+
schema: {
|
|
5833
|
+
type: "string"
|
|
5834
|
+
}
|
|
5835
|
+
},
|
|
5836
|
+
{
|
|
5837
|
+
name: "limit",
|
|
5838
|
+
in: "query",
|
|
5839
|
+
required: false,
|
|
5840
|
+
schema: {
|
|
5841
|
+
type: "integer"
|
|
5842
|
+
}
|
|
5843
|
+
}
|
|
5844
|
+
],
|
|
5845
|
+
responses: {
|
|
5846
|
+
"200": {
|
|
5847
|
+
description: "run receipts",
|
|
5848
|
+
content: {
|
|
5849
|
+
"application/json": {
|
|
5850
|
+
schema: {
|
|
5851
|
+
$ref: "#/components/schemas/RunReceiptListResponse"
|
|
5852
|
+
}
|
|
5853
|
+
}
|
|
5854
|
+
}
|
|
5855
|
+
}
|
|
5856
|
+
}
|
|
5857
|
+
},
|
|
5858
|
+
post: {
|
|
5859
|
+
operationId: "writeRunReceipt",
|
|
5860
|
+
summary: "Write a run receipt",
|
|
5861
|
+
requestBody: {
|
|
5862
|
+
required: true,
|
|
5863
|
+
content: {
|
|
5864
|
+
"application/json": {
|
|
5865
|
+
schema: {
|
|
5866
|
+
$ref: "#/components/schemas/WriteRunReceiptInput"
|
|
5867
|
+
}
|
|
5868
|
+
}
|
|
5869
|
+
}
|
|
5870
|
+
},
|
|
5871
|
+
responses: {
|
|
5872
|
+
"201": {
|
|
5873
|
+
description: "run receipt",
|
|
5874
|
+
content: {
|
|
5875
|
+
"application/json": {
|
|
5876
|
+
schema: {
|
|
5877
|
+
$ref: "#/components/schemas/RunReceiptResponse"
|
|
5878
|
+
}
|
|
5879
|
+
}
|
|
5880
|
+
}
|
|
5881
|
+
}
|
|
5882
|
+
}
|
|
5883
|
+
}
|
|
5884
|
+
},
|
|
5885
|
+
"/v1/receipts/{runId}": {
|
|
5886
|
+
get: {
|
|
5887
|
+
operationId: "getRunReceipt",
|
|
5888
|
+
summary: "Get a run receipt by run id",
|
|
5889
|
+
parameters: [
|
|
5890
|
+
{
|
|
5891
|
+
name: "runId",
|
|
5892
|
+
in: "path",
|
|
5893
|
+
required: true,
|
|
5894
|
+
schema: {
|
|
5895
|
+
type: "string"
|
|
5896
|
+
}
|
|
5897
|
+
}
|
|
5898
|
+
],
|
|
5899
|
+
responses: {
|
|
5900
|
+
"200": {
|
|
5901
|
+
description: "run receipt",
|
|
5902
|
+
content: {
|
|
5903
|
+
"application/json": {
|
|
5904
|
+
schema: {
|
|
5905
|
+
$ref: "#/components/schemas/RunReceiptResponse"
|
|
5906
|
+
}
|
|
5907
|
+
}
|
|
5908
|
+
}
|
|
5909
|
+
}
|
|
5910
|
+
}
|
|
5911
|
+
}
|
|
5912
|
+
}
|
|
5913
|
+
},
|
|
5914
|
+
components: {
|
|
5915
|
+
schemas: {
|
|
5916
|
+
Foundation: {
|
|
5917
|
+
type: "object",
|
|
5918
|
+
properties: {
|
|
5919
|
+
status: {
|
|
5920
|
+
type: "string"
|
|
5921
|
+
},
|
|
5922
|
+
version: {
|
|
5923
|
+
type: "string"
|
|
5924
|
+
},
|
|
5925
|
+
mode: {
|
|
5926
|
+
type: "string"
|
|
5927
|
+
},
|
|
5928
|
+
service: {
|
|
5929
|
+
type: "string"
|
|
5930
|
+
},
|
|
5931
|
+
detail: {
|
|
5932
|
+
type: "string"
|
|
5933
|
+
}
|
|
5934
|
+
},
|
|
5935
|
+
required: [
|
|
5936
|
+
"status",
|
|
5937
|
+
"version",
|
|
5938
|
+
"mode"
|
|
5939
|
+
]
|
|
5940
|
+
},
|
|
5941
|
+
Loop: {
|
|
5942
|
+
type: "object",
|
|
5943
|
+
properties: {
|
|
5944
|
+
id: {
|
|
5945
|
+
type: "string"
|
|
5946
|
+
},
|
|
5947
|
+
name: {
|
|
5948
|
+
type: "string"
|
|
5949
|
+
},
|
|
5950
|
+
description: {
|
|
5951
|
+
type: "string",
|
|
5952
|
+
nullable: true
|
|
5953
|
+
},
|
|
5954
|
+
status: {
|
|
5955
|
+
type: "string"
|
|
5956
|
+
},
|
|
5957
|
+
schedule: {
|
|
5958
|
+
type: "object",
|
|
5959
|
+
additionalProperties: true
|
|
5361
5960
|
},
|
|
5362
5961
|
target: {
|
|
5363
5962
|
type: "object",
|
|
@@ -5452,96 +6051,424 @@ var loops_default = {
|
|
|
5452
6051
|
type: "string",
|
|
5453
6052
|
nullable: true
|
|
5454
6053
|
},
|
|
5455
|
-
finishedAt: {
|
|
5456
|
-
type: "string",
|
|
5457
|
-
nullable: true
|
|
6054
|
+
finishedAt: {
|
|
6055
|
+
type: "string",
|
|
6056
|
+
nullable: true
|
|
6057
|
+
}
|
|
6058
|
+
},
|
|
6059
|
+
required: [
|
|
6060
|
+
"id",
|
|
6061
|
+
"loopId",
|
|
6062
|
+
"status"
|
|
6063
|
+
]
|
|
6064
|
+
},
|
|
6065
|
+
LoopResponse: {
|
|
6066
|
+
type: "object",
|
|
6067
|
+
properties: {
|
|
6068
|
+
ok: {
|
|
6069
|
+
type: "boolean"
|
|
6070
|
+
},
|
|
6071
|
+
loop: {
|
|
6072
|
+
$ref: "#/components/schemas/Loop"
|
|
6073
|
+
}
|
|
6074
|
+
},
|
|
6075
|
+
required: [
|
|
6076
|
+
"ok",
|
|
6077
|
+
"loop"
|
|
6078
|
+
]
|
|
6079
|
+
},
|
|
6080
|
+
LoopListResponse: {
|
|
6081
|
+
type: "object",
|
|
6082
|
+
properties: {
|
|
6083
|
+
ok: {
|
|
6084
|
+
type: "boolean"
|
|
6085
|
+
},
|
|
6086
|
+
loops: {
|
|
6087
|
+
type: "array",
|
|
6088
|
+
items: {
|
|
6089
|
+
$ref: "#/components/schemas/Loop"
|
|
6090
|
+
}
|
|
6091
|
+
}
|
|
6092
|
+
},
|
|
6093
|
+
required: [
|
|
6094
|
+
"ok",
|
|
6095
|
+
"loops"
|
|
6096
|
+
]
|
|
6097
|
+
},
|
|
6098
|
+
RunResponse: {
|
|
6099
|
+
type: "object",
|
|
6100
|
+
properties: {
|
|
6101
|
+
ok: {
|
|
6102
|
+
type: "boolean"
|
|
6103
|
+
},
|
|
6104
|
+
run: {
|
|
6105
|
+
$ref: "#/components/schemas/Run"
|
|
6106
|
+
}
|
|
6107
|
+
},
|
|
6108
|
+
required: [
|
|
6109
|
+
"ok",
|
|
6110
|
+
"run"
|
|
6111
|
+
]
|
|
6112
|
+
},
|
|
6113
|
+
RunListResponse: {
|
|
6114
|
+
type: "object",
|
|
6115
|
+
properties: {
|
|
6116
|
+
ok: {
|
|
6117
|
+
type: "boolean"
|
|
6118
|
+
},
|
|
6119
|
+
runs: {
|
|
6120
|
+
type: "array",
|
|
6121
|
+
items: {
|
|
6122
|
+
$ref: "#/components/schemas/Run"
|
|
6123
|
+
}
|
|
6124
|
+
}
|
|
6125
|
+
},
|
|
6126
|
+
required: [
|
|
6127
|
+
"ok",
|
|
6128
|
+
"runs"
|
|
6129
|
+
]
|
|
6130
|
+
},
|
|
6131
|
+
DeleteResponse: {
|
|
6132
|
+
type: "object",
|
|
6133
|
+
properties: {
|
|
6134
|
+
ok: {
|
|
6135
|
+
type: "boolean"
|
|
6136
|
+
},
|
|
6137
|
+
deleted: {
|
|
6138
|
+
type: "boolean"
|
|
6139
|
+
}
|
|
6140
|
+
},
|
|
6141
|
+
required: [
|
|
6142
|
+
"ok",
|
|
6143
|
+
"deleted"
|
|
6144
|
+
]
|
|
6145
|
+
},
|
|
6146
|
+
RunReceiptMachine: {
|
|
6147
|
+
oneOf: [
|
|
6148
|
+
{
|
|
6149
|
+
type: "string"
|
|
6150
|
+
},
|
|
6151
|
+
{
|
|
6152
|
+
type: "object",
|
|
6153
|
+
additionalProperties: true
|
|
6154
|
+
}
|
|
6155
|
+
]
|
|
6156
|
+
},
|
|
6157
|
+
RunReceiptSummary: {
|
|
6158
|
+
type: "object",
|
|
6159
|
+
required: [
|
|
6160
|
+
"stdout_bytes",
|
|
6161
|
+
"stderr_bytes"
|
|
6162
|
+
],
|
|
6163
|
+
properties: {
|
|
6164
|
+
text: {
|
|
6165
|
+
type: "string"
|
|
6166
|
+
},
|
|
6167
|
+
stdout_bytes: {
|
|
6168
|
+
type: "integer",
|
|
6169
|
+
minimum: 0
|
|
6170
|
+
},
|
|
6171
|
+
stderr_bytes: {
|
|
6172
|
+
type: "integer",
|
|
6173
|
+
minimum: 0
|
|
6174
|
+
},
|
|
6175
|
+
stdout_excerpt: {
|
|
6176
|
+
type: "string"
|
|
6177
|
+
},
|
|
6178
|
+
stderr_excerpt: {
|
|
6179
|
+
type: "string"
|
|
6180
|
+
},
|
|
6181
|
+
error: {
|
|
6182
|
+
type: "string"
|
|
6183
|
+
},
|
|
6184
|
+
duration_ms: {
|
|
6185
|
+
type: "integer",
|
|
6186
|
+
minimum: 0
|
|
6187
|
+
}
|
|
6188
|
+
},
|
|
6189
|
+
additionalProperties: false
|
|
6190
|
+
},
|
|
6191
|
+
RunReceipt: {
|
|
6192
|
+
type: "object",
|
|
6193
|
+
required: [
|
|
6194
|
+
"loop_id",
|
|
6195
|
+
"run_id",
|
|
6196
|
+
"machine",
|
|
6197
|
+
"repo",
|
|
6198
|
+
"task_ids",
|
|
6199
|
+
"knowledge_ids",
|
|
6200
|
+
"digest_id",
|
|
6201
|
+
"started_at",
|
|
6202
|
+
"finished_at",
|
|
6203
|
+
"status",
|
|
6204
|
+
"exit_code",
|
|
6205
|
+
"summary",
|
|
6206
|
+
"evidence_paths",
|
|
6207
|
+
"created_at",
|
|
6208
|
+
"updated_at"
|
|
6209
|
+
],
|
|
6210
|
+
properties: {
|
|
6211
|
+
loop_id: {
|
|
6212
|
+
type: "string"
|
|
6213
|
+
},
|
|
6214
|
+
run_id: {
|
|
6215
|
+
type: "string"
|
|
6216
|
+
},
|
|
6217
|
+
machine: {
|
|
6218
|
+
$ref: "#/components/schemas/RunReceiptMachine"
|
|
6219
|
+
},
|
|
6220
|
+
repo: {
|
|
6221
|
+
type: "string"
|
|
6222
|
+
},
|
|
6223
|
+
task_ids: {
|
|
6224
|
+
type: "array",
|
|
6225
|
+
items: {
|
|
6226
|
+
type: "string"
|
|
6227
|
+
}
|
|
6228
|
+
},
|
|
6229
|
+
knowledge_ids: {
|
|
6230
|
+
type: "array",
|
|
6231
|
+
items: {
|
|
6232
|
+
type: "string"
|
|
6233
|
+
}
|
|
6234
|
+
},
|
|
6235
|
+
digest_id: {
|
|
6236
|
+
type: "string"
|
|
6237
|
+
},
|
|
6238
|
+
started_at: {
|
|
6239
|
+
type: "string",
|
|
6240
|
+
format: "date-time",
|
|
6241
|
+
nullable: true
|
|
6242
|
+
},
|
|
6243
|
+
finished_at: {
|
|
6244
|
+
type: "string",
|
|
6245
|
+
format: "date-time",
|
|
6246
|
+
nullable: true
|
|
6247
|
+
},
|
|
6248
|
+
status: {
|
|
6249
|
+
type: "string"
|
|
6250
|
+
},
|
|
6251
|
+
exit_code: {
|
|
6252
|
+
type: "integer",
|
|
6253
|
+
nullable: true
|
|
6254
|
+
},
|
|
6255
|
+
summary: {
|
|
6256
|
+
$ref: "#/components/schemas/RunReceiptSummary"
|
|
6257
|
+
},
|
|
6258
|
+
evidence_paths: {
|
|
6259
|
+
type: "array",
|
|
6260
|
+
items: {
|
|
6261
|
+
type: "string"
|
|
6262
|
+
}
|
|
6263
|
+
},
|
|
6264
|
+
created_at: {
|
|
6265
|
+
type: "string",
|
|
6266
|
+
format: "date-time"
|
|
6267
|
+
},
|
|
6268
|
+
updated_at: {
|
|
6269
|
+
type: "string",
|
|
6270
|
+
format: "date-time"
|
|
6271
|
+
}
|
|
6272
|
+
},
|
|
6273
|
+
additionalProperties: false
|
|
6274
|
+
},
|
|
6275
|
+
WriteRunReceiptInput: {
|
|
6276
|
+
type: "object",
|
|
6277
|
+
required: [
|
|
6278
|
+
"run_id"
|
|
6279
|
+
],
|
|
6280
|
+
properties: {
|
|
6281
|
+
loop_id: {
|
|
6282
|
+
type: "string"
|
|
6283
|
+
},
|
|
6284
|
+
run_id: {
|
|
6285
|
+
type: "string"
|
|
6286
|
+
},
|
|
6287
|
+
machine: {
|
|
6288
|
+
$ref: "#/components/schemas/RunReceiptMachine"
|
|
6289
|
+
},
|
|
6290
|
+
repo: {
|
|
6291
|
+
type: "string"
|
|
6292
|
+
},
|
|
6293
|
+
task_ids: {
|
|
6294
|
+
type: "array",
|
|
6295
|
+
items: {
|
|
6296
|
+
type: "string"
|
|
6297
|
+
}
|
|
6298
|
+
},
|
|
6299
|
+
knowledge_ids: {
|
|
6300
|
+
type: "array",
|
|
6301
|
+
items: {
|
|
6302
|
+
type: "string"
|
|
6303
|
+
}
|
|
6304
|
+
},
|
|
6305
|
+
digest_id: {
|
|
6306
|
+
type: "string"
|
|
6307
|
+
},
|
|
6308
|
+
started_at: {
|
|
6309
|
+
type: "string",
|
|
6310
|
+
format: "date-time",
|
|
6311
|
+
nullable: true
|
|
6312
|
+
},
|
|
6313
|
+
finished_at: {
|
|
6314
|
+
type: "string",
|
|
6315
|
+
format: "date-time",
|
|
6316
|
+
nullable: true
|
|
6317
|
+
},
|
|
6318
|
+
status: {
|
|
6319
|
+
type: "string"
|
|
6320
|
+
},
|
|
6321
|
+
exit_code: {
|
|
6322
|
+
type: "integer",
|
|
6323
|
+
nullable: true
|
|
6324
|
+
},
|
|
6325
|
+
summary: {
|
|
6326
|
+
oneOf: [
|
|
6327
|
+
{
|
|
6328
|
+
type: "string"
|
|
6329
|
+
},
|
|
6330
|
+
{
|
|
6331
|
+
$ref: "#/components/schemas/RunReceiptSummary"
|
|
6332
|
+
},
|
|
6333
|
+
{
|
|
6334
|
+
type: "null"
|
|
6335
|
+
}
|
|
6336
|
+
]
|
|
6337
|
+
},
|
|
6338
|
+
evidence_paths: {
|
|
6339
|
+
type: "array",
|
|
6340
|
+
items: {
|
|
6341
|
+
type: "string"
|
|
6342
|
+
}
|
|
6343
|
+
},
|
|
6344
|
+
stdout: {
|
|
6345
|
+
type: "string"
|
|
6346
|
+
},
|
|
6347
|
+
stderr: {
|
|
6348
|
+
type: "string"
|
|
6349
|
+
},
|
|
6350
|
+
error: {
|
|
6351
|
+
type: "string"
|
|
6352
|
+
},
|
|
6353
|
+
duration_ms: {
|
|
6354
|
+
type: "integer",
|
|
6355
|
+
minimum: 0
|
|
5458
6356
|
}
|
|
5459
6357
|
},
|
|
5460
|
-
|
|
5461
|
-
"id",
|
|
5462
|
-
"loopId",
|
|
5463
|
-
"status"
|
|
5464
|
-
]
|
|
6358
|
+
additionalProperties: false
|
|
5465
6359
|
},
|
|
5466
|
-
|
|
6360
|
+
RunReceiptResponse: {
|
|
5467
6361
|
type: "object",
|
|
5468
6362
|
properties: {
|
|
5469
6363
|
ok: {
|
|
5470
6364
|
type: "boolean"
|
|
5471
6365
|
},
|
|
5472
|
-
|
|
5473
|
-
$ref: "#/components/schemas/
|
|
6366
|
+
receipt: {
|
|
6367
|
+
$ref: "#/components/schemas/RunReceipt"
|
|
5474
6368
|
}
|
|
5475
6369
|
},
|
|
5476
6370
|
required: [
|
|
5477
6371
|
"ok",
|
|
5478
|
-
"
|
|
6372
|
+
"receipt"
|
|
5479
6373
|
]
|
|
5480
6374
|
},
|
|
5481
|
-
|
|
6375
|
+
RunReceiptListResponse: {
|
|
5482
6376
|
type: "object",
|
|
5483
6377
|
properties: {
|
|
5484
6378
|
ok: {
|
|
5485
6379
|
type: "boolean"
|
|
5486
6380
|
},
|
|
5487
|
-
|
|
6381
|
+
receipts: {
|
|
5488
6382
|
type: "array",
|
|
5489
6383
|
items: {
|
|
5490
|
-
$ref: "#/components/schemas/
|
|
6384
|
+
$ref: "#/components/schemas/RunReceipt"
|
|
5491
6385
|
}
|
|
5492
6386
|
}
|
|
5493
6387
|
},
|
|
5494
6388
|
required: [
|
|
5495
6389
|
"ok",
|
|
5496
|
-
"
|
|
6390
|
+
"receipts"
|
|
5497
6391
|
]
|
|
5498
6392
|
},
|
|
5499
|
-
|
|
6393
|
+
ImportInput: {
|
|
5500
6394
|
type: "object",
|
|
6395
|
+
description: "Batches of full rows to upsert by id. Any array may be omitted.",
|
|
5501
6396
|
properties: {
|
|
5502
|
-
|
|
5503
|
-
type: "
|
|
6397
|
+
workflows: {
|
|
6398
|
+
type: "array",
|
|
6399
|
+
items: {
|
|
6400
|
+
type: "object",
|
|
6401
|
+
additionalProperties: true
|
|
6402
|
+
}
|
|
5504
6403
|
},
|
|
5505
|
-
|
|
5506
|
-
|
|
6404
|
+
loops: {
|
|
6405
|
+
type: "array",
|
|
6406
|
+
items: {
|
|
6407
|
+
type: "object",
|
|
6408
|
+
additionalProperties: true
|
|
6409
|
+
}
|
|
6410
|
+
},
|
|
6411
|
+
runs: {
|
|
6412
|
+
type: "array",
|
|
6413
|
+
items: {
|
|
6414
|
+
type: "object",
|
|
6415
|
+
additionalProperties: true
|
|
6416
|
+
}
|
|
6417
|
+
},
|
|
6418
|
+
replace: {
|
|
6419
|
+
type: "boolean",
|
|
6420
|
+
description: "Update existing rows whose id matches (default false: existing rows are left unchanged)."
|
|
5507
6421
|
}
|
|
5508
|
-
}
|
|
5509
|
-
required: [
|
|
5510
|
-
"ok",
|
|
5511
|
-
"run"
|
|
5512
|
-
]
|
|
6422
|
+
}
|
|
5513
6423
|
},
|
|
5514
|
-
|
|
6424
|
+
ImportResponse: {
|
|
5515
6425
|
type: "object",
|
|
5516
6426
|
properties: {
|
|
5517
6427
|
ok: {
|
|
5518
6428
|
type: "boolean"
|
|
5519
6429
|
},
|
|
5520
|
-
|
|
5521
|
-
type: "
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
6430
|
+
imported: {
|
|
6431
|
+
type: "object",
|
|
6432
|
+
properties: {
|
|
6433
|
+
workflows: {
|
|
6434
|
+
type: "integer"
|
|
6435
|
+
},
|
|
6436
|
+
loops: {
|
|
6437
|
+
type: "integer"
|
|
6438
|
+
},
|
|
6439
|
+
runs: {
|
|
6440
|
+
type: "integer"
|
|
6441
|
+
}
|
|
6442
|
+
},
|
|
6443
|
+
required: [
|
|
6444
|
+
"workflows",
|
|
6445
|
+
"loops",
|
|
6446
|
+
"runs"
|
|
6447
|
+
]
|
|
6448
|
+
},
|
|
6449
|
+
skippedRunning: {
|
|
6450
|
+
type: "integer"
|
|
5525
6451
|
}
|
|
5526
6452
|
},
|
|
5527
6453
|
required: [
|
|
5528
6454
|
"ok",
|
|
5529
|
-
"
|
|
6455
|
+
"imported",
|
|
6456
|
+
"skippedRunning"
|
|
5530
6457
|
]
|
|
5531
6458
|
},
|
|
5532
|
-
|
|
6459
|
+
CountResponse: {
|
|
5533
6460
|
type: "object",
|
|
5534
6461
|
properties: {
|
|
5535
6462
|
ok: {
|
|
5536
6463
|
type: "boolean"
|
|
5537
6464
|
},
|
|
5538
|
-
|
|
5539
|
-
type: "
|
|
6465
|
+
count: {
|
|
6466
|
+
type: "integer"
|
|
5540
6467
|
}
|
|
5541
6468
|
},
|
|
5542
6469
|
required: [
|
|
5543
6470
|
"ok",
|
|
5544
|
-
"
|
|
6471
|
+
"count"
|
|
5545
6472
|
]
|
|
5546
6473
|
}
|
|
5547
6474
|
},
|
|
@@ -5567,6 +6494,7 @@ function openApiDocument() {
|
|
|
5567
6494
|
var program = new Command;
|
|
5568
6495
|
var DEFAULT_BODY_LIMIT_BYTES = 64 * 1024;
|
|
5569
6496
|
var DEFAULT_EVIDENCE_LIMIT_BYTES = 256 * 1024;
|
|
6497
|
+
var DEFAULT_IMPORT_LIMIT_BYTES = 32 * 1024 * 1024;
|
|
5570
6498
|
var MIN_RUNNER_LEASE_MS = 1000;
|
|
5571
6499
|
program.name("loops-api").description("OpenLoops self-hosted control-plane API foundation").version(packageVersion()).option("-j, --json", "print JSON");
|
|
5572
6500
|
function wantsJson(opts) {
|
|
@@ -5677,6 +6605,7 @@ function createLoopsApiServer(opts = {}) {
|
|
|
5677
6605
|
storage: opts.storage,
|
|
5678
6606
|
bodyLimitBytes: opts.bodyLimitBytes ?? DEFAULT_BODY_LIMIT_BYTES,
|
|
5679
6607
|
evidenceLimitBytes: opts.evidenceLimitBytes ?? DEFAULT_EVIDENCE_LIMIT_BYTES,
|
|
6608
|
+
importLimitBytes: opts.importLimitBytes ?? DEFAULT_IMPORT_LIMIT_BYTES,
|
|
5680
6609
|
now: opts.now ?? (() => new Date)
|
|
5681
6610
|
});
|
|
5682
6611
|
}
|
|
@@ -5691,10 +6620,28 @@ async function handleV1Request(ctx) {
|
|
|
5691
6620
|
if (ctx.request.method === "GET" && segments[1] === "status")
|
|
5692
6621
|
return Response.json(apiStatus());
|
|
5693
6622
|
try {
|
|
6623
|
+
if (segments[1] === "import")
|
|
6624
|
+
return await handleImportRequest(ctx, segments.slice(2));
|
|
5694
6625
|
if (segments[1] === "loops")
|
|
5695
6626
|
return await handleLoopsRequest(ctx, segments.slice(2));
|
|
5696
6627
|
if (segments[1] === "runs")
|
|
5697
6628
|
return await handleRunsRequest(ctx, segments.slice(2));
|
|
6629
|
+
if (segments[1] === "receipts")
|
|
6630
|
+
return await handleReceiptsRequest(ctx, segments.slice(2));
|
|
6631
|
+
if (segments[1] === "workflows")
|
|
6632
|
+
return await handleWorkflowsRequest(ctx, segments.slice(2));
|
|
6633
|
+
if (segments[1] === "workflow-runs")
|
|
6634
|
+
return await handleWorkflowRunsRequest(ctx, segments.slice(2));
|
|
6635
|
+
if (segments[1] === "work-items")
|
|
6636
|
+
return await handleWorkItemsRequest(ctx, segments.slice(2));
|
|
6637
|
+
if (segments[1] === "invocations")
|
|
6638
|
+
return await handleInvocationsRequest(ctx, segments.slice(2));
|
|
6639
|
+
if (segments[1] === "goals")
|
|
6640
|
+
return await handleGoalsRequest(ctx, segments.slice(2));
|
|
6641
|
+
if (segments[1] === "goal-runs")
|
|
6642
|
+
return await handleGoalRunsRequest(ctx, segments.slice(2));
|
|
6643
|
+
if (segments[1] === "history")
|
|
6644
|
+
return await handleHistoryRequest(ctx, segments.slice(2));
|
|
5698
6645
|
if (segments[1] === "runners")
|
|
5699
6646
|
return await handleRunnerRequest(ctx, segments.slice(2));
|
|
5700
6647
|
if (segments[1] === "leases" && segments[2] === "recover" && ctx.request.method === "POST") {
|
|
@@ -5705,14 +6652,45 @@ async function handleV1Request(ctx) {
|
|
|
5705
6652
|
return errorResponse(error);
|
|
5706
6653
|
}
|
|
5707
6654
|
}
|
|
6655
|
+
async function handleImportRequest(ctx, segments) {
|
|
6656
|
+
if (segments.length !== 0 || ctx.request.method !== "POST")
|
|
6657
|
+
return fail("not_found", 404);
|
|
6658
|
+
const storage = requireStorage(ctx.storage);
|
|
6659
|
+
const body = await readJsonBody(ctx.request, ctx.importLimitBytes);
|
|
6660
|
+
const replace = body.replace === true;
|
|
6661
|
+
const workflows = Array.isArray(body.workflows) ? body.workflows : [];
|
|
6662
|
+
const loops = Array.isArray(body.loops) ? body.loops : [];
|
|
6663
|
+
const runs = Array.isArray(body.runs) ? body.runs : [];
|
|
6664
|
+
const imported = { workflows: 0, loops: 0, runs: 0 };
|
|
6665
|
+
let skippedRunning = 0;
|
|
6666
|
+
for (const workflow of workflows) {
|
|
6667
|
+
await storage.upsertMigrationWorkflow(workflow, { replace });
|
|
6668
|
+
imported.workflows += 1;
|
|
6669
|
+
}
|
|
6670
|
+
for (const loop of loops) {
|
|
6671
|
+
await storage.upsertMigrationLoop(loop, { replace });
|
|
6672
|
+
imported.loops += 1;
|
|
6673
|
+
}
|
|
6674
|
+
for (const run of runs) {
|
|
6675
|
+
if (run.status === "running") {
|
|
6676
|
+
skippedRunning += 1;
|
|
6677
|
+
continue;
|
|
6678
|
+
}
|
|
6679
|
+
await storage.upsertMigrationRun(run, { replace });
|
|
6680
|
+
imported.runs += 1;
|
|
6681
|
+
}
|
|
6682
|
+
return ok({ imported, skippedRunning });
|
|
6683
|
+
}
|
|
5708
6684
|
async function handleLoopsRequest(ctx, segments) {
|
|
5709
6685
|
const storage = requireStorage(ctx.storage);
|
|
5710
6686
|
if (segments.length === 0 && ctx.request.method === "GET") {
|
|
5711
6687
|
const loops = await storage.listLoops({
|
|
5712
6688
|
status: optionalEnum(ctx.url.searchParams.get("status"), ["active", "paused", "stopped", "expired"]),
|
|
5713
6689
|
limit: optionalLimit(ctx.url.searchParams.get("limit")),
|
|
6690
|
+
offset: optionalOffset(ctx.url.searchParams.get("offset")),
|
|
5714
6691
|
includeArchived: optionalBoolean2(ctx.url.searchParams.get("includeArchived")),
|
|
5715
|
-
archived: optionalBoolean2(ctx.url.searchParams.get("archived"))
|
|
6692
|
+
archived: optionalBoolean2(ctx.url.searchParams.get("archived")),
|
|
6693
|
+
name: optionalString(ctx.url.searchParams.get("name"))
|
|
5716
6694
|
});
|
|
5717
6695
|
return ok({ loops: loops.map(publicLoop) });
|
|
5718
6696
|
}
|
|
@@ -5721,6 +6699,13 @@ async function handleLoopsRequest(ctx, segments) {
|
|
|
5721
6699
|
const loop = await storage.createLoop(body);
|
|
5722
6700
|
return ok({ loop: publicLoop(loop) }, { status: 201 });
|
|
5723
6701
|
}
|
|
6702
|
+
if (segments.length === 1 && segments[0] === "count" && ctx.request.method === "GET") {
|
|
6703
|
+
const count = await storage.countLoops(optionalEnum(ctx.url.searchParams.get("status"), ["active", "paused", "stopped", "expired"]), {
|
|
6704
|
+
includeArchived: optionalBoolean2(ctx.url.searchParams.get("includeArchived")),
|
|
6705
|
+
archived: optionalBoolean2(ctx.url.searchParams.get("archived"))
|
|
6706
|
+
});
|
|
6707
|
+
return ok({ count });
|
|
6708
|
+
}
|
|
5724
6709
|
const id = segments[0];
|
|
5725
6710
|
if (!id)
|
|
5726
6711
|
return fail("not_found", 404);
|
|
@@ -5753,6 +6738,170 @@ async function handleLoopsRequest(ctx, segments) {
|
|
|
5753
6738
|
if (segments.length === 2 && segments[1] === "unarchive" && ctx.request.method === "POST") {
|
|
5754
6739
|
return ok({ loop: publicLoop(await storage.unarchiveLoop(id)) });
|
|
5755
6740
|
}
|
|
6741
|
+
if (segments.length === 2 && segments[1] === "rename" && ctx.request.method === "POST") {
|
|
6742
|
+
const body = await readJsonBody(ctx.request, ctx.bodyLimitBytes);
|
|
6743
|
+
const name = requiredString(body.name, "name");
|
|
6744
|
+
return ok({ loop: publicLoop(await storage.renameLoop(id, name)) });
|
|
6745
|
+
}
|
|
6746
|
+
return fail("not_found", 404);
|
|
6747
|
+
}
|
|
6748
|
+
async function handleWorkflowsRequest(ctx, segments) {
|
|
6749
|
+
const storage = requireStorage(ctx.storage);
|
|
6750
|
+
if (segments.length === 0 && ctx.request.method === "GET") {
|
|
6751
|
+
const workflows = await storage.listWorkflows({
|
|
6752
|
+
status: optionalEnum(ctx.url.searchParams.get("status"), ["active", "archived"]),
|
|
6753
|
+
limit: optionalLimit(ctx.url.searchParams.get("limit")),
|
|
6754
|
+
offset: optionalOffset(ctx.url.searchParams.get("offset"))
|
|
6755
|
+
});
|
|
6756
|
+
return ok({ workflows: workflows.map(publicWorkflow) });
|
|
6757
|
+
}
|
|
6758
|
+
if (segments.length === 0 && ctx.request.method === "POST") {
|
|
6759
|
+
const body = await readJsonBody(ctx.request, ctx.bodyLimitBytes);
|
|
6760
|
+
return ok({ workflow: publicWorkflow(await storage.createWorkflow(body)) }, { status: 201 });
|
|
6761
|
+
}
|
|
6762
|
+
if (segments.length === 1 && segments[0] === "count" && ctx.request.method === "GET") {
|
|
6763
|
+
const count = await storage.countWorkflows({
|
|
6764
|
+
status: optionalEnum(ctx.url.searchParams.get("status"), ["active", "archived"])
|
|
6765
|
+
});
|
|
6766
|
+
return ok({ count });
|
|
6767
|
+
}
|
|
6768
|
+
const id = segments[0];
|
|
6769
|
+
if (!id)
|
|
6770
|
+
return fail("not_found", 404);
|
|
6771
|
+
if (segments.length === 1 && ctx.request.method === "GET") {
|
|
6772
|
+
const workflow = await storage.getWorkflow(id);
|
|
6773
|
+
if (!workflow)
|
|
6774
|
+
return fail("workflow_not_found", 404);
|
|
6775
|
+
return ok({ workflow: publicWorkflow(workflow) });
|
|
6776
|
+
}
|
|
6777
|
+
if (segments.length === 2 && segments[1] === "archive" && ctx.request.method === "POST") {
|
|
6778
|
+
return ok({ workflow: publicWorkflow(await storage.archiveWorkflow(id)) });
|
|
6779
|
+
}
|
|
6780
|
+
return fail("not_found", 404);
|
|
6781
|
+
}
|
|
6782
|
+
async function handleWorkflowRunsRequest(ctx, segments) {
|
|
6783
|
+
const storage = requireStorage(ctx.storage);
|
|
6784
|
+
if (segments.length === 0 && ctx.request.method === "GET") {
|
|
6785
|
+
const runs = await storage.listWorkflowRuns({
|
|
6786
|
+
workflowId: ctx.url.searchParams.get("workflowId") ?? undefined,
|
|
6787
|
+
loopRunId: ctx.url.searchParams.get("loopRunId") ?? undefined,
|
|
6788
|
+
limit: optionalLimit(ctx.url.searchParams.get("limit"))
|
|
6789
|
+
});
|
|
6790
|
+
return ok({ workflowRuns: runs.map(publicWorkflowRun) });
|
|
6791
|
+
}
|
|
6792
|
+
const id = segments[0];
|
|
6793
|
+
if (!id)
|
|
6794
|
+
return fail("not_found", 404);
|
|
6795
|
+
if (segments.length === 1 && ctx.request.method === "GET") {
|
|
6796
|
+
const run = await storage.getWorkflowRun(id);
|
|
6797
|
+
if (!run)
|
|
6798
|
+
return fail("workflow_run_not_found", 404);
|
|
6799
|
+
return ok({ workflowRun: publicWorkflowRun(run) });
|
|
6800
|
+
}
|
|
6801
|
+
if (segments.length === 2 && segments[1] === "steps" && ctx.request.method === "GET") {
|
|
6802
|
+
const steps = await storage.listWorkflowStepRuns(id);
|
|
6803
|
+
return ok({ steps: steps.map((step) => publicWorkflowStepRun(step)) });
|
|
6804
|
+
}
|
|
6805
|
+
if (segments.length === 2 && segments[1] === "events" && ctx.request.method === "GET") {
|
|
6806
|
+
const events = await storage.listWorkflowEvents(id, optionalLimit(ctx.url.searchParams.get("limit")) ?? 200);
|
|
6807
|
+
return ok({ events: events.map(publicWorkflowEvent) });
|
|
6808
|
+
}
|
|
6809
|
+
if (segments.length === 2 && segments[1] === "recover" && ctx.request.method === "POST") {
|
|
6810
|
+
const body = await readJsonBody(ctx.request, ctx.bodyLimitBytes);
|
|
6811
|
+
const reason = optionalText(body.reason);
|
|
6812
|
+
const result = reason === undefined ? await storage.recoverWorkflowRun(id) : await storage.recoverWorkflowRun(id, reason);
|
|
6813
|
+
return ok({
|
|
6814
|
+
workflowRun: publicWorkflowRun(result.run),
|
|
6815
|
+
recoveredSteps: result.recoveredSteps.map((step) => publicWorkflowStepRun(step))
|
|
6816
|
+
});
|
|
6817
|
+
}
|
|
6818
|
+
return fail("not_found", 404);
|
|
6819
|
+
}
|
|
6820
|
+
async function handleWorkItemsRequest(ctx, segments) {
|
|
6821
|
+
const storage = requireStorage(ctx.storage);
|
|
6822
|
+
if (segments.length === 0 && ctx.request.method === "GET") {
|
|
6823
|
+
const items = await storage.listWorkflowWorkItems({
|
|
6824
|
+
status: optionalString(ctx.url.searchParams.get("status")),
|
|
6825
|
+
routeKey: ctx.url.searchParams.get("routeKey") ?? undefined,
|
|
6826
|
+
limit: optionalLimit(ctx.url.searchParams.get("limit"))
|
|
6827
|
+
});
|
|
6828
|
+
return ok({ workItems: items.map(publicWorkflowWorkItem) });
|
|
6829
|
+
}
|
|
6830
|
+
const id = segments[0];
|
|
6831
|
+
if (!id)
|
|
6832
|
+
return fail("not_found", 404);
|
|
6833
|
+
if (segments.length === 1 && ctx.request.method === "GET") {
|
|
6834
|
+
const item = await storage.getWorkflowWorkItem(id);
|
|
6835
|
+
if (!item)
|
|
6836
|
+
return fail("work_item_not_found", 404);
|
|
6837
|
+
return ok({ workItem: publicWorkflowWorkItem(item) });
|
|
6838
|
+
}
|
|
6839
|
+
return fail("not_found", 404);
|
|
6840
|
+
}
|
|
6841
|
+
async function handleInvocationsRequest(ctx, segments) {
|
|
6842
|
+
const storage = requireStorage(ctx.storage);
|
|
6843
|
+
if (segments.length === 0 && ctx.request.method === "GET") {
|
|
6844
|
+
const invocations = await storage.listWorkflowInvocations({ limit: optionalLimit(ctx.url.searchParams.get("limit")) });
|
|
6845
|
+
return ok({ invocations: invocations.map(publicWorkflowInvocation) });
|
|
6846
|
+
}
|
|
6847
|
+
const id = segments[0];
|
|
6848
|
+
if (!id)
|
|
6849
|
+
return fail("not_found", 404);
|
|
6850
|
+
if (segments.length === 1 && ctx.request.method === "GET") {
|
|
6851
|
+
const invocation = await storage.getWorkflowInvocation(id);
|
|
6852
|
+
if (!invocation)
|
|
6853
|
+
return fail("invocation_not_found", 404);
|
|
6854
|
+
return ok({ invocation: publicWorkflowInvocation(invocation) });
|
|
6855
|
+
}
|
|
6856
|
+
return fail("not_found", 404);
|
|
6857
|
+
}
|
|
6858
|
+
async function handleGoalsRequest(ctx, segments) {
|
|
6859
|
+
const storage = requireStorage(ctx.storage);
|
|
6860
|
+
if (segments.length === 0 && ctx.request.method === "GET") {
|
|
6861
|
+
const goals = await storage.listGoals({
|
|
6862
|
+
status: optionalString(ctx.url.searchParams.get("status")),
|
|
6863
|
+
limit: optionalLimit(ctx.url.searchParams.get("limit"))
|
|
6864
|
+
});
|
|
6865
|
+
return ok({ goals: goals.map(publicGoal) });
|
|
6866
|
+
}
|
|
6867
|
+
const id = segments[0];
|
|
6868
|
+
if (!id)
|
|
6869
|
+
return fail("not_found", 404);
|
|
6870
|
+
if (segments.length === 1 && ctx.request.method === "GET") {
|
|
6871
|
+
const goal = await storage.getGoal(id);
|
|
6872
|
+
if (!goal)
|
|
6873
|
+
return fail("goal_not_found", 404);
|
|
6874
|
+
return ok({ goal: publicGoal(goal) });
|
|
6875
|
+
}
|
|
6876
|
+
if (segments.length === 2 && segments[1] === "plan-nodes" && ctx.request.method === "GET") {
|
|
6877
|
+
const nodes = await storage.listGoalPlanNodes(id);
|
|
6878
|
+
return ok({ nodes });
|
|
6879
|
+
}
|
|
6880
|
+
return fail("not_found", 404);
|
|
6881
|
+
}
|
|
6882
|
+
async function handleGoalRunsRequest(ctx, segments) {
|
|
6883
|
+
const storage = requireStorage(ctx.storage);
|
|
6884
|
+
if (segments.length === 0 && ctx.request.method === "GET") {
|
|
6885
|
+
const runs = await storage.listGoalRuns({
|
|
6886
|
+
goalId: ctx.url.searchParams.get("goalId") ?? undefined,
|
|
6887
|
+
runId: ctx.url.searchParams.get("runId") ?? undefined,
|
|
6888
|
+
limit: optionalLimit(ctx.url.searchParams.get("limit"))
|
|
6889
|
+
});
|
|
6890
|
+
return ok({ goalRuns: runs.map(publicGoalRun) });
|
|
6891
|
+
}
|
|
6892
|
+
return fail("not_found", 404);
|
|
6893
|
+
}
|
|
6894
|
+
async function handleHistoryRequest(ctx, segments) {
|
|
6895
|
+
const storage = requireStorage(ctx.storage);
|
|
6896
|
+
if (segments.length === 1 && segments[0] === "prune" && ctx.request.method === "POST") {
|
|
6897
|
+
const body = await readJsonBody(ctx.request, ctx.bodyLimitBytes);
|
|
6898
|
+
const history = await storage.pruneHistory({
|
|
6899
|
+
maxAgeDays: optionalInteger(body.maxAgeDays),
|
|
6900
|
+
keepPerLoop: optionalInteger(body.keepPerLoop),
|
|
6901
|
+
dryRun: body.dryRun === undefined ? undefined : Boolean(body.dryRun)
|
|
6902
|
+
});
|
|
6903
|
+
return ok({ history });
|
|
6904
|
+
}
|
|
5756
6905
|
return fail("not_found", 404);
|
|
5757
6906
|
}
|
|
5758
6907
|
async function handleRunsRequest(ctx, segments) {
|
|
@@ -5788,11 +6937,16 @@ async function handleRunsRequest(ctx, segments) {
|
|
|
5788
6937
|
}
|
|
5789
6938
|
const storage = requireStorage(ctx.storage);
|
|
5790
6939
|
const showOutput = optionalBoolean2(ctx.url.searchParams.get("showOutput")) ?? false;
|
|
6940
|
+
if (segments.length === 1 && id === "count" && ctx.request.method === "GET") {
|
|
6941
|
+
const count = await storage.countRuns(optionalEnum(ctx.url.searchParams.get("status"), ["running", "succeeded", "failed", "timed_out", "abandoned", "skipped"]));
|
|
6942
|
+
return ok({ count });
|
|
6943
|
+
}
|
|
5791
6944
|
if (segments.length === 0 && ctx.request.method === "GET") {
|
|
5792
6945
|
const runs = await storage.listRuns({
|
|
5793
6946
|
loopId: ctx.url.searchParams.get("loopId") ?? undefined,
|
|
5794
6947
|
status: optionalEnum(ctx.url.searchParams.get("status"), ["running", "succeeded", "failed", "timed_out", "abandoned", "skipped"]),
|
|
5795
|
-
limit: optionalLimit(ctx.url.searchParams.get("limit"))
|
|
6948
|
+
limit: optionalLimit(ctx.url.searchParams.get("limit")),
|
|
6949
|
+
offset: optionalOffset(ctx.url.searchParams.get("offset"))
|
|
5796
6950
|
});
|
|
5797
6951
|
return ok({ runs: runs.map((run) => publicRun(run, showOutput, { redactError: true })) });
|
|
5798
6952
|
}
|
|
@@ -5806,6 +6960,34 @@ async function handleRunsRequest(ctx, segments) {
|
|
|
5806
6960
|
}
|
|
5807
6961
|
return fail("not_found", 404);
|
|
5808
6962
|
}
|
|
6963
|
+
async function handleReceiptsRequest(ctx, segments) {
|
|
6964
|
+
const storage = requireStorage(ctx.storage);
|
|
6965
|
+
const id = segments[0];
|
|
6966
|
+
if (segments.length === 0 && ctx.request.method === "GET") {
|
|
6967
|
+
const receipts = await storage.listRunReceipts({
|
|
6968
|
+
loopId: ctx.url.searchParams.get("loopId") ?? undefined,
|
|
6969
|
+
repo: ctx.url.searchParams.get("repo") ?? undefined,
|
|
6970
|
+
taskId: ctx.url.searchParams.get("taskId") ?? undefined,
|
|
6971
|
+
knowledgeId: ctx.url.searchParams.get("knowledgeId") ?? undefined,
|
|
6972
|
+
status: ctx.url.searchParams.get("status") ?? undefined,
|
|
6973
|
+
limit: optionalLimit(ctx.url.searchParams.get("limit"))
|
|
6974
|
+
});
|
|
6975
|
+
return ok({ receipts: receipts.map(publicRunReceipt) });
|
|
6976
|
+
}
|
|
6977
|
+
if (segments.length === 0 && ctx.request.method === "POST") {
|
|
6978
|
+
const body = await readJsonBody(ctx.request, ctx.evidenceLimitBytes);
|
|
6979
|
+
return ok({ receipt: publicRunReceipt(await storage.writeRunReceipt(body)) }, { status: 201 });
|
|
6980
|
+
}
|
|
6981
|
+
if (!id)
|
|
6982
|
+
return fail("not_found", 404);
|
|
6983
|
+
if (segments.length === 1 && ctx.request.method === "GET") {
|
|
6984
|
+
const receipt = await storage.getRunReceipt(id);
|
|
6985
|
+
if (!receipt)
|
|
6986
|
+
return fail("run_receipt_not_found", 404);
|
|
6987
|
+
return ok({ receipt: publicRunReceipt(receipt) });
|
|
6988
|
+
}
|
|
6989
|
+
return fail("not_found", 404);
|
|
6990
|
+
}
|
|
5809
6991
|
async function handleRunnerRequest(ctx, segments) {
|
|
5810
6992
|
if (ctx.request.method !== "POST")
|
|
5811
6993
|
return fail("not_found", 404);
|
|
@@ -5828,14 +7010,14 @@ async function handleRunnerRequest(ctx, segments) {
|
|
|
5828
7010
|
}
|
|
5829
7011
|
function runnerRecord(body) {
|
|
5830
7012
|
const machineId = optionalString(body.machineId);
|
|
5831
|
-
const
|
|
5832
|
-
const id = optionalString(body.runnerId) ?? machineId ??
|
|
7013
|
+
const hostname2 = optionalString(body.hostname);
|
|
7014
|
+
const id = optionalString(body.runnerId) ?? machineId ?? hostname2;
|
|
5833
7015
|
if (!id)
|
|
5834
7016
|
throw Object.assign(new Error("runner_id_required"), { status: 422 });
|
|
5835
7017
|
return {
|
|
5836
7018
|
id,
|
|
5837
7019
|
machineId,
|
|
5838
|
-
hostname,
|
|
7020
|
+
hostname: hostname2,
|
|
5839
7021
|
labels: stringRecord(body.labels),
|
|
5840
7022
|
capabilities: objectRecord(body.capabilities),
|
|
5841
7023
|
lastSeenAt: new Date().toISOString()
|
|
@@ -6017,13 +7199,22 @@ async function readBodyText(request, limitBytes) {
|
|
|
6017
7199
|
function runnerProtocolPending(message) {
|
|
6018
7200
|
return fail("runner_protocol_pending", 501, { message });
|
|
6019
7201
|
}
|
|
7202
|
+
var MAX_PAGE_LIMIT = 1000;
|
|
6020
7203
|
function optionalLimit(value) {
|
|
6021
7204
|
if (value == null || value === "")
|
|
6022
7205
|
return;
|
|
6023
7206
|
const limit = Number(value);
|
|
6024
|
-
if (!Number.isInteger(limit) || limit < 1
|
|
7207
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
6025
7208
|
throw Object.assign(new Error("invalid_limit"), { status: 422 });
|
|
6026
|
-
return limit;
|
|
7209
|
+
return Math.min(limit, MAX_PAGE_LIMIT);
|
|
7210
|
+
}
|
|
7211
|
+
function optionalOffset(value) {
|
|
7212
|
+
if (value == null || value === "")
|
|
7213
|
+
return;
|
|
7214
|
+
const offset = Number(value);
|
|
7215
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
7216
|
+
throw Object.assign(new Error("invalid_offset"), { status: 422 });
|
|
7217
|
+
return offset;
|
|
6027
7218
|
}
|
|
6028
7219
|
function optionalString(value) {
|
|
6029
7220
|
if (value === undefined || value === null)
|
|
@@ -6137,12 +7328,12 @@ if (import.meta.main && (import.meta.url.endsWith("api/index.ts") || import.meta
|
|
|
6137
7328
|
}
|
|
6138
7329
|
|
|
6139
7330
|
// src/lib/storage/postgres-schema.ts
|
|
6140
|
-
import { createHash as
|
|
7331
|
+
import { createHash as createHash3 } from "crypto";
|
|
6141
7332
|
var POSTGRES_MIGRATION_LEDGER_TABLE = "open_loops_schema_migrations";
|
|
6142
7333
|
function checksumStorageSql(sql) {
|
|
6143
7334
|
const normalized = sql.trim().replace(/\r\n/g, `
|
|
6144
7335
|
`);
|
|
6145
|
-
return `sha256:${
|
|
7336
|
+
return `sha256:${createHash3("sha256").update(normalized).digest("hex")}`;
|
|
6146
7337
|
}
|
|
6147
7338
|
function migration(id, sql) {
|
|
6148
7339
|
return Object.freeze({ id, sql: sql.trim(), checksum: checksumStorageSql(sql) });
|
|
@@ -6460,6 +7651,35 @@ CREATE INDEX IF NOT EXISTS idx_audit_events_action ON audit_events(action, creat
|
|
|
6460
7651
|
migration("0004_work_item_route_scope", `
|
|
6461
7652
|
ALTER TABLE workflow_work_items ADD COLUMN IF NOT EXISTS route_scope TEXT;
|
|
6462
7653
|
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_scope ON workflow_work_items(route_scope, status);
|
|
7654
|
+
`),
|
|
7655
|
+
migration("0005_run_receipts", `
|
|
7656
|
+
CREATE TABLE IF NOT EXISTS run_receipts (
|
|
7657
|
+
run_id TEXT PRIMARY KEY,
|
|
7658
|
+
loop_id TEXT NOT NULL,
|
|
7659
|
+
machine_json JSONB NOT NULL,
|
|
7660
|
+
repo TEXT NOT NULL,
|
|
7661
|
+
task_ids_json JSONB NOT NULL,
|
|
7662
|
+
knowledge_ids_json JSONB NOT NULL,
|
|
7663
|
+
digest_id TEXT NOT NULL,
|
|
7664
|
+
started_at TIMESTAMPTZ,
|
|
7665
|
+
finished_at TIMESTAMPTZ,
|
|
7666
|
+
status TEXT NOT NULL,
|
|
7667
|
+
exit_code INTEGER,
|
|
7668
|
+
summary_json JSONB NOT NULL,
|
|
7669
|
+
evidence_paths_json JSONB NOT NULL,
|
|
7670
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
7671
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
7672
|
+
);
|
|
7673
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_loop ON run_receipts(loop_id, created_at DESC);
|
|
7674
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_repo ON run_receipts(repo, created_at DESC);
|
|
7675
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_digest ON run_receipts(digest_id);
|
|
7676
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_status ON run_receipts(status, created_at DESC);
|
|
7677
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_task_ids ON run_receipts USING GIN (task_ids_json);
|
|
7678
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_knowledge_ids ON run_receipts USING GIN (knowledge_ids_json);
|
|
7679
|
+
`),
|
|
7680
|
+
migration("0006_work_item_machine_id", `
|
|
7681
|
+
ALTER TABLE workflow_work_items ADD COLUMN IF NOT EXISTS machine_id TEXT;
|
|
7682
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_machine ON workflow_work_items(machine_id, status);
|
|
6463
7683
|
`)
|
|
6464
7684
|
]);
|
|
6465
7685
|
|
|
@@ -6738,6 +7958,9 @@ class NotImplementedError extends Error {
|
|
|
6738
7958
|
}
|
|
6739
7959
|
var TERMINAL_RUN_STATUSES2 = ["succeeded", "failed", "timed_out", "abandoned", "skipped"];
|
|
6740
7960
|
var PRUNE_BATCH_SIZE2 = 400;
|
|
7961
|
+
function isUniqueViolation(error) {
|
|
7962
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "23505";
|
|
7963
|
+
}
|
|
6741
7964
|
var DEFAULT_RECOVERY_BATCH_LIMIT2 = 100;
|
|
6742
7965
|
var DEFAULT_RECOVERY_SCAN_MULTIPLIER2 = 5;
|
|
6743
7966
|
|
|
@@ -6869,19 +8092,22 @@ class PostgresLoopStorage {
|
|
|
6869
8092
|
async listLoops(...args) {
|
|
6870
8093
|
const opts = args[0] ?? {};
|
|
6871
8094
|
const limit = opts.limit ?? 200;
|
|
8095
|
+
const offset = Math.max(0, Math.floor(opts.offset ?? 0));
|
|
6872
8096
|
let rows;
|
|
6873
|
-
if (opts.
|
|
6874
|
-
rows = await this.client.many("SELECT * FROM loops WHERE
|
|
8097
|
+
if (opts.name != null) {
|
|
8098
|
+
rows = await this.client.many("SELECT * FROM loops WHERE name = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3", [opts.name, limit, offset]);
|
|
8099
|
+
} else if (opts.status && opts.archived) {
|
|
8100
|
+
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]);
|
|
6875
8101
|
} else if (opts.status && opts.includeArchived) {
|
|
6876
|
-
rows = await this.client.many("SELECT * FROM loops WHERE status = $1 ORDER BY next_run_at ASC LIMIT $2", [opts.status, limit]);
|
|
8102
|
+
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]);
|
|
6877
8103
|
} else if (opts.status) {
|
|
6878
|
-
rows = await this.client.many("SELECT * FROM loops WHERE status = $1 AND archived_at IS NULL ORDER BY next_run_at ASC LIMIT $2", [opts.status, limit]);
|
|
8104
|
+
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]);
|
|
6879
8105
|
} else if (opts.archived) {
|
|
6880
|
-
rows = await this.client.many("SELECT * FROM loops WHERE archived_at IS NOT NULL ORDER BY archived_at DESC LIMIT $1", [limit]);
|
|
8106
|
+
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]);
|
|
6881
8107
|
} else if (opts.includeArchived) {
|
|
6882
|
-
rows = await this.client.many("SELECT * FROM loops ORDER BY status ASC, next_run_at ASC LIMIT $1", [limit]);
|
|
8108
|
+
rows = await this.client.many("SELECT * FROM loops ORDER BY status ASC, next_run_at ASC LIMIT $1 OFFSET $2", [limit, offset]);
|
|
6883
8109
|
} else {
|
|
6884
|
-
rows = await this.client.many("SELECT * FROM loops WHERE archived_at IS NULL ORDER BY status ASC, next_run_at ASC LIMIT $1", [limit]);
|
|
8110
|
+
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]);
|
|
6885
8111
|
}
|
|
6886
8112
|
return rows.map(rowToLoop);
|
|
6887
8113
|
}
|
|
@@ -7012,6 +8238,172 @@ class PostgresLoopStorage {
|
|
|
7012
8238
|
const row = await this.client.get(sql, params);
|
|
7013
8239
|
return row?.count ?? 0;
|
|
7014
8240
|
}
|
|
8241
|
+
async upsertMigrationWorkflow(...args) {
|
|
8242
|
+
const [workflow, opts = {}] = args;
|
|
8243
|
+
const existing = await this.getWorkflow(workflow.id);
|
|
8244
|
+
if (existing && !opts.replace)
|
|
8245
|
+
return existing;
|
|
8246
|
+
try {
|
|
8247
|
+
await this.client.execute(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
|
|
8248
|
+
VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7::jsonb,$8,$9)
|
|
8249
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
8250
|
+
name=EXCLUDED.name,
|
|
8251
|
+
description=EXCLUDED.description,
|
|
8252
|
+
version=EXCLUDED.version,
|
|
8253
|
+
status=EXCLUDED.status,
|
|
8254
|
+
goal_json=EXCLUDED.goal_json,
|
|
8255
|
+
steps_json=EXCLUDED.steps_json,
|
|
8256
|
+
created_at=EXCLUDED.created_at,
|
|
8257
|
+
updated_at=EXCLUDED.updated_at`, [
|
|
8258
|
+
workflow.id,
|
|
8259
|
+
workflow.name,
|
|
8260
|
+
workflow.description ?? null,
|
|
8261
|
+
workflow.version,
|
|
8262
|
+
workflow.status,
|
|
8263
|
+
workflow.goal ? JSON.stringify(workflow.goal) : null,
|
|
8264
|
+
JSON.stringify(workflow.steps),
|
|
8265
|
+
workflow.createdAt,
|
|
8266
|
+
workflow.updatedAt
|
|
8267
|
+
]);
|
|
8268
|
+
} catch (error) {
|
|
8269
|
+
if (isUniqueViolation(error)) {
|
|
8270
|
+
const owner = await this.client.get("SELECT * FROM workflow_specs WHERE name = $1 AND status = 'active' LIMIT 1", [workflow.name]);
|
|
8271
|
+
if (owner)
|
|
8272
|
+
return rowToWorkflow(owner);
|
|
8273
|
+
}
|
|
8274
|
+
throw error;
|
|
8275
|
+
}
|
|
8276
|
+
const imported = await this.getWorkflow(workflow.id);
|
|
8277
|
+
if (!imported)
|
|
8278
|
+
throw new Error(`workflow not found after migration import: ${workflow.id}`);
|
|
8279
|
+
return imported;
|
|
8280
|
+
}
|
|
8281
|
+
async upsertMigrationLoop(...args) {
|
|
8282
|
+
const [loop, opts = {}] = args;
|
|
8283
|
+
const existing = await this.loadLoop(this.client, loop.id);
|
|
8284
|
+
if (existing && !opts.replace)
|
|
8285
|
+
return existing;
|
|
8286
|
+
await this.client.execute(`INSERT INTO loops (id, name, description, status, archived_at, archived_from_status, schedule_json, target_json,
|
|
8287
|
+
goal_json, machine_json, next_run_at, retry_scheduled_for, catch_up, catch_up_limit, overlap, max_attempts,
|
|
8288
|
+
retry_delay_ms, lease_ms, expires_at, created_at, updated_at)
|
|
8289
|
+
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)
|
|
8290
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
8291
|
+
name=EXCLUDED.name,
|
|
8292
|
+
description=EXCLUDED.description,
|
|
8293
|
+
status=EXCLUDED.status,
|
|
8294
|
+
archived_at=EXCLUDED.archived_at,
|
|
8295
|
+
archived_from_status=EXCLUDED.archived_from_status,
|
|
8296
|
+
schedule_json=EXCLUDED.schedule_json,
|
|
8297
|
+
target_json=EXCLUDED.target_json,
|
|
8298
|
+
goal_json=EXCLUDED.goal_json,
|
|
8299
|
+
machine_json=EXCLUDED.machine_json,
|
|
8300
|
+
next_run_at=EXCLUDED.next_run_at,
|
|
8301
|
+
retry_scheduled_for=EXCLUDED.retry_scheduled_for,
|
|
8302
|
+
catch_up=EXCLUDED.catch_up,
|
|
8303
|
+
catch_up_limit=EXCLUDED.catch_up_limit,
|
|
8304
|
+
overlap=EXCLUDED.overlap,
|
|
8305
|
+
max_attempts=EXCLUDED.max_attempts,
|
|
8306
|
+
retry_delay_ms=EXCLUDED.retry_delay_ms,
|
|
8307
|
+
lease_ms=EXCLUDED.lease_ms,
|
|
8308
|
+
expires_at=EXCLUDED.expires_at,
|
|
8309
|
+
created_at=EXCLUDED.created_at,
|
|
8310
|
+
updated_at=EXCLUDED.updated_at`, [
|
|
8311
|
+
loop.id,
|
|
8312
|
+
loop.name,
|
|
8313
|
+
loop.description ?? null,
|
|
8314
|
+
loop.status,
|
|
8315
|
+
loop.archivedAt ?? null,
|
|
8316
|
+
loop.archivedFromStatus ?? null,
|
|
8317
|
+
JSON.stringify(loop.schedule),
|
|
8318
|
+
JSON.stringify(loop.target),
|
|
8319
|
+
loop.goal ? JSON.stringify(loop.goal) : null,
|
|
8320
|
+
loop.machine ? JSON.stringify(loop.machine) : null,
|
|
8321
|
+
loop.nextRunAt ?? null,
|
|
8322
|
+
loop.retryScheduledFor ?? null,
|
|
8323
|
+
loop.catchUp,
|
|
8324
|
+
loop.catchUpLimit,
|
|
8325
|
+
loop.overlap,
|
|
8326
|
+
loop.maxAttempts,
|
|
8327
|
+
loop.retryDelayMs,
|
|
8328
|
+
loop.leaseMs,
|
|
8329
|
+
loop.expiresAt ?? null,
|
|
8330
|
+
loop.createdAt,
|
|
8331
|
+
loop.updatedAt
|
|
8332
|
+
]);
|
|
8333
|
+
const imported = await this.loadLoop(this.client, loop.id);
|
|
8334
|
+
if (!imported)
|
|
8335
|
+
throw new Error(`loop not found after migration import: ${loop.id}`);
|
|
8336
|
+
return imported;
|
|
8337
|
+
}
|
|
8338
|
+
async upsertMigrationRun(...args) {
|
|
8339
|
+
const [run, opts = {}] = args;
|
|
8340
|
+
if (run.status === "running")
|
|
8341
|
+
throw new ValidationError(`cannot import running run ${run.id}`);
|
|
8342
|
+
const existing = await this.loadRun(this.client, run.id);
|
|
8343
|
+
if (existing && !opts.replace)
|
|
8344
|
+
return existing;
|
|
8345
|
+
try {
|
|
8346
|
+
await this.client.execute(`INSERT INTO loop_runs (id, loop_id, loop_name, scheduled_for, attempt, status, started_at, finished_at,
|
|
8347
|
+
claimed_by, claim_token, lease_expires_at, pid, pgid, process_started_at, exit_code, duration_ms,
|
|
8348
|
+
stdout, stderr, error, goal_run_id, created_at, updated_at)
|
|
8349
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,NULL,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21)
|
|
8350
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
8351
|
+
loop_id=EXCLUDED.loop_id,
|
|
8352
|
+
loop_name=EXCLUDED.loop_name,
|
|
8353
|
+
scheduled_for=EXCLUDED.scheduled_for,
|
|
8354
|
+
attempt=EXCLUDED.attempt,
|
|
8355
|
+
status=EXCLUDED.status,
|
|
8356
|
+
started_at=EXCLUDED.started_at,
|
|
8357
|
+
finished_at=EXCLUDED.finished_at,
|
|
8358
|
+
claimed_by=EXCLUDED.claimed_by,
|
|
8359
|
+
claim_token=NULL,
|
|
8360
|
+
lease_expires_at=EXCLUDED.lease_expires_at,
|
|
8361
|
+
pid=EXCLUDED.pid,
|
|
8362
|
+
pgid=EXCLUDED.pgid,
|
|
8363
|
+
process_started_at=EXCLUDED.process_started_at,
|
|
8364
|
+
exit_code=EXCLUDED.exit_code,
|
|
8365
|
+
duration_ms=EXCLUDED.duration_ms,
|
|
8366
|
+
stdout=EXCLUDED.stdout,
|
|
8367
|
+
stderr=EXCLUDED.stderr,
|
|
8368
|
+
error=EXCLUDED.error,
|
|
8369
|
+
goal_run_id=EXCLUDED.goal_run_id,
|
|
8370
|
+
created_at=EXCLUDED.created_at,
|
|
8371
|
+
updated_at=EXCLUDED.updated_at`, [
|
|
8372
|
+
run.id,
|
|
8373
|
+
run.loopId,
|
|
8374
|
+
run.loopName,
|
|
8375
|
+
run.scheduledFor,
|
|
8376
|
+
run.attempt,
|
|
8377
|
+
run.status,
|
|
8378
|
+
run.startedAt ?? null,
|
|
8379
|
+
run.finishedAt ?? null,
|
|
8380
|
+
run.claimedBy ?? null,
|
|
8381
|
+
run.leaseExpiresAt ?? null,
|
|
8382
|
+
run.pid ?? null,
|
|
8383
|
+
run.pgid ?? null,
|
|
8384
|
+
run.processStartedAt ?? null,
|
|
8385
|
+
run.exitCode ?? null,
|
|
8386
|
+
run.durationMs ?? null,
|
|
8387
|
+
persistedRunOutput(run.stdout),
|
|
8388
|
+
persistedRunOutput(run.stderr),
|
|
8389
|
+
scrubbedOrNull(run.error),
|
|
8390
|
+
run.goalRunId ?? null,
|
|
8391
|
+
run.createdAt,
|
|
8392
|
+
run.updatedAt
|
|
8393
|
+
]);
|
|
8394
|
+
} catch (error) {
|
|
8395
|
+
if (isUniqueViolation(error)) {
|
|
8396
|
+
const slot = await this.loadRunBySlot(this.client, run.loopId, run.scheduledFor);
|
|
8397
|
+
if (slot)
|
|
8398
|
+
return slot;
|
|
8399
|
+
}
|
|
8400
|
+
throw error;
|
|
8401
|
+
}
|
|
8402
|
+
const imported = await this.loadRun(this.client, run.id);
|
|
8403
|
+
if (!imported)
|
|
8404
|
+
throw new Error(`run not found after migration import: ${run.id}`);
|
|
8405
|
+
return imported;
|
|
8406
|
+
}
|
|
7015
8407
|
async createSkippedRun(...args) {
|
|
7016
8408
|
const [loop, scheduledFor, reason, opts = {}] = args;
|
|
7017
8409
|
const now = nowIso();
|
|
@@ -7179,18 +8571,102 @@ class PostgresLoopStorage {
|
|
|
7179
8571
|
async listRuns(...args) {
|
|
7180
8572
|
const opts = args[0] ?? {};
|
|
7181
8573
|
const limit = opts.limit ?? 100;
|
|
8574
|
+
const offset = Math.max(0, Math.floor(opts.offset ?? 0));
|
|
7182
8575
|
let rows;
|
|
7183
8576
|
if (opts.loopId && opts.status) {
|
|
7184
|
-
rows = await this.client.many("SELECT * FROM loop_runs WHERE loop_id = $1 AND status = $2 ORDER BY created_at DESC LIMIT $3", [opts.loopId, opts.status, limit]);
|
|
8577
|
+
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]);
|
|
7185
8578
|
} else if (opts.loopId) {
|
|
7186
|
-
rows = await this.client.many("SELECT * FROM loop_runs WHERE loop_id = $1 ORDER BY created_at DESC LIMIT $2", [opts.loopId, limit]);
|
|
8579
|
+
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]);
|
|
7187
8580
|
} else if (opts.status) {
|
|
7188
|
-
rows = await this.client.many("SELECT * FROM loop_runs WHERE status = $1 ORDER BY created_at DESC LIMIT $2", [opts.status, limit]);
|
|
8581
|
+
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]);
|
|
7189
8582
|
} else {
|
|
7190
|
-
rows = await this.client.many("SELECT * FROM loop_runs ORDER BY created_at DESC LIMIT $1", [limit]);
|
|
8583
|
+
rows = await this.client.many("SELECT * FROM loop_runs ORDER BY created_at DESC LIMIT $1 OFFSET $2", [limit, offset]);
|
|
7191
8584
|
}
|
|
7192
8585
|
return rows.map(rowToRun);
|
|
7193
8586
|
}
|
|
8587
|
+
async writeRunReceipt(...args) {
|
|
8588
|
+
const [input, opts = {}] = args;
|
|
8589
|
+
const inputRunId = typeof input.run_id === "string" && input.run_id.trim() ? input.run_id : undefined;
|
|
8590
|
+
const existing = inputRunId ? await this.getRunReceipt(inputRunId) : undefined;
|
|
8591
|
+
const run = inputRunId ? await this.getRun(inputRunId) : undefined;
|
|
8592
|
+
const loop = input.loop_id ? await this.getLoop(input.loop_id) : run ? await this.getLoop(run.loopId) : undefined;
|
|
8593
|
+
const receipt = normalizeRunReceipt(input, { now: opts.now, run, loop, existing });
|
|
8594
|
+
await this.client.execute(`INSERT INTO run_receipts (run_id, loop_id, machine_json, repo, task_ids_json, knowledge_ids_json, digest_id,
|
|
8595
|
+
started_at, finished_at, status, exit_code, summary_json, evidence_paths_json, created_at, updated_at)
|
|
8596
|
+
VALUES ($1,$2,$3::jsonb,$4,$5::jsonb,$6::jsonb,$7,$8,$9,$10,$11,$12::jsonb,$13::jsonb,$14,$15)
|
|
8597
|
+
ON CONFLICT(run_id) DO UPDATE SET
|
|
8598
|
+
loop_id=EXCLUDED.loop_id,
|
|
8599
|
+
machine_json=EXCLUDED.machine_json,
|
|
8600
|
+
repo=EXCLUDED.repo,
|
|
8601
|
+
task_ids_json=EXCLUDED.task_ids_json,
|
|
8602
|
+
knowledge_ids_json=EXCLUDED.knowledge_ids_json,
|
|
8603
|
+
digest_id=EXCLUDED.digest_id,
|
|
8604
|
+
started_at=EXCLUDED.started_at,
|
|
8605
|
+
finished_at=EXCLUDED.finished_at,
|
|
8606
|
+
status=EXCLUDED.status,
|
|
8607
|
+
exit_code=EXCLUDED.exit_code,
|
|
8608
|
+
summary_json=EXCLUDED.summary_json,
|
|
8609
|
+
evidence_paths_json=EXCLUDED.evidence_paths_json,
|
|
8610
|
+
updated_at=EXCLUDED.updated_at`, [
|
|
8611
|
+
receipt.run_id,
|
|
8612
|
+
receipt.loop_id,
|
|
8613
|
+
JSON.stringify(receipt.machine),
|
|
8614
|
+
receipt.repo,
|
|
8615
|
+
JSON.stringify(receipt.task_ids),
|
|
8616
|
+
JSON.stringify(receipt.knowledge_ids),
|
|
8617
|
+
receipt.digest_id,
|
|
8618
|
+
receipt.started_at,
|
|
8619
|
+
receipt.finished_at,
|
|
8620
|
+
receipt.status,
|
|
8621
|
+
receipt.exit_code,
|
|
8622
|
+
JSON.stringify(receipt.summary),
|
|
8623
|
+
JSON.stringify(receipt.evidence_paths),
|
|
8624
|
+
receipt.created_at,
|
|
8625
|
+
receipt.updated_at
|
|
8626
|
+
]);
|
|
8627
|
+
return await this.getRunReceipt(receipt.run_id) ?? receipt;
|
|
8628
|
+
}
|
|
8629
|
+
async getRunReceipt(...args) {
|
|
8630
|
+
const row = await this.client.get("SELECT * FROM run_receipts WHERE run_id = $1", [args[0]]);
|
|
8631
|
+
return row ? rowToRunReceipt(row) : undefined;
|
|
8632
|
+
}
|
|
8633
|
+
async listRunReceipts(...args) {
|
|
8634
|
+
const opts = args[0] ?? {};
|
|
8635
|
+
const limit = opts.limit ?? 100;
|
|
8636
|
+
const filters = [];
|
|
8637
|
+
const params = [];
|
|
8638
|
+
const next = () => `$${params.length + 1}`;
|
|
8639
|
+
if (opts.loopId) {
|
|
8640
|
+
const slot = next();
|
|
8641
|
+
filters.push(`loop_id = ${slot}`);
|
|
8642
|
+
params.push(opts.loopId);
|
|
8643
|
+
}
|
|
8644
|
+
if (opts.repo) {
|
|
8645
|
+
const slot = next();
|
|
8646
|
+
filters.push(`repo = ${slot}`);
|
|
8647
|
+
params.push(opts.repo);
|
|
8648
|
+
}
|
|
8649
|
+
if (opts.status) {
|
|
8650
|
+
const slot = next();
|
|
8651
|
+
filters.push(`status = ${slot}`);
|
|
8652
|
+
params.push(opts.status);
|
|
8653
|
+
}
|
|
8654
|
+
if (opts.taskId) {
|
|
8655
|
+
const slot = next();
|
|
8656
|
+
filters.push(`task_ids_json ? ${slot}`);
|
|
8657
|
+
params.push(opts.taskId);
|
|
8658
|
+
}
|
|
8659
|
+
if (opts.knowledgeId) {
|
|
8660
|
+
const slot = next();
|
|
8661
|
+
filters.push(`knowledge_ids_json ? ${slot}`);
|
|
8662
|
+
params.push(opts.knowledgeId);
|
|
8663
|
+
}
|
|
8664
|
+
const limitSlot = next();
|
|
8665
|
+
params.push(limit);
|
|
8666
|
+
const where = filters.length ? `WHERE ${filters.join(" AND ")}` : "";
|
|
8667
|
+
const rows = await this.client.many(`SELECT * FROM run_receipts ${where} ORDER BY created_at DESC LIMIT ${limitSlot}`, params);
|
|
8668
|
+
return rows.map(rowToRunReceipt);
|
|
8669
|
+
}
|
|
7194
8670
|
async countRuns(...args) {
|
|
7195
8671
|
const status = args[0];
|
|
7196
8672
|
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", []);
|
|
@@ -7513,7 +8989,7 @@ var APP = "loops";
|
|
|
7513
8989
|
function resolveDatabaseUrl2() {
|
|
7514
8990
|
const dsn = process.env.HASNA_LOOPS_DATABASE_URL?.trim() || process.env.LOOPS_DATABASE_URL?.trim() || process.env.DATABASE_URL?.trim();
|
|
7515
8991
|
if (!dsn) {
|
|
7516
|
-
throw new Error("loops-serve requires a
|
|
8992
|
+
throw new Error("loops-serve requires a self-hosted database URL: set HASNA_LOOPS_DATABASE_URL (or LOOPS_DATABASE_URL / DATABASE_URL)");
|
|
7517
8993
|
}
|
|
7518
8994
|
return dsn;
|
|
7519
8995
|
}
|