@hasna/loops 0.4.14 → 0.4.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +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 +5419 -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
|
@@ -1202,6 +1202,175 @@ function discardWorkflowRunManifest(staged) {
|
|
|
1202
1202
|
} catch {}
|
|
1203
1203
|
}
|
|
1204
1204
|
|
|
1205
|
+
// src/lib/run-receipts.ts
|
|
1206
|
+
import { createHash as createHash2 } from "crypto";
|
|
1207
|
+
import { hostname } from "os";
|
|
1208
|
+
|
|
1209
|
+
// src/lib/run-envelope.ts
|
|
1210
|
+
var ENVELOPE_EXCERPT_CHARS = 2048;
|
|
1211
|
+
var BLOCKED_STEP_ERROR_PREFIX = "blocked:";
|
|
1212
|
+
function boundedExcerpt(text, limit = ENVELOPE_EXCERPT_CHARS) {
|
|
1213
|
+
if (!text)
|
|
1214
|
+
return;
|
|
1215
|
+
const scrubbed = scrubSecrets(text);
|
|
1216
|
+
if (scrubbed.length <= limit * 2)
|
|
1217
|
+
return scrubbed;
|
|
1218
|
+
const omitted = scrubbed.length - limit * 2;
|
|
1219
|
+
return `${scrubbed.slice(0, limit)}
|
|
1220
|
+
[... ${omitted} chars omitted ...]
|
|
1221
|
+
${scrubbed.slice(-limit)}`;
|
|
1222
|
+
}
|
|
1223
|
+
function summarizeOutput(stdout, stderr) {
|
|
1224
|
+
return {
|
|
1225
|
+
stdoutBytes: stdout ? Buffer.byteLength(stdout, "utf8") : 0,
|
|
1226
|
+
stderrBytes: stderr ? Buffer.byteLength(stderr, "utf8") : 0,
|
|
1227
|
+
stdoutExcerpt: boundedExcerpt(stdout),
|
|
1228
|
+
stderrExcerpt: boundedExcerpt(stderr)
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
function summarizeExecutorResult(result) {
|
|
1232
|
+
return {
|
|
1233
|
+
...summarizeOutput(result.stdout, result.stderr),
|
|
1234
|
+
status: result.status,
|
|
1235
|
+
exitCode: result.exitCode,
|
|
1236
|
+
durationMs: result.durationMs,
|
|
1237
|
+
error: boundedExcerpt(result.error)
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
function isBlockedStepRun(step) {
|
|
1241
|
+
return step?.status === "skipped" && (step.error?.startsWith(BLOCKED_STEP_ERROR_PREFIX) ?? false);
|
|
1242
|
+
}
|
|
1243
|
+
function summarizeWorkflowStepRun(step) {
|
|
1244
|
+
return {
|
|
1245
|
+
...summarizeOutput(step.stdout, step.stderr),
|
|
1246
|
+
stepId: step.stepId,
|
|
1247
|
+
status: step.status,
|
|
1248
|
+
exitCode: step.exitCode,
|
|
1249
|
+
durationMs: step.durationMs,
|
|
1250
|
+
error: boundedExcerpt(step.error),
|
|
1251
|
+
blocked: isBlockedStepRun(step)
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
function workflowRunEnvelope(run, steps) {
|
|
1255
|
+
return JSON.stringify({ workflowRun: run, steps: steps.map(summarizeWorkflowStepRun) }, null, 2);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// src/lib/run-receipts.ts
|
|
1259
|
+
var RUN_RECEIPT_SUMMARY_TEXT_CHARS = 4096;
|
|
1260
|
+
var RUN_RECEIPT_MAX_IDS = 100;
|
|
1261
|
+
var RUN_RECEIPT_MAX_EVIDENCE_PATHS = 100;
|
|
1262
|
+
var RUN_RECEIPT_MAX_PATH_CHARS = 1024;
|
|
1263
|
+
function canonicalJson(value) {
|
|
1264
|
+
if (Array.isArray(value))
|
|
1265
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
1266
|
+
if (value && typeof value === "object") {
|
|
1267
|
+
return `{${Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
|
|
1268
|
+
}
|
|
1269
|
+
return JSON.stringify(value);
|
|
1270
|
+
}
|
|
1271
|
+
function digestReceipt(receipt) {
|
|
1272
|
+
return `sha256:${createHash2("sha256").update(canonicalJson(receipt)).digest("hex")}`;
|
|
1273
|
+
}
|
|
1274
|
+
function isoOrNull(value, label) {
|
|
1275
|
+
if (value === undefined || value === null || value === "")
|
|
1276
|
+
return null;
|
|
1277
|
+
const date = new Date(value);
|
|
1278
|
+
if (Number.isNaN(date.getTime()))
|
|
1279
|
+
throw new Error(`${label} must be a valid ISO date/time`);
|
|
1280
|
+
return date.toISOString();
|
|
1281
|
+
}
|
|
1282
|
+
function nonEmpty(value, label) {
|
|
1283
|
+
const trimmed = value?.trim();
|
|
1284
|
+
if (!trimmed)
|
|
1285
|
+
throw new Error(`${label} must be non-empty`);
|
|
1286
|
+
return trimmed;
|
|
1287
|
+
}
|
|
1288
|
+
function normalizedStringArray(values, opts) {
|
|
1289
|
+
const out = [];
|
|
1290
|
+
const seen = new Set;
|
|
1291
|
+
for (const raw of values ?? []) {
|
|
1292
|
+
const value = String(raw).trim();
|
|
1293
|
+
if (!value || seen.has(value))
|
|
1294
|
+
continue;
|
|
1295
|
+
seen.add(value);
|
|
1296
|
+
out.push(opts.itemMax ? value.slice(0, opts.itemMax) : value);
|
|
1297
|
+
if (out.length >= opts.max)
|
|
1298
|
+
break;
|
|
1299
|
+
}
|
|
1300
|
+
if ((values?.length ?? 0) > opts.max) {
|
|
1301
|
+
out.push(`[truncated ${values.length - opts.max} ${opts.label}]`);
|
|
1302
|
+
}
|
|
1303
|
+
return out;
|
|
1304
|
+
}
|
|
1305
|
+
function receiptMachine(input, opts) {
|
|
1306
|
+
if (typeof input.machine === "string")
|
|
1307
|
+
return nonEmpty(input.machine, "machine");
|
|
1308
|
+
if (input.machine && typeof input.machine === "object")
|
|
1309
|
+
return input.machine;
|
|
1310
|
+
if (opts.loop?.machine)
|
|
1311
|
+
return { ...opts.loop.machine };
|
|
1312
|
+
if (typeof opts.defaultMachine === "string")
|
|
1313
|
+
return nonEmpty(opts.defaultMachine, "machine");
|
|
1314
|
+
if (opts.defaultMachine && typeof opts.defaultMachine === "object")
|
|
1315
|
+
return opts.defaultMachine;
|
|
1316
|
+
return hostname();
|
|
1317
|
+
}
|
|
1318
|
+
function normalizeSummary(input, run) {
|
|
1319
|
+
const stdout = input.stdout ?? run?.stdout;
|
|
1320
|
+
const stderr = input.stderr ?? run?.stderr;
|
|
1321
|
+
const output = summarizeOutput(stdout, stderr);
|
|
1322
|
+
const summaryInput = input.summary;
|
|
1323
|
+
const provided = typeof summaryInput === "object" && summaryInput !== null ? summaryInput : {};
|
|
1324
|
+
const text = typeof summaryInput === "string" ? boundedExcerpt(summaryInput, Math.floor(RUN_RECEIPT_SUMMARY_TEXT_CHARS / 2)) : typeof provided.text === "string" ? boundedExcerpt(provided.text, Math.floor(RUN_RECEIPT_SUMMARY_TEXT_CHARS / 2)) : undefined;
|
|
1325
|
+
return {
|
|
1326
|
+
text,
|
|
1327
|
+
stdout_bytes: Number.isFinite(provided.stdout_bytes) ? Number(provided.stdout_bytes) : output.stdoutBytes,
|
|
1328
|
+
stderr_bytes: Number.isFinite(provided.stderr_bytes) ? Number(provided.stderr_bytes) : output.stderrBytes,
|
|
1329
|
+
stdout_excerpt: typeof provided.stdout_excerpt === "string" ? boundedExcerpt(provided.stdout_excerpt) : output.stdoutExcerpt,
|
|
1330
|
+
stderr_excerpt: typeof provided.stderr_excerpt === "string" ? boundedExcerpt(provided.stderr_excerpt) : output.stderrExcerpt,
|
|
1331
|
+
error: typeof provided.error === "string" ? boundedExcerpt(provided.error) : boundedExcerpt(input.error ?? run?.error),
|
|
1332
|
+
duration_ms: Number.isFinite(provided.duration_ms) ? Number(provided.duration_ms) : input.duration_ms ?? run?.durationMs
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
function normalizeRunReceipt(input, opts = {}) {
|
|
1336
|
+
const now = (opts.now ?? new Date).toISOString();
|
|
1337
|
+
const run = opts.run;
|
|
1338
|
+
const loopId = input.loop_id ?? run?.loopId ?? opts.loop?.id;
|
|
1339
|
+
const normalized = {
|
|
1340
|
+
loop_id: nonEmpty(loopId, "loop_id"),
|
|
1341
|
+
run_id: nonEmpty(input.run_id ?? run?.id, "run_id"),
|
|
1342
|
+
machine: receiptMachine(input, opts),
|
|
1343
|
+
repo: nonEmpty(input.repo ?? opts.defaultRepo ?? targetRepo(opts.loop) ?? process.cwd(), "repo"),
|
|
1344
|
+
task_ids: normalizedStringArray(input.task_ids, { max: RUN_RECEIPT_MAX_IDS, label: "task_ids" }),
|
|
1345
|
+
knowledge_ids: normalizedStringArray(input.knowledge_ids, { max: RUN_RECEIPT_MAX_IDS, label: "knowledge_ids" }),
|
|
1346
|
+
started_at: isoOrNull(input.started_at ?? run?.startedAt, "started_at"),
|
|
1347
|
+
finished_at: isoOrNull(input.finished_at ?? run?.finishedAt, "finished_at"),
|
|
1348
|
+
status: nonEmpty(input.status ?? run?.status, "status"),
|
|
1349
|
+
exit_code: input.exit_code === undefined ? run?.exitCode ?? null : input.exit_code,
|
|
1350
|
+
summary: normalizeSummary(input, run),
|
|
1351
|
+
evidence_paths: normalizedStringArray(input.evidence_paths, {
|
|
1352
|
+
max: RUN_RECEIPT_MAX_EVIDENCE_PATHS,
|
|
1353
|
+
label: "evidence_paths",
|
|
1354
|
+
itemMax: RUN_RECEIPT_MAX_PATH_CHARS
|
|
1355
|
+
})
|
|
1356
|
+
};
|
|
1357
|
+
return {
|
|
1358
|
+
...normalized,
|
|
1359
|
+
digest_id: input.digest_id?.trim() || digestReceipt(normalized),
|
|
1360
|
+
created_at: opts.existing?.created_at ?? now,
|
|
1361
|
+
updated_at: now
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
function targetRepo(loop) {
|
|
1365
|
+
if (!loop)
|
|
1366
|
+
return;
|
|
1367
|
+
if (loop.target.type === "command")
|
|
1368
|
+
return loop.target.cwd;
|
|
1369
|
+
if (loop.target.type === "agent")
|
|
1370
|
+
return loop.target.cwd;
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1205
1374
|
// src/lib/route/todos-cli.ts
|
|
1206
1375
|
import { closeSync, mkdtempSync, openSync, readFileSync as readFileSync3, rmSync as rmSync2 } from "fs";
|
|
1207
1376
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
@@ -1273,6 +1442,9 @@ function publicRun(run, showOutput = false, opts = {}) {
|
|
|
1273
1442
|
error: opts.redactError ? redact(run.error) : run.error
|
|
1274
1443
|
};
|
|
1275
1444
|
}
|
|
1445
|
+
function publicRunReceipt(receipt) {
|
|
1446
|
+
return { ...receipt };
|
|
1447
|
+
}
|
|
1276
1448
|
function publicExecutorResult(result, showOutput = false) {
|
|
1277
1449
|
return {
|
|
1278
1450
|
...result,
|
|
@@ -1396,7 +1568,7 @@ function todosMutationSummary(result) {
|
|
|
1396
1568
|
var DEFAULT_RECOVERY_BATCH_LIMIT = 100;
|
|
1397
1569
|
var DEFAULT_RECOVERY_SCAN_MULTIPLIER = 5;
|
|
1398
1570
|
var LIVE_EXPIRED_RUN_GRACE_MS = 60000;
|
|
1399
|
-
var SCHEMA_USER_VERSION =
|
|
1571
|
+
var SCHEMA_USER_VERSION = 8;
|
|
1400
1572
|
var TERMINAL_RUN_STATUSES = ["succeeded", "failed", "timed_out", "abandoned", "skipped"];
|
|
1401
1573
|
var PRUNE_BATCH_SIZE = 400;
|
|
1402
1574
|
var GENERATED_ROUTE_TEMPLATE_IDS = new Set(["todos-task-worker-verifier", "task-lifecycle", "event-worker-verifier"]);
|
|
@@ -1452,6 +1624,25 @@ function rowToRun(row) {
|
|
|
1452
1624
|
updatedAt: row.updated_at
|
|
1453
1625
|
};
|
|
1454
1626
|
}
|
|
1627
|
+
function rowToRunReceipt(row) {
|
|
1628
|
+
return {
|
|
1629
|
+
loop_id: row.loop_id,
|
|
1630
|
+
run_id: row.run_id,
|
|
1631
|
+
machine: JSON.parse(row.machine_json),
|
|
1632
|
+
repo: row.repo,
|
|
1633
|
+
task_ids: JSON.parse(row.task_ids_json),
|
|
1634
|
+
knowledge_ids: JSON.parse(row.knowledge_ids_json),
|
|
1635
|
+
digest_id: row.digest_id,
|
|
1636
|
+
started_at: row.started_at,
|
|
1637
|
+
finished_at: row.finished_at,
|
|
1638
|
+
status: row.status,
|
|
1639
|
+
exit_code: row.exit_code,
|
|
1640
|
+
summary: JSON.parse(row.summary_json),
|
|
1641
|
+
evidence_paths: JSON.parse(row.evidence_paths_json),
|
|
1642
|
+
created_at: row.created_at,
|
|
1643
|
+
updated_at: row.updated_at
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1455
1646
|
function latestRunTime(row) {
|
|
1456
1647
|
return row.finished_at ?? row.started_at ?? row.created_at;
|
|
1457
1648
|
}
|
|
@@ -1515,6 +1706,7 @@ function rowToWorkflowWorkItem(row) {
|
|
|
1515
1706
|
subjectRef: row.subject_ref,
|
|
1516
1707
|
projectKey: row.project_key ?? undefined,
|
|
1517
1708
|
projectGroup: row.project_group ?? undefined,
|
|
1709
|
+
machineId: row.machine_id ?? undefined,
|
|
1518
1710
|
routeScope: row.route_scope ?? undefined,
|
|
1519
1711
|
priority: row.priority,
|
|
1520
1712
|
status: row.status,
|
|
@@ -1672,6 +1864,7 @@ function scrubbedOrNull(value) {
|
|
|
1672
1864
|
return value == null ? null : scrubSecrets(value);
|
|
1673
1865
|
}
|
|
1674
1866
|
var MAX_PERSISTED_RUN_OUTPUT_CHARS = 64 * 1024;
|
|
1867
|
+
var MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS = 64 * 1024;
|
|
1675
1868
|
function clampPersistedRunOutput(value) {
|
|
1676
1869
|
if (value == null || value.length <= MAX_PERSISTED_RUN_OUTPUT_CHARS)
|
|
1677
1870
|
return value;
|
|
@@ -1686,6 +1879,42 @@ ${tail}`;
|
|
|
1686
1879
|
function persistedRunOutput(value) {
|
|
1687
1880
|
return clampPersistedRunOutput(scrubbedOrNull(value));
|
|
1688
1881
|
}
|
|
1882
|
+
function clampTextToChars(value, maxChars, reason) {
|
|
1883
|
+
if (value.length <= maxChars)
|
|
1884
|
+
return value;
|
|
1885
|
+
const marker = `
|
|
1886
|
+
...[truncated by ${reason}]...
|
|
1887
|
+
`;
|
|
1888
|
+
const budget = Math.max(0, maxChars - marker.length);
|
|
1889
|
+
const headLength = Math.ceil(budget / 2);
|
|
1890
|
+
const tailLength = Math.floor(budget / 2);
|
|
1891
|
+
return `${value.slice(0, headLength)}${marker}${value.slice(value.length - tailLength)}`;
|
|
1892
|
+
}
|
|
1893
|
+
function boundedWorkflowEventPayloadJson(scrubbedJson) {
|
|
1894
|
+
if (scrubbedJson.length <= MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS)
|
|
1895
|
+
return scrubbedJson;
|
|
1896
|
+
const base = {
|
|
1897
|
+
truncated: true,
|
|
1898
|
+
originalChars: scrubbedJson.length,
|
|
1899
|
+
maxChars: MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS,
|
|
1900
|
+
preview: ""
|
|
1901
|
+
};
|
|
1902
|
+
const baseChars = JSON.stringify(base).length;
|
|
1903
|
+
let previewBudget = Math.max(0, MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS - baseChars - 64);
|
|
1904
|
+
while (true) {
|
|
1905
|
+
const preview = clampTextToChars(scrubbedJson, previewBudget, "loops workflow-event payload retention");
|
|
1906
|
+
const bounded = JSON.stringify({ ...base, preview });
|
|
1907
|
+
if (bounded.length <= MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS || previewBudget === 0)
|
|
1908
|
+
return bounded;
|
|
1909
|
+
previewBudget = Math.max(0, previewBudget - (bounded.length - MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS) - 64);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
function persistedWorkflowEventPayload(payload) {
|
|
1913
|
+
if (payload == null)
|
|
1914
|
+
return null;
|
|
1915
|
+
const scrubbed = scrubSecretsDeep(payload);
|
|
1916
|
+
return boundedWorkflowEventPayloadJson(scrubSecrets(JSON.stringify(scrubbed)));
|
|
1917
|
+
}
|
|
1689
1918
|
function chmodIfExists(path, mode) {
|
|
1690
1919
|
try {
|
|
1691
1920
|
if (existsSync(path))
|
|
@@ -1803,6 +2032,17 @@ class Store {
|
|
|
1803
2032
|
this.addColumnIfMissing("workflow_work_items", "route_scope", "TEXT");
|
|
1804
2033
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_workflow_work_items_scope ON workflow_work_items(route_scope, status)");
|
|
1805
2034
|
}
|
|
2035
|
+
},
|
|
2036
|
+
{
|
|
2037
|
+
id: "0009_run_receipts",
|
|
2038
|
+
apply: () => this.createRunReceiptsSchema()
|
|
2039
|
+
},
|
|
2040
|
+
{
|
|
2041
|
+
id: "0010_work_item_machine_id",
|
|
2042
|
+
apply: () => {
|
|
2043
|
+
this.addColumnIfMissing("workflow_work_items", "machine_id", "TEXT");
|
|
2044
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_workflow_work_items_machine ON workflow_work_items(machine_id, status)");
|
|
2045
|
+
}
|
|
1806
2046
|
}
|
|
1807
2047
|
];
|
|
1808
2048
|
}
|
|
@@ -1864,6 +2104,28 @@ class Store {
|
|
|
1864
2104
|
CREATE INDEX IF NOT EXISTS idx_runs_status_lease ON loop_runs(status, lease_expires_at);
|
|
1865
2105
|
CREATE INDEX IF NOT EXISTS idx_runs_scheduled ON loop_runs(scheduled_for);
|
|
1866
2106
|
|
|
2107
|
+
CREATE TABLE IF NOT EXISTS run_receipts (
|
|
2108
|
+
run_id TEXT PRIMARY KEY,
|
|
2109
|
+
loop_id TEXT NOT NULL,
|
|
2110
|
+
machine_json TEXT NOT NULL,
|
|
2111
|
+
repo TEXT NOT NULL,
|
|
2112
|
+
task_ids_json TEXT NOT NULL,
|
|
2113
|
+
knowledge_ids_json TEXT NOT NULL,
|
|
2114
|
+
digest_id TEXT NOT NULL,
|
|
2115
|
+
started_at TEXT,
|
|
2116
|
+
finished_at TEXT,
|
|
2117
|
+
status TEXT NOT NULL,
|
|
2118
|
+
exit_code INTEGER,
|
|
2119
|
+
summary_json TEXT NOT NULL,
|
|
2120
|
+
evidence_paths_json TEXT NOT NULL,
|
|
2121
|
+
created_at TEXT NOT NULL,
|
|
2122
|
+
updated_at TEXT NOT NULL
|
|
2123
|
+
);
|
|
2124
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_loop ON run_receipts(loop_id, created_at DESC);
|
|
2125
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_repo ON run_receipts(repo, created_at DESC);
|
|
2126
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_digest ON run_receipts(digest_id);
|
|
2127
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_status ON run_receipts(status, created_at DESC);
|
|
2128
|
+
|
|
1867
2129
|
CREATE TABLE IF NOT EXISTS daemon_lease (
|
|
1868
2130
|
id TEXT PRIMARY KEY,
|
|
1869
2131
|
pid INTEGER NOT NULL,
|
|
@@ -1950,6 +2212,7 @@ class Store {
|
|
|
1950
2212
|
subject_ref TEXT NOT NULL,
|
|
1951
2213
|
project_key TEXT,
|
|
1952
2214
|
project_group TEXT,
|
|
2215
|
+
machine_id TEXT,
|
|
1953
2216
|
route_scope TEXT,
|
|
1954
2217
|
priority INTEGER NOT NULL,
|
|
1955
2218
|
status TEXT NOT NULL,
|
|
@@ -1968,10 +2231,10 @@ class Store {
|
|
|
1968
2231
|
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_project ON workflow_work_items(project_key, status);
|
|
1969
2232
|
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_group ON workflow_work_items(project_group, status);
|
|
1970
2233
|
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_invocation ON workflow_work_items(invocation_id);
|
|
1971
|
-
--
|
|
1972
|
-
--
|
|
2234
|
+
-- New-column indexes (route_scope, machine_id, etc.) are created ONLY by
|
|
2235
|
+
-- their additive migrations, never here: this baseline DDL
|
|
1973
2236
|
-- re-runs on EVERY open (0001 is not skip-guarded), and on a pre-0008
|
|
1974
|
-
-- database the CREATE TABLE above is a no-op, so an index on
|
|
2237
|
+
-- database the CREATE TABLE above is a no-op, so an index on a new column
|
|
1975
2238
|
-- here would execute before the column exists and crash the open
|
|
1976
2239
|
-- ("no such column: route_scope"). New columns may be folded into the
|
|
1977
2240
|
-- CREATE TABLE (fresh-db only); their indexes must live in the migration.
|
|
@@ -2082,6 +2345,31 @@ class Store {
|
|
|
2082
2345
|
CREATE INDEX IF NOT EXISTS idx_goal_runs_workflow_run ON goal_runs(workflow_run_id);
|
|
2083
2346
|
`);
|
|
2084
2347
|
}
|
|
2348
|
+
createRunReceiptsSchema() {
|
|
2349
|
+
this.db.exec(`
|
|
2350
|
+
CREATE TABLE IF NOT EXISTS run_receipts (
|
|
2351
|
+
run_id TEXT PRIMARY KEY,
|
|
2352
|
+
loop_id TEXT NOT NULL,
|
|
2353
|
+
machine_json TEXT NOT NULL,
|
|
2354
|
+
repo TEXT NOT NULL,
|
|
2355
|
+
task_ids_json TEXT NOT NULL,
|
|
2356
|
+
knowledge_ids_json TEXT NOT NULL,
|
|
2357
|
+
digest_id TEXT NOT NULL,
|
|
2358
|
+
started_at TEXT,
|
|
2359
|
+
finished_at TEXT,
|
|
2360
|
+
status TEXT NOT NULL,
|
|
2361
|
+
exit_code INTEGER,
|
|
2362
|
+
summary_json TEXT NOT NULL,
|
|
2363
|
+
evidence_paths_json TEXT NOT NULL,
|
|
2364
|
+
created_at TEXT NOT NULL,
|
|
2365
|
+
updated_at TEXT NOT NULL
|
|
2366
|
+
);
|
|
2367
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_loop ON run_receipts(loop_id, created_at DESC);
|
|
2368
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_repo ON run_receipts(repo, created_at DESC);
|
|
2369
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_digest ON run_receipts(digest_id);
|
|
2370
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_status ON run_receipts(status, created_at DESC);
|
|
2371
|
+
`);
|
|
2372
|
+
}
|
|
2085
2373
|
addColumnIfMissing(table, column, definition) {
|
|
2086
2374
|
const columns = this.db.query(`PRAGMA table_info(${table})`).all();
|
|
2087
2375
|
if (columns.some((c) => c.name === column))
|
|
@@ -2193,19 +2481,24 @@ class Store {
|
|
|
2193
2481
|
}
|
|
2194
2482
|
listLoops(opts = {}) {
|
|
2195
2483
|
const limit = opts.limit ?? 200;
|
|
2484
|
+
const offset = Math.max(0, Math.floor(opts.offset ?? 0));
|
|
2485
|
+
if (opts.name != null) {
|
|
2486
|
+
const rows2 = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT ? OFFSET ?").all(opts.name, limit, offset);
|
|
2487
|
+
return this.withLatestRunSummaries(rows2.map(rowToLoop));
|
|
2488
|
+
}
|
|
2196
2489
|
let rows;
|
|
2197
2490
|
if (opts.status && opts.archived) {
|
|
2198
|
-
rows = this.db.query("SELECT * FROM loops WHERE status = ? AND archived_at IS NOT NULL ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit);
|
|
2491
|
+
rows = this.db.query("SELECT * FROM loops WHERE status = ? AND archived_at IS NOT NULL ORDER BY next_run_at ASC LIMIT ? OFFSET ?").all(opts.status, limit, offset);
|
|
2199
2492
|
} else if (opts.status && opts.includeArchived) {
|
|
2200
|
-
rows = this.db.query("SELECT * FROM loops WHERE status = ? ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit);
|
|
2493
|
+
rows = this.db.query("SELECT * FROM loops WHERE status = ? ORDER BY next_run_at ASC LIMIT ? OFFSET ?").all(opts.status, limit, offset);
|
|
2201
2494
|
} else if (opts.status) {
|
|
2202
|
-
rows = this.db.query("SELECT * FROM loops WHERE status = ? AND archived_at IS NULL ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit);
|
|
2495
|
+
rows = this.db.query("SELECT * FROM loops WHERE status = ? AND archived_at IS NULL ORDER BY next_run_at ASC LIMIT ? OFFSET ?").all(opts.status, limit, offset);
|
|
2203
2496
|
} else if (opts.archived) {
|
|
2204
|
-
rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NOT NULL ORDER BY archived_at DESC LIMIT ?").all(limit);
|
|
2497
|
+
rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NOT NULL ORDER BY archived_at DESC LIMIT ? OFFSET ?").all(limit, offset);
|
|
2205
2498
|
} else if (opts.includeArchived) {
|
|
2206
|
-
rows = this.db.query("SELECT * FROM loops ORDER BY status ASC, next_run_at ASC LIMIT ?").all(limit);
|
|
2499
|
+
rows = this.db.query("SELECT * FROM loops ORDER BY status ASC, next_run_at ASC LIMIT ? OFFSET ?").all(limit, offset);
|
|
2207
2500
|
} else {
|
|
2208
|
-
rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NULL ORDER BY status ASC, next_run_at ASC LIMIT ?").all(limit);
|
|
2501
|
+
rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NULL ORDER BY status ASC, next_run_at ASC LIMIT ? OFFSET ?").all(limit, offset);
|
|
2209
2502
|
}
|
|
2210
2503
|
return this.withLatestRunSummaries(rows.map(rowToLoop));
|
|
2211
2504
|
}
|
|
@@ -2883,10 +3176,10 @@ class Store {
|
|
|
2883
3176
|
const id = genId();
|
|
2884
3177
|
const status = input.status ?? "queued";
|
|
2885
3178
|
this.db.query(`INSERT INTO workflow_work_items (id, route_key, idempotency_key, invocation_id, source_type, source_ref,
|
|
2886
|
-
subject_ref, project_key, project_group, route_scope, priority, status, attempts, next_attempt_at, lease_expires_at,
|
|
3179
|
+
subject_ref, project_key, project_group, machine_id, route_scope, priority, status, attempts, next_attempt_at, lease_expires_at,
|
|
2887
3180
|
workflow_id, loop_id, workflow_run_id, last_reason, created_at, updated_at)
|
|
2888
3181
|
VALUES ($id, $routeKey, $idempotencyKey, $invocationId, $sourceType, $sourceRef, $subjectRef,
|
|
2889
|
-
$projectKey, $projectGroup, $routeScope, $priority, $status, 0, $nextAttemptAt, NULL, NULL, NULL, NULL,
|
|
3182
|
+
$projectKey, $projectGroup, $machineId, $routeScope, $priority, $status, 0, $nextAttemptAt, NULL, NULL, NULL, NULL,
|
|
2890
3183
|
$lastReason, $created, $updated)
|
|
2891
3184
|
ON CONFLICT(route_key, idempotency_key) DO UPDATE SET
|
|
2892
3185
|
invocation_id=excluded.invocation_id,
|
|
@@ -2895,6 +3188,10 @@ class Store {
|
|
|
2895
3188
|
subject_ref=excluded.subject_ref,
|
|
2896
3189
|
project_key=excluded.project_key,
|
|
2897
3190
|
project_group=excluded.project_group,
|
|
3191
|
+
machine_id=CASE
|
|
3192
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.machine_id
|
|
3193
|
+
ELSE excluded.machine_id
|
|
3194
|
+
END,
|
|
2898
3195
|
route_scope=excluded.route_scope,
|
|
2899
3196
|
priority=excluded.priority,
|
|
2900
3197
|
status=CASE
|
|
@@ -2937,6 +3234,7 @@ class Store {
|
|
|
2937
3234
|
$subjectRef: input.subjectRef,
|
|
2938
3235
|
$projectKey: input.projectKey ?? null,
|
|
2939
3236
|
$projectGroup: input.projectGroup ?? null,
|
|
3237
|
+
$machineId: input.machineId ?? null,
|
|
2940
3238
|
$routeScope: input.routeScope ?? null,
|
|
2941
3239
|
$priority: input.priority ?? 0,
|
|
2942
3240
|
$status: status,
|
|
@@ -3446,7 +3744,7 @@ class Store {
|
|
|
3446
3744
|
VALUES ($id, $workflowRunId, 1, 'created', NULL, $payload, $created)`).run({
|
|
3447
3745
|
$id: genId(),
|
|
3448
3746
|
$workflowRunId: runId,
|
|
3449
|
-
$payload:
|
|
3747
|
+
$payload: persistedWorkflowEventPayload({
|
|
3450
3748
|
workflowId: input.workflow.id,
|
|
3451
3749
|
workflowName: input.workflow.name,
|
|
3452
3750
|
stepCount: input.workflow.steps.length,
|
|
@@ -3777,7 +4075,7 @@ class Store {
|
|
|
3777
4075
|
$sequence: sequence,
|
|
3778
4076
|
$eventType: eventType,
|
|
3779
4077
|
$stepId: stepId ?? null,
|
|
3780
|
-
$payload:
|
|
4078
|
+
$payload: persistedWorkflowEventPayload(payload),
|
|
3781
4079
|
$created: now
|
|
3782
4080
|
});
|
|
3783
4081
|
const event = this.db.query("SELECT * FROM workflow_events WHERE id = ?").get(id);
|
|
@@ -4100,18 +4398,93 @@ class Store {
|
|
|
4100
4398
|
}
|
|
4101
4399
|
listRuns(opts = {}) {
|
|
4102
4400
|
const limit = opts.limit ?? 100;
|
|
4401
|
+
const offset = Math.max(0, Math.floor(opts.offset ?? 0));
|
|
4103
4402
|
let rows;
|
|
4104
4403
|
if (opts.loopId && opts.status) {
|
|
4105
|
-
rows = this.db.query("SELECT * FROM loop_runs WHERE loop_id = ? AND status = ? ORDER BY created_at DESC LIMIT ?").all(opts.loopId, opts.status, limit);
|
|
4404
|
+
rows = this.db.query("SELECT * FROM loop_runs WHERE loop_id = ? AND status = ? ORDER BY created_at DESC LIMIT ? OFFSET ?").all(opts.loopId, opts.status, limit, offset);
|
|
4106
4405
|
} else if (opts.loopId) {
|
|
4107
|
-
rows = this.db.query("SELECT * FROM loop_runs WHERE loop_id = ? ORDER BY created_at DESC LIMIT ?").all(opts.loopId, limit);
|
|
4406
|
+
rows = this.db.query("SELECT * FROM loop_runs WHERE loop_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?").all(opts.loopId, limit, offset);
|
|
4108
4407
|
} else if (opts.status) {
|
|
4109
|
-
rows = this.db.query("SELECT * FROM loop_runs WHERE status = ? ORDER BY created_at DESC LIMIT ?").all(opts.status, limit);
|
|
4408
|
+
rows = this.db.query("SELECT * FROM loop_runs WHERE status = ? ORDER BY created_at DESC LIMIT ? OFFSET ?").all(opts.status, limit, offset);
|
|
4110
4409
|
} else {
|
|
4111
|
-
rows = this.db.query("SELECT * FROM loop_runs ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
4410
|
+
rows = this.db.query("SELECT * FROM loop_runs ORDER BY created_at DESC LIMIT ? OFFSET ?").all(limit, offset);
|
|
4112
4411
|
}
|
|
4113
4412
|
return rows.map(rowToRun);
|
|
4114
4413
|
}
|
|
4414
|
+
writeRunReceipt(input, opts = {}) {
|
|
4415
|
+
const inputRunId = typeof input.run_id === "string" && input.run_id.trim() ? input.run_id : undefined;
|
|
4416
|
+
const existing = inputRunId ? this.getRunReceipt(inputRunId) : undefined;
|
|
4417
|
+
const run = inputRunId ? this.getRun(inputRunId) : undefined;
|
|
4418
|
+
const loop = input.loop_id ? this.getLoop(input.loop_id) : run ? this.getLoop(run.loopId) : undefined;
|
|
4419
|
+
const receipt = normalizeRunReceipt(input, { now: opts.now, run, loop, existing });
|
|
4420
|
+
this.db.query(`INSERT INTO run_receipts (run_id, loop_id, machine_json, repo, task_ids_json, knowledge_ids_json, digest_id,
|
|
4421
|
+
started_at, finished_at, status, exit_code, summary_json, evidence_paths_json, created_at, updated_at)
|
|
4422
|
+
VALUES ($runId, $loopId, $machineJson, $repo, $taskIdsJson, $knowledgeIdsJson, $digestId,
|
|
4423
|
+
$startedAt, $finishedAt, $status, $exitCode, $summaryJson, $evidencePathsJson, $createdAt, $updatedAt)
|
|
4424
|
+
ON CONFLICT(run_id) DO UPDATE SET
|
|
4425
|
+
loop_id=excluded.loop_id,
|
|
4426
|
+
machine_json=excluded.machine_json,
|
|
4427
|
+
repo=excluded.repo,
|
|
4428
|
+
task_ids_json=excluded.task_ids_json,
|
|
4429
|
+
knowledge_ids_json=excluded.knowledge_ids_json,
|
|
4430
|
+
digest_id=excluded.digest_id,
|
|
4431
|
+
started_at=excluded.started_at,
|
|
4432
|
+
finished_at=excluded.finished_at,
|
|
4433
|
+
status=excluded.status,
|
|
4434
|
+
exit_code=excluded.exit_code,
|
|
4435
|
+
summary_json=excluded.summary_json,
|
|
4436
|
+
evidence_paths_json=excluded.evidence_paths_json,
|
|
4437
|
+
updated_at=excluded.updated_at`).run({
|
|
4438
|
+
$runId: receipt.run_id,
|
|
4439
|
+
$loopId: receipt.loop_id,
|
|
4440
|
+
$machineJson: JSON.stringify(receipt.machine),
|
|
4441
|
+
$repo: receipt.repo,
|
|
4442
|
+
$taskIdsJson: JSON.stringify(receipt.task_ids),
|
|
4443
|
+
$knowledgeIdsJson: JSON.stringify(receipt.knowledge_ids),
|
|
4444
|
+
$digestId: receipt.digest_id,
|
|
4445
|
+
$startedAt: receipt.started_at,
|
|
4446
|
+
$finishedAt: receipt.finished_at,
|
|
4447
|
+
$status: receipt.status,
|
|
4448
|
+
$exitCode: receipt.exit_code,
|
|
4449
|
+
$summaryJson: JSON.stringify(receipt.summary),
|
|
4450
|
+
$evidencePathsJson: JSON.stringify(receipt.evidence_paths),
|
|
4451
|
+
$createdAt: receipt.created_at,
|
|
4452
|
+
$updatedAt: receipt.updated_at
|
|
4453
|
+
});
|
|
4454
|
+
return this.getRunReceipt(receipt.run_id) ?? receipt;
|
|
4455
|
+
}
|
|
4456
|
+
getRunReceipt(runId) {
|
|
4457
|
+
const row = this.db.query("SELECT * FROM run_receipts WHERE run_id = ?").get(runId);
|
|
4458
|
+
return row ? rowToRunReceipt(row) : undefined;
|
|
4459
|
+
}
|
|
4460
|
+
listRunReceipts(opts = {}) {
|
|
4461
|
+
const limit = opts.limit ?? 100;
|
|
4462
|
+
const filters = [];
|
|
4463
|
+
const params = [];
|
|
4464
|
+
if (opts.loopId) {
|
|
4465
|
+
filters.push("loop_id = ?");
|
|
4466
|
+
params.push(opts.loopId);
|
|
4467
|
+
}
|
|
4468
|
+
if (opts.repo) {
|
|
4469
|
+
filters.push("repo = ?");
|
|
4470
|
+
params.push(opts.repo);
|
|
4471
|
+
}
|
|
4472
|
+
if (opts.status) {
|
|
4473
|
+
filters.push("status = ?");
|
|
4474
|
+
params.push(opts.status);
|
|
4475
|
+
}
|
|
4476
|
+
if (opts.taskId) {
|
|
4477
|
+
filters.push("EXISTS (SELECT 1 FROM json_each(run_receipts.task_ids_json) WHERE value = ?)");
|
|
4478
|
+
params.push(opts.taskId);
|
|
4479
|
+
}
|
|
4480
|
+
if (opts.knowledgeId) {
|
|
4481
|
+
filters.push("EXISTS (SELECT 1 FROM json_each(run_receipts.knowledge_ids_json) WHERE value = ?)");
|
|
4482
|
+
params.push(opts.knowledgeId);
|
|
4483
|
+
}
|
|
4484
|
+
const where = filters.length ? `WHERE ${filters.join(" AND ")}` : "";
|
|
4485
|
+
const rows = this.db.query(`SELECT * FROM run_receipts ${where} ORDER BY created_at DESC LIMIT ?`).all(...params, limit);
|
|
4486
|
+
return rows.map(rowToRunReceipt);
|
|
4487
|
+
}
|
|
4115
4488
|
deferLiveExpiredRun(id, now, opts = {}) {
|
|
4116
4489
|
const updated = now.toISOString();
|
|
4117
4490
|
const deferredUntil = new Date(now.getTime() + LIVE_EXPIRED_RUN_GRACE_MS).toISOString();
|
|
@@ -4270,6 +4643,9 @@ class Store {
|
|
|
4270
4643
|
const runs = includeRuns ? this.db.query("SELECT * FROM loop_runs ORDER BY created_at ASC, id ASC").all().map(rowToRun) : [];
|
|
4271
4644
|
return { schemaVersion: SCHEMA_USER_VERSION, workflows, loops, runs, checks: this.migrationChecks() };
|
|
4272
4645
|
}
|
|
4646
|
+
exportMigrationRunPage(opts) {
|
|
4647
|
+
return this.db.query("SELECT * FROM loop_runs ORDER BY created_at ASC, id ASC LIMIT ? OFFSET ?").all(opts.limit, opts.offset).map(rowToRun);
|
|
4648
|
+
}
|
|
4273
4649
|
countTable(table) {
|
|
4274
4650
|
const row = this.db.query(`SELECT COUNT(*) AS count FROM ${table}`).get();
|
|
4275
4651
|
return row?.count ?? 0;
|
|
@@ -4593,12 +4969,12 @@ class Store {
|
|
|
4593
4969
|
}
|
|
4594
4970
|
|
|
4595
4971
|
// src/lib/storage/postgres-schema.ts
|
|
4596
|
-
import { createHash as
|
|
4972
|
+
import { createHash as createHash3 } from "crypto";
|
|
4597
4973
|
var POSTGRES_MIGRATION_LEDGER_TABLE = "open_loops_schema_migrations";
|
|
4598
4974
|
function checksumStorageSql(sql) {
|
|
4599
4975
|
const normalized = sql.trim().replace(/\r\n/g, `
|
|
4600
4976
|
`);
|
|
4601
|
-
return `sha256:${
|
|
4977
|
+
return `sha256:${createHash3("sha256").update(normalized).digest("hex")}`;
|
|
4602
4978
|
}
|
|
4603
4979
|
function migration(id, sql) {
|
|
4604
4980
|
return Object.freeze({ id, sql: sql.trim(), checksum: checksumStorageSql(sql) });
|
|
@@ -4916,6 +5292,35 @@ CREATE INDEX IF NOT EXISTS idx_audit_events_action ON audit_events(action, creat
|
|
|
4916
5292
|
migration("0004_work_item_route_scope", `
|
|
4917
5293
|
ALTER TABLE workflow_work_items ADD COLUMN IF NOT EXISTS route_scope TEXT;
|
|
4918
5294
|
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_scope ON workflow_work_items(route_scope, status);
|
|
5295
|
+
`),
|
|
5296
|
+
migration("0005_run_receipts", `
|
|
5297
|
+
CREATE TABLE IF NOT EXISTS run_receipts (
|
|
5298
|
+
run_id TEXT PRIMARY KEY,
|
|
5299
|
+
loop_id TEXT NOT NULL,
|
|
5300
|
+
machine_json JSONB NOT NULL,
|
|
5301
|
+
repo TEXT NOT NULL,
|
|
5302
|
+
task_ids_json JSONB NOT NULL,
|
|
5303
|
+
knowledge_ids_json JSONB NOT NULL,
|
|
5304
|
+
digest_id TEXT NOT NULL,
|
|
5305
|
+
started_at TIMESTAMPTZ,
|
|
5306
|
+
finished_at TIMESTAMPTZ,
|
|
5307
|
+
status TEXT NOT NULL,
|
|
5308
|
+
exit_code INTEGER,
|
|
5309
|
+
summary_json JSONB NOT NULL,
|
|
5310
|
+
evidence_paths_json JSONB NOT NULL,
|
|
5311
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
5312
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
5313
|
+
);
|
|
5314
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_loop ON run_receipts(loop_id, created_at DESC);
|
|
5315
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_repo ON run_receipts(repo, created_at DESC);
|
|
5316
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_digest ON run_receipts(digest_id);
|
|
5317
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_status ON run_receipts(status, created_at DESC);
|
|
5318
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_task_ids ON run_receipts USING GIN (task_ids_json);
|
|
5319
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_knowledge_ids ON run_receipts USING GIN (knowledge_ids_json);
|
|
5320
|
+
`),
|
|
5321
|
+
migration("0006_work_item_machine_id", `
|
|
5322
|
+
ALTER TABLE workflow_work_items ADD COLUMN IF NOT EXISTS machine_id TEXT;
|
|
5323
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_machine ON workflow_work_items(machine_id, status);
|
|
4919
5324
|
`)
|
|
4920
5325
|
]);
|
|
4921
5326
|
|
|
@@ -5062,6 +5467,15 @@ class SqliteLoopStorage {
|
|
|
5062
5467
|
deleteLoop(...args) {
|
|
5063
5468
|
return this.call("deleteLoop", ...args);
|
|
5064
5469
|
}
|
|
5470
|
+
upsertMigrationLoop(...args) {
|
|
5471
|
+
return this.call("upsertMigrationLoop", ...args);
|
|
5472
|
+
}
|
|
5473
|
+
upsertMigrationRun(...args) {
|
|
5474
|
+
return this.call("upsertMigrationRun", ...args);
|
|
5475
|
+
}
|
|
5476
|
+
upsertMigrationWorkflow(...args) {
|
|
5477
|
+
return this.call("upsertMigrationWorkflow", ...args);
|
|
5478
|
+
}
|
|
5065
5479
|
createWorkflow(...args) {
|
|
5066
5480
|
return this.call("createWorkflow", ...args);
|
|
5067
5481
|
}
|
|
@@ -5185,6 +5599,15 @@ class SqliteLoopStorage {
|
|
|
5185
5599
|
listRuns(...args) {
|
|
5186
5600
|
return this.call("listRuns", ...args);
|
|
5187
5601
|
}
|
|
5602
|
+
writeRunReceipt(...args) {
|
|
5603
|
+
return this.call("writeRunReceipt", ...args);
|
|
5604
|
+
}
|
|
5605
|
+
getRunReceipt(...args) {
|
|
5606
|
+
return this.call("getRunReceipt", ...args);
|
|
5607
|
+
}
|
|
5608
|
+
listRunReceipts(...args) {
|
|
5609
|
+
return this.call("listRunReceipts", ...args);
|
|
5610
|
+
}
|
|
5188
5611
|
recoverExpiredRunLeases(...args) {
|
|
5189
5612
|
return this.call("recoverExpiredRunLeases", ...args);
|
|
5190
5613
|
}
|
|
@@ -5234,6 +5657,9 @@ class NotImplementedError extends Error {
|
|
|
5234
5657
|
}
|
|
5235
5658
|
var TERMINAL_RUN_STATUSES2 = ["succeeded", "failed", "timed_out", "abandoned", "skipped"];
|
|
5236
5659
|
var PRUNE_BATCH_SIZE2 = 400;
|
|
5660
|
+
function isUniqueViolation(error) {
|
|
5661
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "23505";
|
|
5662
|
+
}
|
|
5237
5663
|
var DEFAULT_RECOVERY_BATCH_LIMIT2 = 100;
|
|
5238
5664
|
var DEFAULT_RECOVERY_SCAN_MULTIPLIER2 = 5;
|
|
5239
5665
|
|
|
@@ -5365,19 +5791,22 @@ class PostgresLoopStorage {
|
|
|
5365
5791
|
async listLoops(...args) {
|
|
5366
5792
|
const opts = args[0] ?? {};
|
|
5367
5793
|
const limit = opts.limit ?? 200;
|
|
5794
|
+
const offset = Math.max(0, Math.floor(opts.offset ?? 0));
|
|
5368
5795
|
let rows;
|
|
5369
|
-
if (opts.
|
|
5370
|
-
rows = await this.client.many("SELECT * FROM loops WHERE
|
|
5796
|
+
if (opts.name != null) {
|
|
5797
|
+
rows = await this.client.many("SELECT * FROM loops WHERE name = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3", [opts.name, limit, offset]);
|
|
5798
|
+
} else if (opts.status && opts.archived) {
|
|
5799
|
+
rows = await this.client.many("SELECT * FROM loops WHERE status = $1 AND archived_at IS NOT NULL ORDER BY next_run_at ASC LIMIT $2 OFFSET $3", [opts.status, limit, offset]);
|
|
5371
5800
|
} else if (opts.status && opts.includeArchived) {
|
|
5372
|
-
rows = await this.client.many("SELECT * FROM loops WHERE status = $1 ORDER BY next_run_at ASC LIMIT $2", [opts.status, limit]);
|
|
5801
|
+
rows = await this.client.many("SELECT * FROM loops WHERE status = $1 ORDER BY next_run_at ASC LIMIT $2 OFFSET $3", [opts.status, limit, offset]);
|
|
5373
5802
|
} else if (opts.status) {
|
|
5374
|
-
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]);
|
|
5803
|
+
rows = await this.client.many("SELECT * FROM loops WHERE status = $1 AND archived_at IS NULL ORDER BY next_run_at ASC LIMIT $2 OFFSET $3", [opts.status, limit, offset]);
|
|
5375
5804
|
} else if (opts.archived) {
|
|
5376
|
-
rows = await this.client.many("SELECT * FROM loops WHERE archived_at IS NOT NULL ORDER BY archived_at DESC LIMIT $1", [limit]);
|
|
5805
|
+
rows = await this.client.many("SELECT * FROM loops WHERE archived_at IS NOT NULL ORDER BY archived_at DESC LIMIT $1 OFFSET $2", [limit, offset]);
|
|
5377
5806
|
} else if (opts.includeArchived) {
|
|
5378
|
-
rows = await this.client.many("SELECT * FROM loops ORDER BY status ASC, next_run_at ASC LIMIT $1", [limit]);
|
|
5807
|
+
rows = await this.client.many("SELECT * FROM loops ORDER BY status ASC, next_run_at ASC LIMIT $1 OFFSET $2", [limit, offset]);
|
|
5379
5808
|
} else {
|
|
5380
|
-
rows = await this.client.many("SELECT * FROM loops WHERE archived_at IS NULL ORDER BY status ASC, next_run_at ASC LIMIT $1", [limit]);
|
|
5809
|
+
rows = await this.client.many("SELECT * FROM loops WHERE archived_at IS NULL ORDER BY status ASC, next_run_at ASC LIMIT $1 OFFSET $2", [limit, offset]);
|
|
5381
5810
|
}
|
|
5382
5811
|
return rows.map(rowToLoop);
|
|
5383
5812
|
}
|
|
@@ -5508,6 +5937,172 @@ class PostgresLoopStorage {
|
|
|
5508
5937
|
const row = await this.client.get(sql, params);
|
|
5509
5938
|
return row?.count ?? 0;
|
|
5510
5939
|
}
|
|
5940
|
+
async upsertMigrationWorkflow(...args) {
|
|
5941
|
+
const [workflow, opts = {}] = args;
|
|
5942
|
+
const existing = await this.getWorkflow(workflow.id);
|
|
5943
|
+
if (existing && !opts.replace)
|
|
5944
|
+
return existing;
|
|
5945
|
+
try {
|
|
5946
|
+
await this.client.execute(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
|
|
5947
|
+
VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7::jsonb,$8,$9)
|
|
5948
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
5949
|
+
name=EXCLUDED.name,
|
|
5950
|
+
description=EXCLUDED.description,
|
|
5951
|
+
version=EXCLUDED.version,
|
|
5952
|
+
status=EXCLUDED.status,
|
|
5953
|
+
goal_json=EXCLUDED.goal_json,
|
|
5954
|
+
steps_json=EXCLUDED.steps_json,
|
|
5955
|
+
created_at=EXCLUDED.created_at,
|
|
5956
|
+
updated_at=EXCLUDED.updated_at`, [
|
|
5957
|
+
workflow.id,
|
|
5958
|
+
workflow.name,
|
|
5959
|
+
workflow.description ?? null,
|
|
5960
|
+
workflow.version,
|
|
5961
|
+
workflow.status,
|
|
5962
|
+
workflow.goal ? JSON.stringify(workflow.goal) : null,
|
|
5963
|
+
JSON.stringify(workflow.steps),
|
|
5964
|
+
workflow.createdAt,
|
|
5965
|
+
workflow.updatedAt
|
|
5966
|
+
]);
|
|
5967
|
+
} catch (error) {
|
|
5968
|
+
if (isUniqueViolation(error)) {
|
|
5969
|
+
const owner = await this.client.get("SELECT * FROM workflow_specs WHERE name = $1 AND status = 'active' LIMIT 1", [workflow.name]);
|
|
5970
|
+
if (owner)
|
|
5971
|
+
return rowToWorkflow(owner);
|
|
5972
|
+
}
|
|
5973
|
+
throw error;
|
|
5974
|
+
}
|
|
5975
|
+
const imported = await this.getWorkflow(workflow.id);
|
|
5976
|
+
if (!imported)
|
|
5977
|
+
throw new Error(`workflow not found after migration import: ${workflow.id}`);
|
|
5978
|
+
return imported;
|
|
5979
|
+
}
|
|
5980
|
+
async upsertMigrationLoop(...args) {
|
|
5981
|
+
const [loop, opts = {}] = args;
|
|
5982
|
+
const existing = await this.loadLoop(this.client, loop.id);
|
|
5983
|
+
if (existing && !opts.replace)
|
|
5984
|
+
return existing;
|
|
5985
|
+
await this.client.execute(`INSERT INTO loops (id, name, description, status, archived_at, archived_from_status, schedule_json, target_json,
|
|
5986
|
+
goal_json, machine_json, next_run_at, retry_scheduled_for, catch_up, catch_up_limit, overlap, max_attempts,
|
|
5987
|
+
retry_delay_ms, lease_ms, expires_at, created_at, updated_at)
|
|
5988
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb,$8::jsonb,$9::jsonb,$10::jsonb,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21)
|
|
5989
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
5990
|
+
name=EXCLUDED.name,
|
|
5991
|
+
description=EXCLUDED.description,
|
|
5992
|
+
status=EXCLUDED.status,
|
|
5993
|
+
archived_at=EXCLUDED.archived_at,
|
|
5994
|
+
archived_from_status=EXCLUDED.archived_from_status,
|
|
5995
|
+
schedule_json=EXCLUDED.schedule_json,
|
|
5996
|
+
target_json=EXCLUDED.target_json,
|
|
5997
|
+
goal_json=EXCLUDED.goal_json,
|
|
5998
|
+
machine_json=EXCLUDED.machine_json,
|
|
5999
|
+
next_run_at=EXCLUDED.next_run_at,
|
|
6000
|
+
retry_scheduled_for=EXCLUDED.retry_scheduled_for,
|
|
6001
|
+
catch_up=EXCLUDED.catch_up,
|
|
6002
|
+
catch_up_limit=EXCLUDED.catch_up_limit,
|
|
6003
|
+
overlap=EXCLUDED.overlap,
|
|
6004
|
+
max_attempts=EXCLUDED.max_attempts,
|
|
6005
|
+
retry_delay_ms=EXCLUDED.retry_delay_ms,
|
|
6006
|
+
lease_ms=EXCLUDED.lease_ms,
|
|
6007
|
+
expires_at=EXCLUDED.expires_at,
|
|
6008
|
+
created_at=EXCLUDED.created_at,
|
|
6009
|
+
updated_at=EXCLUDED.updated_at`, [
|
|
6010
|
+
loop.id,
|
|
6011
|
+
loop.name,
|
|
6012
|
+
loop.description ?? null,
|
|
6013
|
+
loop.status,
|
|
6014
|
+
loop.archivedAt ?? null,
|
|
6015
|
+
loop.archivedFromStatus ?? null,
|
|
6016
|
+
JSON.stringify(loop.schedule),
|
|
6017
|
+
JSON.stringify(loop.target),
|
|
6018
|
+
loop.goal ? JSON.stringify(loop.goal) : null,
|
|
6019
|
+
loop.machine ? JSON.stringify(loop.machine) : null,
|
|
6020
|
+
loop.nextRunAt ?? null,
|
|
6021
|
+
loop.retryScheduledFor ?? null,
|
|
6022
|
+
loop.catchUp,
|
|
6023
|
+
loop.catchUpLimit,
|
|
6024
|
+
loop.overlap,
|
|
6025
|
+
loop.maxAttempts,
|
|
6026
|
+
loop.retryDelayMs,
|
|
6027
|
+
loop.leaseMs,
|
|
6028
|
+
loop.expiresAt ?? null,
|
|
6029
|
+
loop.createdAt,
|
|
6030
|
+
loop.updatedAt
|
|
6031
|
+
]);
|
|
6032
|
+
const imported = await this.loadLoop(this.client, loop.id);
|
|
6033
|
+
if (!imported)
|
|
6034
|
+
throw new Error(`loop not found after migration import: ${loop.id}`);
|
|
6035
|
+
return imported;
|
|
6036
|
+
}
|
|
6037
|
+
async upsertMigrationRun(...args) {
|
|
6038
|
+
const [run, opts = {}] = args;
|
|
6039
|
+
if (run.status === "running")
|
|
6040
|
+
throw new ValidationError(`cannot import running run ${run.id}`);
|
|
6041
|
+
const existing = await this.loadRun(this.client, run.id);
|
|
6042
|
+
if (existing && !opts.replace)
|
|
6043
|
+
return existing;
|
|
6044
|
+
try {
|
|
6045
|
+
await this.client.execute(`INSERT INTO loop_runs (id, loop_id, loop_name, scheduled_for, attempt, status, started_at, finished_at,
|
|
6046
|
+
claimed_by, claim_token, lease_expires_at, pid, pgid, process_started_at, exit_code, duration_ms,
|
|
6047
|
+
stdout, stderr, error, goal_run_id, created_at, updated_at)
|
|
6048
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,NULL,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21)
|
|
6049
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
6050
|
+
loop_id=EXCLUDED.loop_id,
|
|
6051
|
+
loop_name=EXCLUDED.loop_name,
|
|
6052
|
+
scheduled_for=EXCLUDED.scheduled_for,
|
|
6053
|
+
attempt=EXCLUDED.attempt,
|
|
6054
|
+
status=EXCLUDED.status,
|
|
6055
|
+
started_at=EXCLUDED.started_at,
|
|
6056
|
+
finished_at=EXCLUDED.finished_at,
|
|
6057
|
+
claimed_by=EXCLUDED.claimed_by,
|
|
6058
|
+
claim_token=NULL,
|
|
6059
|
+
lease_expires_at=EXCLUDED.lease_expires_at,
|
|
6060
|
+
pid=EXCLUDED.pid,
|
|
6061
|
+
pgid=EXCLUDED.pgid,
|
|
6062
|
+
process_started_at=EXCLUDED.process_started_at,
|
|
6063
|
+
exit_code=EXCLUDED.exit_code,
|
|
6064
|
+
duration_ms=EXCLUDED.duration_ms,
|
|
6065
|
+
stdout=EXCLUDED.stdout,
|
|
6066
|
+
stderr=EXCLUDED.stderr,
|
|
6067
|
+
error=EXCLUDED.error,
|
|
6068
|
+
goal_run_id=EXCLUDED.goal_run_id,
|
|
6069
|
+
created_at=EXCLUDED.created_at,
|
|
6070
|
+
updated_at=EXCLUDED.updated_at`, [
|
|
6071
|
+
run.id,
|
|
6072
|
+
run.loopId,
|
|
6073
|
+
run.loopName,
|
|
6074
|
+
run.scheduledFor,
|
|
6075
|
+
run.attempt,
|
|
6076
|
+
run.status,
|
|
6077
|
+
run.startedAt ?? null,
|
|
6078
|
+
run.finishedAt ?? null,
|
|
6079
|
+
run.claimedBy ?? null,
|
|
6080
|
+
run.leaseExpiresAt ?? null,
|
|
6081
|
+
run.pid ?? null,
|
|
6082
|
+
run.pgid ?? null,
|
|
6083
|
+
run.processStartedAt ?? null,
|
|
6084
|
+
run.exitCode ?? null,
|
|
6085
|
+
run.durationMs ?? null,
|
|
6086
|
+
persistedRunOutput(run.stdout),
|
|
6087
|
+
persistedRunOutput(run.stderr),
|
|
6088
|
+
scrubbedOrNull(run.error),
|
|
6089
|
+
run.goalRunId ?? null,
|
|
6090
|
+
run.createdAt,
|
|
6091
|
+
run.updatedAt
|
|
6092
|
+
]);
|
|
6093
|
+
} catch (error) {
|
|
6094
|
+
if (isUniqueViolation(error)) {
|
|
6095
|
+
const slot = await this.loadRunBySlot(this.client, run.loopId, run.scheduledFor);
|
|
6096
|
+
if (slot)
|
|
6097
|
+
return slot;
|
|
6098
|
+
}
|
|
6099
|
+
throw error;
|
|
6100
|
+
}
|
|
6101
|
+
const imported = await this.loadRun(this.client, run.id);
|
|
6102
|
+
if (!imported)
|
|
6103
|
+
throw new Error(`run not found after migration import: ${run.id}`);
|
|
6104
|
+
return imported;
|
|
6105
|
+
}
|
|
5511
6106
|
async createSkippedRun(...args) {
|
|
5512
6107
|
const [loop, scheduledFor, reason, opts = {}] = args;
|
|
5513
6108
|
const now = nowIso();
|
|
@@ -5675,18 +6270,102 @@ class PostgresLoopStorage {
|
|
|
5675
6270
|
async listRuns(...args) {
|
|
5676
6271
|
const opts = args[0] ?? {};
|
|
5677
6272
|
const limit = opts.limit ?? 100;
|
|
6273
|
+
const offset = Math.max(0, Math.floor(opts.offset ?? 0));
|
|
5678
6274
|
let rows;
|
|
5679
6275
|
if (opts.loopId && opts.status) {
|
|
5680
|
-
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]);
|
|
6276
|
+
rows = await this.client.many("SELECT * FROM loop_runs WHERE loop_id = $1 AND status = $2 ORDER BY created_at DESC LIMIT $3 OFFSET $4", [opts.loopId, opts.status, limit, offset]);
|
|
5681
6277
|
} else if (opts.loopId) {
|
|
5682
|
-
rows = await this.client.many("SELECT * FROM loop_runs WHERE loop_id = $1 ORDER BY created_at DESC LIMIT $2", [opts.loopId, limit]);
|
|
6278
|
+
rows = await this.client.many("SELECT * FROM loop_runs WHERE loop_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3", [opts.loopId, limit, offset]);
|
|
5683
6279
|
} else if (opts.status) {
|
|
5684
|
-
rows = await this.client.many("SELECT * FROM loop_runs WHERE status = $1 ORDER BY created_at DESC LIMIT $2", [opts.status, limit]);
|
|
6280
|
+
rows = await this.client.many("SELECT * FROM loop_runs WHERE status = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3", [opts.status, limit, offset]);
|
|
5685
6281
|
} else {
|
|
5686
|
-
rows = await this.client.many("SELECT * FROM loop_runs ORDER BY created_at DESC LIMIT $1", [limit]);
|
|
6282
|
+
rows = await this.client.many("SELECT * FROM loop_runs ORDER BY created_at DESC LIMIT $1 OFFSET $2", [limit, offset]);
|
|
5687
6283
|
}
|
|
5688
6284
|
return rows.map(rowToRun);
|
|
5689
6285
|
}
|
|
6286
|
+
async writeRunReceipt(...args) {
|
|
6287
|
+
const [input, opts = {}] = args;
|
|
6288
|
+
const inputRunId = typeof input.run_id === "string" && input.run_id.trim() ? input.run_id : undefined;
|
|
6289
|
+
const existing = inputRunId ? await this.getRunReceipt(inputRunId) : undefined;
|
|
6290
|
+
const run = inputRunId ? await this.getRun(inputRunId) : undefined;
|
|
6291
|
+
const loop = input.loop_id ? await this.getLoop(input.loop_id) : run ? await this.getLoop(run.loopId) : undefined;
|
|
6292
|
+
const receipt = normalizeRunReceipt(input, { now: opts.now, run, loop, existing });
|
|
6293
|
+
await this.client.execute(`INSERT INTO run_receipts (run_id, loop_id, machine_json, repo, task_ids_json, knowledge_ids_json, digest_id,
|
|
6294
|
+
started_at, finished_at, status, exit_code, summary_json, evidence_paths_json, created_at, updated_at)
|
|
6295
|
+
VALUES ($1,$2,$3::jsonb,$4,$5::jsonb,$6::jsonb,$7,$8,$9,$10,$11,$12::jsonb,$13::jsonb,$14,$15)
|
|
6296
|
+
ON CONFLICT(run_id) DO UPDATE SET
|
|
6297
|
+
loop_id=EXCLUDED.loop_id,
|
|
6298
|
+
machine_json=EXCLUDED.machine_json,
|
|
6299
|
+
repo=EXCLUDED.repo,
|
|
6300
|
+
task_ids_json=EXCLUDED.task_ids_json,
|
|
6301
|
+
knowledge_ids_json=EXCLUDED.knowledge_ids_json,
|
|
6302
|
+
digest_id=EXCLUDED.digest_id,
|
|
6303
|
+
started_at=EXCLUDED.started_at,
|
|
6304
|
+
finished_at=EXCLUDED.finished_at,
|
|
6305
|
+
status=EXCLUDED.status,
|
|
6306
|
+
exit_code=EXCLUDED.exit_code,
|
|
6307
|
+
summary_json=EXCLUDED.summary_json,
|
|
6308
|
+
evidence_paths_json=EXCLUDED.evidence_paths_json,
|
|
6309
|
+
updated_at=EXCLUDED.updated_at`, [
|
|
6310
|
+
receipt.run_id,
|
|
6311
|
+
receipt.loop_id,
|
|
6312
|
+
JSON.stringify(receipt.machine),
|
|
6313
|
+
receipt.repo,
|
|
6314
|
+
JSON.stringify(receipt.task_ids),
|
|
6315
|
+
JSON.stringify(receipt.knowledge_ids),
|
|
6316
|
+
receipt.digest_id,
|
|
6317
|
+
receipt.started_at,
|
|
6318
|
+
receipt.finished_at,
|
|
6319
|
+
receipt.status,
|
|
6320
|
+
receipt.exit_code,
|
|
6321
|
+
JSON.stringify(receipt.summary),
|
|
6322
|
+
JSON.stringify(receipt.evidence_paths),
|
|
6323
|
+
receipt.created_at,
|
|
6324
|
+
receipt.updated_at
|
|
6325
|
+
]);
|
|
6326
|
+
return await this.getRunReceipt(receipt.run_id) ?? receipt;
|
|
6327
|
+
}
|
|
6328
|
+
async getRunReceipt(...args) {
|
|
6329
|
+
const row = await this.client.get("SELECT * FROM run_receipts WHERE run_id = $1", [args[0]]);
|
|
6330
|
+
return row ? rowToRunReceipt(row) : undefined;
|
|
6331
|
+
}
|
|
6332
|
+
async listRunReceipts(...args) {
|
|
6333
|
+
const opts = args[0] ?? {};
|
|
6334
|
+
const limit = opts.limit ?? 100;
|
|
6335
|
+
const filters = [];
|
|
6336
|
+
const params = [];
|
|
6337
|
+
const next = () => `$${params.length + 1}`;
|
|
6338
|
+
if (opts.loopId) {
|
|
6339
|
+
const slot = next();
|
|
6340
|
+
filters.push(`loop_id = ${slot}`);
|
|
6341
|
+
params.push(opts.loopId);
|
|
6342
|
+
}
|
|
6343
|
+
if (opts.repo) {
|
|
6344
|
+
const slot = next();
|
|
6345
|
+
filters.push(`repo = ${slot}`);
|
|
6346
|
+
params.push(opts.repo);
|
|
6347
|
+
}
|
|
6348
|
+
if (opts.status) {
|
|
6349
|
+
const slot = next();
|
|
6350
|
+
filters.push(`status = ${slot}`);
|
|
6351
|
+
params.push(opts.status);
|
|
6352
|
+
}
|
|
6353
|
+
if (opts.taskId) {
|
|
6354
|
+
const slot = next();
|
|
6355
|
+
filters.push(`task_ids_json ? ${slot}`);
|
|
6356
|
+
params.push(opts.taskId);
|
|
6357
|
+
}
|
|
6358
|
+
if (opts.knowledgeId) {
|
|
6359
|
+
const slot = next();
|
|
6360
|
+
filters.push(`knowledge_ids_json ? ${slot}`);
|
|
6361
|
+
params.push(opts.knowledgeId);
|
|
6362
|
+
}
|
|
6363
|
+
const limitSlot = next();
|
|
6364
|
+
params.push(limit);
|
|
6365
|
+
const where = filters.length ? `WHERE ${filters.join(" AND ")}` : "";
|
|
6366
|
+
const rows = await this.client.many(`SELECT * FROM run_receipts ${where} ORDER BY created_at DESC LIMIT ${limitSlot}`, params);
|
|
6367
|
+
return rows.map(rowToRunReceipt);
|
|
6368
|
+
}
|
|
5690
6369
|
async countRuns(...args) {
|
|
5691
6370
|
const status = args[0];
|
|
5692
6371
|
const row = status ? await this.client.get("SELECT COUNT(*)::int AS count FROM loop_runs WHERE status = $1", [status]) : await this.client.get("SELECT COUNT(*)::int AS count FROM loop_runs", []);
|