@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
package/dist/sdk/index.js
CHANGED
|
@@ -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;
|
|
@@ -4594,7 +4970,7 @@ class Store {
|
|
|
4594
4970
|
// package.json
|
|
4595
4971
|
var package_default = {
|
|
4596
4972
|
name: "@hasna/loops",
|
|
4597
|
-
version: "0.4.
|
|
4973
|
+
version: "0.4.22",
|
|
4598
4974
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4599
4975
|
type: "module",
|
|
4600
4976
|
main: "dist/index.js",
|
|
@@ -4704,7 +5080,6 @@ var package_default = {
|
|
|
4704
5080
|
bun: ">=1.0.0"
|
|
4705
5081
|
},
|
|
4706
5082
|
dependencies: {
|
|
4707
|
-
"@hasna/contracts": "^0.4.1",
|
|
4708
5083
|
"@hasna/events": "^0.1.9",
|
|
4709
5084
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
4710
5085
|
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
@@ -4714,7 +5089,8 @@ var package_default = {
|
|
|
4714
5089
|
zod: "4.4.3"
|
|
4715
5090
|
},
|
|
4716
5091
|
optionalDependencies: {
|
|
4717
|
-
"@hasna/machines": "0.0.49"
|
|
5092
|
+
"@hasna/machines": "0.0.49",
|
|
5093
|
+
"@hasna/contracts": "^0.4.2"
|
|
4718
5094
|
},
|
|
4719
5095
|
devDependencies: {
|
|
4720
5096
|
"@types/bun": "latest",
|
|
@@ -4929,7 +5305,7 @@ function deploymentStatusLine(status) {
|
|
|
4929
5305
|
// src/daemon/control.ts
|
|
4930
5306
|
import { existsSync as existsSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync as rmSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
4931
5307
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
4932
|
-
import { hostname } from "os";
|
|
5308
|
+
import { hostname as hostname2 } from "os";
|
|
4933
5309
|
import { dirname as dirname3, join as join5 } from "path";
|
|
4934
5310
|
|
|
4935
5311
|
// src/daemon/loop.ts
|
|
@@ -5082,7 +5458,7 @@ function daemonStatus(store, path = pidFilePath()) {
|
|
|
5082
5458
|
return {
|
|
5083
5459
|
...isDaemonRunning(path),
|
|
5084
5460
|
lease: store.getDaemonLease(),
|
|
5085
|
-
host:
|
|
5461
|
+
host: hostname2(),
|
|
5086
5462
|
loops: {
|
|
5087
5463
|
total: store.countLoops(),
|
|
5088
5464
|
active: store.countLoops("active"),
|
|
@@ -5439,6 +5815,7 @@ var TRANSPORT_ENV_KEYS = new Set([
|
|
|
5439
5815
|
"LOGNAME",
|
|
5440
5816
|
"PATH",
|
|
5441
5817
|
"SHELL",
|
|
5818
|
+
"SHLVL",
|
|
5442
5819
|
"SSH_AGENT_PID",
|
|
5443
5820
|
"SSH_AUTH_SOCK",
|
|
5444
5821
|
"TERM",
|
|
@@ -5660,6 +6037,7 @@ function composeExecutionEnv(spec, metadata, opts, accountEnv) {
|
|
|
5660
6037
|
Object.assign(env, spec.env ?? {});
|
|
5661
6038
|
Object.assign(env, allowlistEnv(spec.allowlist));
|
|
5662
6039
|
env.PATH = normalizeExecutionPath(env);
|
|
6040
|
+
env.SHLVL ||= "1";
|
|
5663
6041
|
Object.assign(env, metadataEnv(metadata));
|
|
5664
6042
|
return env;
|
|
5665
6043
|
}
|
|
@@ -5731,7 +6109,7 @@ function remoteWorktreePrepareLines(worktree) {
|
|
|
5731
6109
|
return [
|
|
5732
6110
|
"__openloops_prepare_worktree() {",
|
|
5733
6111
|
` local repo=${shellQuote(repoRoot)} path=${shellQuote(path)} branch=${shellQuote(branch)}`,
|
|
5734
|
-
" local top expected_common actual_common current",
|
|
6112
|
+
" local top expected_common actual_common current status recovered",
|
|
5735
6113
|
' if [ -L "$path" ]; then echo "refusing symlinked worktree path $path" >&2; return 1; fi',
|
|
5736
6114
|
' if [ -e "$path" ]; then',
|
|
5737
6115
|
' top="$(git -C "$path" rev-parse --show-toplevel 2>/dev/null)" || { echo "refusing to reuse non-worktree path: $path" >&2; return 1; }',
|
|
@@ -5740,7 +6118,19 @@ function remoteWorktreePrepareLines(worktree) {
|
|
|
5740
6118
|
' actual_common="$(cd "$path" 2>/dev/null && cd "$(git rev-parse --git-common-dir 2>/dev/null)" 2>/dev/null && pwd -P)"',
|
|
5741
6119
|
' if [ -z "$expected_common" ] || [ "$expected_common" != "$actual_common" ]; then echo "existing worktree $path belongs to a different git common dir" >&2; return 1; fi',
|
|
5742
6120
|
' current="$(git -C "$path" branch --show-current 2>/dev/null)"',
|
|
5743
|
-
' if [ "$current" != "$branch" ]; then
|
|
6121
|
+
' if [ "$current" != "$branch" ]; then',
|
|
6122
|
+
' status="$(git -C "$path" status --porcelain=v1 --untracked-files=all 2>/dev/null)" || { echo "existing worktree $path is on branch ${current:-unknown}, expected $branch, and cleanliness could not be checked; inspect or remove the stale worktree" >&2; return 1; }',
|
|
6123
|
+
' if [ -n "$status" ]; then echo "existing worktree $path is on branch ${current:-unknown}, expected $branch, and has local changes; commit/stash them or remove the stale worktree before retrying" >&2; return 1; fi',
|
|
6124
|
+
' if git -C "$repo" show-ref --verify --quiet "refs/heads/$branch"; then',
|
|
6125
|
+
' git -C "$path" checkout "$branch" 1>&2 || { echo "existing worktree $path is clean but could not switch from branch ${current:-unknown} to expected $branch; remove/prune the stale worktree or free the branch before retrying" >&2; return 1; }',
|
|
6126
|
+
' elif [ -z "$current" ]; then',
|
|
6127
|
+
' git -C "$path" checkout -b "$branch" 1>&2 || { echo "existing worktree $path is clean but could not recreate expected branch $branch at the current detached HEAD; remove/prune the stale worktree before retrying" >&2; return 1; }',
|
|
6128
|
+
" else",
|
|
6129
|
+
' echo "existing worktree $path is on branch $current, expected $branch, but expected branch does not exist; remove/prune the stale worktree or recreate the expected branch before retrying" >&2; return 1',
|
|
6130
|
+
" fi",
|
|
6131
|
+
' recovered="$(git -C "$path" branch --show-current 2>/dev/null)"',
|
|
6132
|
+
' if [ "$recovered" != "$branch" ]; then echo "existing worktree $path branch recovery ended on ${recovered:-unknown}, expected $branch; remove/prune the stale worktree before retrying" >&2; return 1; fi',
|
|
6133
|
+
" fi",
|
|
5744
6134
|
" return 0",
|
|
5745
6135
|
" fi",
|
|
5746
6136
|
' git -C "$repo" rev-parse --is-inside-work-tree >/dev/null 2>&1 || { echo "worktree repoRoot is not a git repository: $repo" >&2; return 1; }',
|
|
@@ -5821,8 +6211,21 @@ function transportEnv(opts) {
|
|
|
5821
6211
|
env[key] = value;
|
|
5822
6212
|
}
|
|
5823
6213
|
env.PATH = normalizeExecutionPath(env);
|
|
6214
|
+
env.SHLVL ||= "1";
|
|
5824
6215
|
return env;
|
|
5825
6216
|
}
|
|
6217
|
+
function remotePreflightFailureMessage(machineId, detail) {
|
|
6218
|
+
const normalized = detail.trim();
|
|
6219
|
+
if (/Host key verification failed|REMOTE HOST IDENTIFICATION HAS CHANGED|Offending .*key in .*known_hosts/i.test(normalized)) {
|
|
6220
|
+
return [
|
|
6221
|
+
`remote preflight failed on ${machineId}: SSH host key verification failed.`,
|
|
6222
|
+
`Verify ${machineId}'s host identity and repair SSH known_hosts/trust material outside OpenLoops;`,
|
|
6223
|
+
"OpenLoops will not disable host-key checking or modify known_hosts automatically.",
|
|
6224
|
+
normalized ? `Transport detail: ${normalized}` : ""
|
|
6225
|
+
].filter(Boolean).join(" ");
|
|
6226
|
+
}
|
|
6227
|
+
return `remote preflight failed on ${machineId}${normalized ? `: ${normalized}` : ""}`;
|
|
6228
|
+
}
|
|
5826
6229
|
function assertCodewithProfileListed(profile, result) {
|
|
5827
6230
|
const profiles = codewithProfileSetFromResult(result);
|
|
5828
6231
|
if (!profiles.has(profile)) {
|
|
@@ -5874,6 +6277,9 @@ async function preflightNativeAuthProfile(spec, env) {
|
|
|
5874
6277
|
const tableResult = await spawnCapture(spec.command, ["profile", "list"], { env, timeoutMs: 15000 });
|
|
5875
6278
|
assertCodewithProfileListed(spec.nativeAuthProfile.profile, tableResult);
|
|
5876
6279
|
}
|
|
6280
|
+
function spawnDetail(result) {
|
|
6281
|
+
return (result.stderr || result.stdout || result.error || "").toString().trim();
|
|
6282
|
+
}
|
|
5877
6283
|
function resolvedDirEquals(left, right) {
|
|
5878
6284
|
try {
|
|
5879
6285
|
return realpathSync(left) === realpathSync(right);
|
|
@@ -5923,8 +6329,45 @@ async function ensureLocalWorktree(worktree, env) {
|
|
|
5923
6329
|
}
|
|
5924
6330
|
const current = await git(["-C", path, "branch", "--show-current"]);
|
|
5925
6331
|
const actualBranch = current.stdout.trim();
|
|
5926
|
-
if (current.error || (current.status ?? 1) !== 0
|
|
5927
|
-
return { error: `existing worktree ${path}
|
|
6332
|
+
if (current.error || (current.status ?? 1) !== 0) {
|
|
6333
|
+
return { error: `existing worktree ${path} branch could not be inspected${spawnDetail(current) ? `: ${spawnDetail(current)}` : ""}` };
|
|
6334
|
+
}
|
|
6335
|
+
if (actualBranch !== branch) {
|
|
6336
|
+
const actualLabel = actualBranch || "unknown";
|
|
6337
|
+
const status = await git(["-C", path, "status", "--porcelain=v1", "--untracked-files=all"]);
|
|
6338
|
+
if (status.error || (status.status ?? 1) !== 0) {
|
|
6339
|
+
const detail = spawnDetail(status);
|
|
6340
|
+
return {
|
|
6341
|
+
error: `existing worktree ${path} is on branch ${actualLabel}, expected ${branch}, and cleanliness could not be checked; inspect or remove the stale worktree${detail ? `: ${detail}` : ""}`
|
|
6342
|
+
};
|
|
6343
|
+
}
|
|
6344
|
+
if (status.stdout.trim()) {
|
|
6345
|
+
return {
|
|
6346
|
+
error: `existing worktree ${path} is on branch ${actualLabel}, expected ${branch}, and has local changes; commit/stash them or remove the stale worktree before retrying`
|
|
6347
|
+
};
|
|
6348
|
+
}
|
|
6349
|
+
const hasBranch2 = await git(["-C", repoRoot, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`]);
|
|
6350
|
+
const checkout = hasBranch2.status === 0 ? await git(["-C", path, "checkout", branch]) : actualBranch ? {
|
|
6351
|
+
status: 1,
|
|
6352
|
+
stdout: "",
|
|
6353
|
+
stderr: `existing worktree ${path} is on branch ${actualLabel}, expected ${branch}, but expected branch does not exist; remove/prune the stale worktree or recreate the expected branch before retrying`,
|
|
6354
|
+
signal: null,
|
|
6355
|
+
timedOut: false
|
|
6356
|
+
} : await git(["-C", path, "checkout", "-b", branch]);
|
|
6357
|
+
if (checkout.error || (checkout.status ?? 1) !== 0) {
|
|
6358
|
+
const detail = spawnDetail(checkout);
|
|
6359
|
+
return {
|
|
6360
|
+
error: `existing worktree ${path} is clean but could not recover branch ${branch} from ${actualLabel}; remove/prune the stale worktree before retrying${detail ? `: ${detail}` : ""}`
|
|
6361
|
+
};
|
|
6362
|
+
}
|
|
6363
|
+
const recovered = await git(["-C", path, "branch", "--show-current"]);
|
|
6364
|
+
if (recovered.error || (recovered.status ?? 1) !== 0 || recovered.stdout.trim() !== branch) {
|
|
6365
|
+
const recoveredBranch = recovered.stdout.trim() || "unknown";
|
|
6366
|
+
const detail = spawnDetail(recovered);
|
|
6367
|
+
return {
|
|
6368
|
+
error: `existing worktree ${path} branch recovery ended on ${recoveredBranch}, expected ${branch}; remove/prune the stale worktree before retrying${detail ? `: ${detail}` : ""}`
|
|
6369
|
+
};
|
|
6370
|
+
}
|
|
5928
6371
|
}
|
|
5929
6372
|
return { cwd: worktree.cwd };
|
|
5930
6373
|
}
|
|
@@ -5940,7 +6383,7 @@ async function ensureLocalWorktree(worktree, env) {
|
|
|
5940
6383
|
const hasBranch = await git(["-C", repoRoot, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`]);
|
|
5941
6384
|
const add = hasBranch.status === 0 ? await git(["-C", repoRoot, "worktree", "add", path, branch]) : await git(["-C", repoRoot, "worktree", "add", "-b", branch, path, "HEAD"]);
|
|
5942
6385
|
if (add.error || (add.status ?? 1) !== 0) {
|
|
5943
|
-
const detail = (add
|
|
6386
|
+
const detail = spawnDetail(add);
|
|
5944
6387
|
return { error: `git worktree add failed for ${path}${detail ? `: ${detail}` : ""}` };
|
|
5945
6388
|
}
|
|
5946
6389
|
return { cwd: worktree.cwd };
|
|
@@ -5986,7 +6429,7 @@ function preflightRemoteSpec(spec, machine, metadata, opts) {
|
|
|
5986
6429
|
throw new Error(`remote preflight failed on ${machine.id}: ${result.error.message}`);
|
|
5987
6430
|
if ((result.status ?? 1) !== 0) {
|
|
5988
6431
|
const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
|
|
5989
|
-
throw new Error(
|
|
6432
|
+
throw new Error(remotePreflightFailureMessage(machine.id, detail));
|
|
5990
6433
|
}
|
|
5991
6434
|
}
|
|
5992
6435
|
async function executeRemoteSpec(spec, machine, metadata, opts, fallbackSpec) {
|
|
@@ -6246,7 +6689,7 @@ async function executeLoop(loop, run, opts = {}) {
|
|
|
6246
6689
|
}
|
|
6247
6690
|
|
|
6248
6691
|
// src/lib/health.ts
|
|
6249
|
-
import { createHash as
|
|
6692
|
+
import { createHash as createHash3 } from "crypto";
|
|
6250
6693
|
import { existsSync as existsSync4, mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
6251
6694
|
import { join as join7 } from "path";
|
|
6252
6695
|
var EVIDENCE_CHARS = 2000;
|
|
@@ -6308,7 +6751,7 @@ function tagsFromValue(value) {
|
|
|
6308
6751
|
return [];
|
|
6309
6752
|
}
|
|
6310
6753
|
function stableFingerprint(parts) {
|
|
6311
|
-
return
|
|
6754
|
+
return createHash3("sha256").update(parts.join(`
|
|
6312
6755
|
`)).digest("hex").slice(0, 16);
|
|
6313
6756
|
}
|
|
6314
6757
|
function stableScanFingerprint(parts) {
|
|
@@ -7203,8 +7646,8 @@ function runDoctor(store) {
|
|
|
7203
7646
|
}
|
|
7204
7647
|
|
|
7205
7648
|
// src/lib/migration.ts
|
|
7206
|
-
import { createHash as
|
|
7207
|
-
import { hostname as
|
|
7649
|
+
import { createHash as createHash4 } from "crypto";
|
|
7650
|
+
import { hostname as hostname3 } from "os";
|
|
7208
7651
|
var LOOPS_MIGRATION_SCHEMA = "open-loops.migration/v1";
|
|
7209
7652
|
function canonicalize(value) {
|
|
7210
7653
|
if (Array.isArray(value))
|
|
@@ -7215,7 +7658,7 @@ function canonicalize(value) {
|
|
|
7215
7658
|
return value;
|
|
7216
7659
|
}
|
|
7217
7660
|
function migrationHash(value) {
|
|
7218
|
-
return
|
|
7661
|
+
return createHash4("sha256").update(JSON.stringify(canonicalize(value))).digest("hex");
|
|
7219
7662
|
}
|
|
7220
7663
|
function pushBlocker(rows, resource, id, reason, name) {
|
|
7221
7664
|
rows.push({ resource, id, name, action: "blocked", reason });
|
|
@@ -7291,7 +7734,7 @@ function exportLoopsMigrationBundle(store, opts = {}) {
|
|
|
7291
7734
|
source: {
|
|
7292
7735
|
backend: "sqlite",
|
|
7293
7736
|
schemaVersion: rows.schemaVersion,
|
|
7294
|
-
hostname:
|
|
7737
|
+
hostname: hostname3()
|
|
7295
7738
|
},
|
|
7296
7739
|
checks: rows.checks,
|
|
7297
7740
|
importable: sanitized.blockers.length === 0,
|
|
@@ -7672,6 +8115,85 @@ async function buildSelfHostedMigrationPlan(store, opts) {
|
|
|
7672
8115
|
});
|
|
7673
8116
|
return { ...plan, importable: false };
|
|
7674
8117
|
}
|
|
8118
|
+
async function postImportBatch(fetchImpl, config, payload) {
|
|
8119
|
+
const body = await requestJson(fetchImpl, config, "/v1/import", { method: "POST", body: JSON.stringify(payload) });
|
|
8120
|
+
const imported = body.imported ?? {};
|
|
8121
|
+
return {
|
|
8122
|
+
imported: { workflows: imported.workflows ?? 0, loops: imported.loops ?? 0, runs: imported.runs ?? 0 },
|
|
8123
|
+
skippedRunning: typeof body.skippedRunning === "number" ? body.skippedRunning : 0
|
|
8124
|
+
};
|
|
8125
|
+
}
|
|
8126
|
+
async function applySelfHostedPush(store, opts) {
|
|
8127
|
+
const resolved = resolveApiConfig(opts);
|
|
8128
|
+
if (!resolved.apiUrl)
|
|
8129
|
+
throw new ValidationError("LOOPS_API_URL or --api-url is required for self-hosted push");
|
|
8130
|
+
if (!isLocalApiUrl(resolved.apiUrl) && !resolved.token) {
|
|
8131
|
+
throw new ValidationError("non-local self-hosted APIs require LOOPS_API_TOKEN or HASNA_LOOPS_API_TOKEN");
|
|
8132
|
+
}
|
|
8133
|
+
const config = { apiUrl: resolved.apiUrl, token: resolved.token };
|
|
8134
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
8135
|
+
const includeRuns = opts.includeRuns ?? true;
|
|
8136
|
+
const replace = opts.replace ?? false;
|
|
8137
|
+
const batchRows = Math.max(1, opts.batchRows ?? 200);
|
|
8138
|
+
const runBatchBytes = Math.max(64 * 1024, opts.runBatchBytes ?? 4 * 1024 * 1024);
|
|
8139
|
+
const applied = { workflows: 0, loops: 0, runs: 0 };
|
|
8140
|
+
const skipped = { runningRuns: 0, orphanRuns: 0 };
|
|
8141
|
+
let requests = 0;
|
|
8142
|
+
const base = store.exportMigrationRows({ includeRuns: false });
|
|
8143
|
+
const loopIds = new Set(base.loops.map((loop) => loop.id));
|
|
8144
|
+
for (let i = 0;i < base.workflows.length; i += batchRows) {
|
|
8145
|
+
const batch = base.workflows.slice(i, i + batchRows);
|
|
8146
|
+
const result = await postImportBatch(fetchImpl, config, { workflows: batch, replace });
|
|
8147
|
+
applied.workflows += result.imported.workflows;
|
|
8148
|
+
requests += 1;
|
|
8149
|
+
opts.onProgress?.({ phase: "workflows", sent: applied.workflows, requests });
|
|
8150
|
+
}
|
|
8151
|
+
for (let i = 0;i < base.loops.length; i += batchRows) {
|
|
8152
|
+
const batch = base.loops.slice(i, i + batchRows);
|
|
8153
|
+
const result = await postImportBatch(fetchImpl, config, { loops: batch, replace });
|
|
8154
|
+
applied.loops += result.imported.loops;
|
|
8155
|
+
requests += 1;
|
|
8156
|
+
opts.onProgress?.({ phase: "loops", sent: applied.loops, requests });
|
|
8157
|
+
}
|
|
8158
|
+
if (includeRuns) {
|
|
8159
|
+
const pageSize = 500;
|
|
8160
|
+
let pending = [];
|
|
8161
|
+
let pendingBytes = 0;
|
|
8162
|
+
const flush = async () => {
|
|
8163
|
+
if (pending.length === 0)
|
|
8164
|
+
return;
|
|
8165
|
+
const result = await postImportBatch(fetchImpl, config, { runs: pending, replace });
|
|
8166
|
+
applied.runs += result.imported.runs;
|
|
8167
|
+
skipped.runningRuns += result.skippedRunning;
|
|
8168
|
+
requests += 1;
|
|
8169
|
+
opts.onProgress?.({ phase: "runs", sent: applied.runs, requests });
|
|
8170
|
+
pending = [];
|
|
8171
|
+
pendingBytes = 0;
|
|
8172
|
+
};
|
|
8173
|
+
for (let offset = 0;; offset += pageSize) {
|
|
8174
|
+
const page = store.exportMigrationRunPage({ limit: pageSize, offset });
|
|
8175
|
+
if (page.length === 0)
|
|
8176
|
+
break;
|
|
8177
|
+
for (const run of page) {
|
|
8178
|
+
if (!loopIds.has(run.loopId)) {
|
|
8179
|
+
skipped.orphanRuns += 1;
|
|
8180
|
+
continue;
|
|
8181
|
+
}
|
|
8182
|
+
const encoded = JSON.stringify(run).length;
|
|
8183
|
+
if (pending.length > 0 && pendingBytes + encoded > runBatchBytes)
|
|
8184
|
+
await flush();
|
|
8185
|
+
pending.push(run);
|
|
8186
|
+
pendingBytes += encoded;
|
|
8187
|
+
if (pending.length >= batchRows * 4)
|
|
8188
|
+
await flush();
|
|
8189
|
+
}
|
|
8190
|
+
if (page.length < pageSize)
|
|
8191
|
+
break;
|
|
8192
|
+
}
|
|
8193
|
+
await flush();
|
|
8194
|
+
}
|
|
8195
|
+
return { ok: true, apiUrl: config.apiUrl, applied, skipped, requests };
|
|
8196
|
+
}
|
|
7675
8197
|
async function registerSelfHostedRunner(opts) {
|
|
7676
8198
|
const config = resolveApiConfig(opts);
|
|
7677
8199
|
if (!config.apiUrl)
|
|
@@ -7796,55 +8318,6 @@ ${evidence.join(`
|
|
|
7796
8318
|
import { generateObject } from "ai";
|
|
7797
8319
|
import { z } from "zod";
|
|
7798
8320
|
|
|
7799
|
-
// src/lib/run-envelope.ts
|
|
7800
|
-
var ENVELOPE_EXCERPT_CHARS = 2048;
|
|
7801
|
-
var BLOCKED_STEP_ERROR_PREFIX = "blocked:";
|
|
7802
|
-
function boundedExcerpt(text, limit = ENVELOPE_EXCERPT_CHARS) {
|
|
7803
|
-
if (!text)
|
|
7804
|
-
return;
|
|
7805
|
-
const scrubbed = scrubSecrets(text);
|
|
7806
|
-
if (scrubbed.length <= limit * 2)
|
|
7807
|
-
return scrubbed;
|
|
7808
|
-
const omitted = scrubbed.length - limit * 2;
|
|
7809
|
-
return `${scrubbed.slice(0, limit)}
|
|
7810
|
-
[... ${omitted} chars omitted ...]
|
|
7811
|
-
${scrubbed.slice(-limit)}`;
|
|
7812
|
-
}
|
|
7813
|
-
function summarizeOutput(stdout, stderr) {
|
|
7814
|
-
return {
|
|
7815
|
-
stdoutBytes: stdout ? Buffer.byteLength(stdout, "utf8") : 0,
|
|
7816
|
-
stderrBytes: stderr ? Buffer.byteLength(stderr, "utf8") : 0,
|
|
7817
|
-
stdoutExcerpt: boundedExcerpt(stdout),
|
|
7818
|
-
stderrExcerpt: boundedExcerpt(stderr)
|
|
7819
|
-
};
|
|
7820
|
-
}
|
|
7821
|
-
function summarizeExecutorResult(result) {
|
|
7822
|
-
return {
|
|
7823
|
-
...summarizeOutput(result.stdout, result.stderr),
|
|
7824
|
-
status: result.status,
|
|
7825
|
-
exitCode: result.exitCode,
|
|
7826
|
-
durationMs: result.durationMs,
|
|
7827
|
-
error: boundedExcerpt(result.error)
|
|
7828
|
-
};
|
|
7829
|
-
}
|
|
7830
|
-
function isBlockedStepRun(step) {
|
|
7831
|
-
return step?.status === "skipped" && (step.error?.startsWith(BLOCKED_STEP_ERROR_PREFIX) ?? false);
|
|
7832
|
-
}
|
|
7833
|
-
function summarizeWorkflowStepRun(step) {
|
|
7834
|
-
return {
|
|
7835
|
-
...summarizeOutput(step.stdout, step.stderr),
|
|
7836
|
-
stepId: step.stepId,
|
|
7837
|
-
status: step.status,
|
|
7838
|
-
exitCode: step.exitCode,
|
|
7839
|
-
durationMs: step.durationMs,
|
|
7840
|
-
error: boundedExcerpt(step.error),
|
|
7841
|
-
blocked: isBlockedStepRun(step)
|
|
7842
|
-
};
|
|
7843
|
-
}
|
|
7844
|
-
function workflowRunEnvelope(run, steps) {
|
|
7845
|
-
return JSON.stringify({ workflowRun: run, steps: steps.map(summarizeWorkflowStepRun) }, null, 2);
|
|
7846
|
-
}
|
|
7847
|
-
|
|
7848
8321
|
// src/lib/goal/model-factory.ts
|
|
7849
8322
|
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
|
|
7850
8323
|
var DEFAULT_GOAL_MODEL = "openai/gpt-4o-mini";
|
|
@@ -9084,35 +9557,926 @@ async function tick(deps) {
|
|
|
9084
9557
|
return { claimed, completed, skipped, recovered, expired };
|
|
9085
9558
|
}
|
|
9086
9559
|
|
|
9087
|
-
// src/
|
|
9088
|
-
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9560
|
+
// src/lib/cloud/mode.ts
|
|
9561
|
+
var DEPRECATED_STORAGE_MODE_ALIASES = ["remote", "hybrid", "self_hosted"];
|
|
9562
|
+
function normalizeStorageMode(value) {
|
|
9563
|
+
const normalized = value.trim().toLowerCase().replace(/-/g, "_");
|
|
9564
|
+
if (normalized === "local")
|
|
9565
|
+
return { mode: "local", deprecatedAlias: null };
|
|
9566
|
+
if (normalized === "cloud")
|
|
9567
|
+
return { mode: "cloud", deprecatedAlias: null };
|
|
9568
|
+
if (DEPRECATED_STORAGE_MODE_ALIASES.includes(normalized)) {
|
|
9569
|
+
return { mode: "cloud", deprecatedAlias: normalized };
|
|
9096
9570
|
}
|
|
9097
|
-
|
|
9098
|
-
|
|
9571
|
+
throw new Error(`Unknown storage mode: ${value}. Use local or cloud.`);
|
|
9572
|
+
}
|
|
9573
|
+
function envToken(name) {
|
|
9574
|
+
return name.toUpperCase().replace(/-/g, "_");
|
|
9575
|
+
}
|
|
9576
|
+
|
|
9577
|
+
// src/lib/cloud/transport.ts
|
|
9578
|
+
function defaultCloudBaseUrl(name) {
|
|
9579
|
+
return `https://${name}.hasna.xyz`;
|
|
9580
|
+
}
|
|
9581
|
+
function clientTransportEnvKeys(name) {
|
|
9582
|
+
const token = envToken(name);
|
|
9583
|
+
return {
|
|
9584
|
+
modeKeys: [
|
|
9585
|
+
`HASNA_${token}_STORAGE_MODE`,
|
|
9586
|
+
`HASNA_${token}_MODE`,
|
|
9587
|
+
`${token}_STORAGE_MODE`,
|
|
9588
|
+
`${token}_MODE`
|
|
9589
|
+
],
|
|
9590
|
+
apiUrlKeys: [`HASNA_${token}_API_URL`, `${token}_API_URL`],
|
|
9591
|
+
apiKeyKeys: [
|
|
9592
|
+
`HASNA_${token}_API_KEY`,
|
|
9593
|
+
`${token}_API_KEY`,
|
|
9594
|
+
`HASNA_${token}_API_TOKEN`,
|
|
9595
|
+
`${token}_API_TOKEN`
|
|
9596
|
+
]
|
|
9597
|
+
};
|
|
9598
|
+
}
|
|
9599
|
+
function firstEnv(env, keys) {
|
|
9600
|
+
for (const key of keys) {
|
|
9601
|
+
const value = env[key]?.trim();
|
|
9602
|
+
if (value)
|
|
9603
|
+
return { key, value };
|
|
9099
9604
|
}
|
|
9100
|
-
|
|
9101
|
-
|
|
9102
|
-
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
|
|
9605
|
+
return null;
|
|
9606
|
+
}
|
|
9607
|
+
function toV1BaseUrl(apiUrl) {
|
|
9608
|
+
const url = new URL(apiUrl);
|
|
9609
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
9610
|
+
throw new Error("API URL must use http or https.");
|
|
9611
|
+
}
|
|
9612
|
+
let path = url.pathname.replace(/\/+$/, "");
|
|
9613
|
+
if (path.endsWith("/v1"))
|
|
9614
|
+
path = path.slice(0, -"/v1".length);
|
|
9615
|
+
url.pathname = `${path}/v1`;
|
|
9616
|
+
url.search = "";
|
|
9617
|
+
url.hash = "";
|
|
9618
|
+
return url.toString().replace(/\/+$/, "");
|
|
9619
|
+
}
|
|
9620
|
+
function resolveClientTransport(name, env = process.env) {
|
|
9621
|
+
const keys = clientTransportEnvKeys(name);
|
|
9622
|
+
const modeHit = firstEnv(env, keys.modeKeys);
|
|
9623
|
+
const urlHit = firstEnv(env, keys.apiUrlKeys);
|
|
9624
|
+
const keyHit = firstEnv(env, keys.apiKeyKeys);
|
|
9625
|
+
let mode = "local";
|
|
9626
|
+
let deprecatedAlias = null;
|
|
9627
|
+
let modeSource = "default";
|
|
9628
|
+
const warnings = [];
|
|
9629
|
+
if (modeHit) {
|
|
9630
|
+
const normalized = normalizeStorageMode(modeHit.value);
|
|
9631
|
+
mode = normalized.mode;
|
|
9632
|
+
deprecatedAlias = normalized.deprecatedAlias;
|
|
9633
|
+
modeSource = modeHit.key;
|
|
9634
|
+
if (deprecatedAlias) {
|
|
9635
|
+
warnings.push(`Deprecated mode '${deprecatedAlias}' from ${modeHit.key} is treated as 'cloud'. Prefer ${keys.modeKeys[0]}=cloud.`);
|
|
9636
|
+
}
|
|
9637
|
+
}
|
|
9638
|
+
if (mode === "local") {
|
|
9639
|
+
return {
|
|
9640
|
+
transport: "local",
|
|
9641
|
+
mode,
|
|
9642
|
+
deprecatedAlias,
|
|
9643
|
+
modeSource,
|
|
9644
|
+
baseUrl: null,
|
|
9645
|
+
apiUrlSource: null,
|
|
9646
|
+
apiKeyPresent: Boolean(keyHit),
|
|
9647
|
+
apiKeySource: keyHit ? keyHit.key : null,
|
|
9648
|
+
misconfigured: false,
|
|
9649
|
+
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
9650
|
+
};
|
|
9651
|
+
}
|
|
9652
|
+
if (!keyHit) {
|
|
9653
|
+
warnings.push(`${modeSource}=cloud but no API key is set (${keys.apiKeyKeys[0]}). Refusing to route to cloud; using local store. Set ${keys.apiKeyKeys[0]} to enable the cloud client.`);
|
|
9654
|
+
return {
|
|
9655
|
+
transport: "local",
|
|
9656
|
+
mode,
|
|
9657
|
+
deprecatedAlias,
|
|
9658
|
+
modeSource,
|
|
9659
|
+
baseUrl: null,
|
|
9660
|
+
apiUrlSource: null,
|
|
9661
|
+
apiKeyPresent: false,
|
|
9662
|
+
apiKeySource: null,
|
|
9663
|
+
misconfigured: true,
|
|
9664
|
+
warning: warnings.join(" ")
|
|
9665
|
+
};
|
|
9666
|
+
}
|
|
9667
|
+
const rawUrl = urlHit?.value ?? defaultCloudBaseUrl(name);
|
|
9668
|
+
const apiUrlSource = urlHit ? urlHit.key : "default";
|
|
9669
|
+
let baseUrl;
|
|
9670
|
+
try {
|
|
9671
|
+
baseUrl = toV1BaseUrl(rawUrl);
|
|
9672
|
+
} catch (error) {
|
|
9673
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9674
|
+
warnings.push(`Invalid API URL from ${apiUrlSource}: ${message}. Using local store.`);
|
|
9675
|
+
return {
|
|
9676
|
+
transport: "local",
|
|
9677
|
+
mode,
|
|
9678
|
+
deprecatedAlias,
|
|
9679
|
+
modeSource,
|
|
9680
|
+
baseUrl: null,
|
|
9681
|
+
apiUrlSource: null,
|
|
9682
|
+
apiKeyPresent: true,
|
|
9683
|
+
apiKeySource: keyHit.key,
|
|
9684
|
+
misconfigured: true,
|
|
9685
|
+
warning: warnings.join(" ")
|
|
9686
|
+
};
|
|
9687
|
+
}
|
|
9688
|
+
return {
|
|
9689
|
+
transport: "cloud-http",
|
|
9690
|
+
mode,
|
|
9691
|
+
deprecatedAlias,
|
|
9692
|
+
modeSource,
|
|
9693
|
+
baseUrl,
|
|
9694
|
+
apiUrlSource,
|
|
9695
|
+
apiKeyPresent: true,
|
|
9696
|
+
apiKeySource: keyHit.key,
|
|
9697
|
+
misconfigured: false,
|
|
9698
|
+
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
9699
|
+
};
|
|
9700
|
+
}
|
|
9701
|
+
|
|
9702
|
+
class HasnaHttpError extends Error {
|
|
9703
|
+
status;
|
|
9704
|
+
method;
|
|
9705
|
+
path;
|
|
9706
|
+
body;
|
|
9707
|
+
constructor(method, path, status, body) {
|
|
9708
|
+
super(`Hasna cloud request failed: ${method} ${path} -> ${status}`);
|
|
9709
|
+
this.name = "HasnaHttpError";
|
|
9710
|
+
this.status = status;
|
|
9711
|
+
this.method = method;
|
|
9712
|
+
this.path = path;
|
|
9713
|
+
this.body = body;
|
|
9714
|
+
}
|
|
9715
|
+
}
|
|
9716
|
+
var DEFAULT_RETRY_STATUSES = [408, 425, 429, 500, 502, 503, 504];
|
|
9717
|
+
var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
9718
|
+
function appendQuery(path, query) {
|
|
9719
|
+
if (!query)
|
|
9720
|
+
return path;
|
|
9721
|
+
const params = query instanceof URLSearchParams ? query : new URLSearchParams;
|
|
9722
|
+
if (!(query instanceof URLSearchParams)) {
|
|
9723
|
+
for (const [key, value] of Object.entries(query)) {
|
|
9724
|
+
if (value === null || value === undefined)
|
|
9725
|
+
continue;
|
|
9726
|
+
if (Array.isArray(value)) {
|
|
9727
|
+
for (const v of value)
|
|
9728
|
+
params.append(key, String(v));
|
|
9729
|
+
} else {
|
|
9730
|
+
params.append(key, String(value));
|
|
9731
|
+
}
|
|
9732
|
+
}
|
|
9733
|
+
}
|
|
9734
|
+
const qs = params.toString();
|
|
9735
|
+
if (!qs)
|
|
9736
|
+
return path;
|
|
9737
|
+
return `${path}${path.includes("?") ? "&" : "?"}${qs}`;
|
|
9738
|
+
}
|
|
9739
|
+
var defaultSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
9740
|
+
function createHasnaHttpTransport(options) {
|
|
9741
|
+
const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
9742
|
+
const base = options.baseUrl.replace(/\/+$/, "");
|
|
9743
|
+
const timeoutMs = options.timeoutMs ?? 30000;
|
|
9744
|
+
const sleep = options.sleepImpl ?? defaultSleep;
|
|
9745
|
+
const defaultRetry = options.retry;
|
|
9746
|
+
function resolveRetry(callRetry) {
|
|
9747
|
+
const chosen = callRetry !== undefined ? callRetry : defaultRetry;
|
|
9748
|
+
if (chosen === false)
|
|
9749
|
+
return null;
|
|
9750
|
+
const r = chosen ?? {};
|
|
9751
|
+
return {
|
|
9752
|
+
retries: r.retries ?? 2,
|
|
9753
|
+
baseDelayMs: r.baseDelayMs ?? 200,
|
|
9754
|
+
maxDelayMs: r.maxDelayMs ?? 2000,
|
|
9755
|
+
retryStatuses: r.retryStatuses ?? [...DEFAULT_RETRY_STATUSES]
|
|
9756
|
+
};
|
|
9757
|
+
}
|
|
9758
|
+
async function once2(method, rel, url, body, opts) {
|
|
9759
|
+
const headers = {
|
|
9760
|
+
"x-api-key": options.apiKey,
|
|
9761
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
9762
|
+
Accept: "application/json",
|
|
9763
|
+
...options.headers ?? {},
|
|
9764
|
+
...opts.headers ?? {}
|
|
9765
|
+
};
|
|
9766
|
+
if (opts.idempotencyKey)
|
|
9767
|
+
headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
9768
|
+
const init = { method, headers };
|
|
9769
|
+
if (body !== undefined) {
|
|
9770
|
+
headers["Content-Type"] = "application/json";
|
|
9771
|
+
init.body = JSON.stringify(body);
|
|
9772
|
+
}
|
|
9773
|
+
const controller = new AbortController;
|
|
9774
|
+
const onAbort = () => controller.abort();
|
|
9775
|
+
if (opts.signal) {
|
|
9776
|
+
if (opts.signal.aborted)
|
|
9777
|
+
controller.abort();
|
|
9778
|
+
else
|
|
9779
|
+
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
9780
|
+
}
|
|
9781
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? timeoutMs);
|
|
9782
|
+
init.signal = controller.signal;
|
|
9783
|
+
let response;
|
|
9784
|
+
try {
|
|
9785
|
+
response = await fetchImpl(url, init);
|
|
9786
|
+
} catch (error) {
|
|
9787
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
9788
|
+
if (opts.signal?.aborted)
|
|
9789
|
+
return { ok: false, retryable: false, error: err };
|
|
9790
|
+
return { ok: false, retryable: true, error: err };
|
|
9791
|
+
} finally {
|
|
9792
|
+
clearTimeout(timer);
|
|
9793
|
+
if (opts.signal)
|
|
9794
|
+
opts.signal.removeEventListener("abort", onAbort);
|
|
9795
|
+
}
|
|
9796
|
+
const text = await response.text();
|
|
9797
|
+
let parsed = undefined;
|
|
9798
|
+
if (text.length > 0) {
|
|
9799
|
+
try {
|
|
9800
|
+
parsed = JSON.parse(text);
|
|
9801
|
+
} catch {
|
|
9802
|
+
parsed = text;
|
|
9803
|
+
}
|
|
9804
|
+
}
|
|
9805
|
+
if (!response.ok) {
|
|
9806
|
+
const retry = resolveRetry(opts.retry);
|
|
9807
|
+
const retryable = retry ? retry.retryStatuses.includes(response.status) : false;
|
|
9808
|
+
return { ok: false, retryable, error: new HasnaHttpError(method, rel, response.status, parsed) };
|
|
9809
|
+
}
|
|
9810
|
+
return { ok: true, value: parsed };
|
|
9811
|
+
}
|
|
9812
|
+
async function request(method, path, body, opts = {}) {
|
|
9813
|
+
const upper = method.toUpperCase();
|
|
9814
|
+
const rel = appendQuery(path.startsWith("/") ? path : `/${path}`, opts.query);
|
|
9815
|
+
const url = `${base}${rel}`;
|
|
9816
|
+
const retry = resolveRetry(opts.retry);
|
|
9817
|
+
const methodRetryable = IDEMPOTENT_METHODS.has(upper) || Boolean(opts.idempotencyKey);
|
|
9818
|
+
const maxAttempts = retry && methodRetryable ? retry.retries + 1 : 1;
|
|
9819
|
+
let last = null;
|
|
9820
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
9821
|
+
const result = await once2(upper, rel, url, body, opts);
|
|
9822
|
+
if (result.ok)
|
|
9823
|
+
return result.value;
|
|
9824
|
+
last = result;
|
|
9825
|
+
const canRetry = retry !== null && methodRetryable && result.retryable && attempt < maxAttempts;
|
|
9826
|
+
if (!canRetry)
|
|
9827
|
+
break;
|
|
9828
|
+
const backoff = Math.min(retry.maxDelayMs, retry.baseDelayMs * 2 ** (attempt - 1));
|
|
9829
|
+
const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
|
|
9830
|
+
await sleep(backoff + jitter);
|
|
9831
|
+
}
|
|
9832
|
+
throw last.error;
|
|
9833
|
+
}
|
|
9834
|
+
return {
|
|
9835
|
+
baseUrl: base,
|
|
9836
|
+
request,
|
|
9837
|
+
get: (path, opts) => request("GET", path, undefined, opts),
|
|
9838
|
+
post: (path, body, opts) => request("POST", path, body, opts),
|
|
9839
|
+
put: (path, body, opts) => request("PUT", path, body, opts),
|
|
9840
|
+
patch: (path, body, opts) => request("PATCH", path, body, opts),
|
|
9841
|
+
del: (path, body, opts) => request("DELETE", path, body, opts)
|
|
9842
|
+
};
|
|
9843
|
+
}
|
|
9844
|
+
function createClientTransport(name, env = process.env, overrides) {
|
|
9845
|
+
const resolution = resolveClientTransport(name, env);
|
|
9846
|
+
if (resolution.misconfigured) {
|
|
9847
|
+
throw new Error(resolution.warning ?? `Client for '${name}' is misconfigured for cloud mode.`);
|
|
9848
|
+
}
|
|
9849
|
+
if (resolution.transport === "local" || !resolution.baseUrl) {
|
|
9850
|
+
return { transport: "local", client: null, resolution };
|
|
9851
|
+
}
|
|
9852
|
+
const keys = clientTransportEnvKeys(name);
|
|
9853
|
+
const apiKey = firstEnv(env, keys.apiKeyKeys)?.value;
|
|
9854
|
+
if (!apiKey) {
|
|
9855
|
+
throw new Error(`Client for '${name}' resolved to cloud-http without an API key.`);
|
|
9856
|
+
}
|
|
9857
|
+
return {
|
|
9858
|
+
transport: "cloud-http",
|
|
9859
|
+
client: createHasnaHttpTransport({
|
|
9860
|
+
name,
|
|
9861
|
+
baseUrl: resolution.baseUrl,
|
|
9862
|
+
apiKey,
|
|
9863
|
+
...overrides?.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
|
|
9864
|
+
...overrides?.headers ? { headers: overrides.headers } : {},
|
|
9865
|
+
...overrides?.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
|
|
9866
|
+
...overrides?.retry !== undefined ? { retry: overrides.retry } : {},
|
|
9867
|
+
...overrides?.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
|
|
9868
|
+
}),
|
|
9869
|
+
resolution
|
|
9870
|
+
};
|
|
9871
|
+
}
|
|
9872
|
+
|
|
9873
|
+
// src/lib/cloud/storage.ts
|
|
9874
|
+
function resourcePath(resource) {
|
|
9875
|
+
const trimmed = resource.replace(/^\/+|\/+$/g, "");
|
|
9876
|
+
if (!trimmed)
|
|
9877
|
+
throw new Error("resource must be a non-empty path segment");
|
|
9878
|
+
return `/${trimmed}`;
|
|
9879
|
+
}
|
|
9880
|
+
function entityPath(resource, id) {
|
|
9881
|
+
if (id === undefined || id === null || `${id}`.length === 0) {
|
|
9882
|
+
throw new Error("id must be a non-empty string");
|
|
9883
|
+
}
|
|
9884
|
+
return `${resourcePath(resource)}/${encodeURIComponent(String(id))}`;
|
|
9885
|
+
}
|
|
9886
|
+
function newIdempotencyKey() {
|
|
9887
|
+
const g = globalThis;
|
|
9888
|
+
if (g.crypto?.randomUUID)
|
|
9889
|
+
return g.crypto.randomUUID();
|
|
9890
|
+
return `idmp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
|
|
9891
|
+
}
|
|
9892
|
+
function extractItems(raw) {
|
|
9893
|
+
if (Array.isArray(raw))
|
|
9894
|
+
return raw;
|
|
9895
|
+
if (raw && typeof raw === "object") {
|
|
9896
|
+
const obj = raw;
|
|
9897
|
+
for (const key of ["items", "data", "results", "rows", "records"]) {
|
|
9898
|
+
if (Array.isArray(obj[key]))
|
|
9899
|
+
return obj[key];
|
|
9900
|
+
}
|
|
9901
|
+
}
|
|
9902
|
+
return [];
|
|
9903
|
+
}
|
|
9904
|
+
function extractTotal(raw) {
|
|
9905
|
+
if (raw && typeof raw === "object") {
|
|
9906
|
+
const obj = raw;
|
|
9907
|
+
for (const key of ["total", "count", "totalCount", "total_count"]) {
|
|
9908
|
+
if (typeof obj[key] === "number")
|
|
9909
|
+
return obj[key];
|
|
9910
|
+
}
|
|
9911
|
+
}
|
|
9912
|
+
return null;
|
|
9913
|
+
}
|
|
9914
|
+
function extractCursor(raw) {
|
|
9915
|
+
if (raw && typeof raw === "object") {
|
|
9916
|
+
const obj = raw;
|
|
9917
|
+
for (const key of ["cursor", "nextCursor", "next_cursor", "next"]) {
|
|
9918
|
+
if (typeof obj[key] === "string")
|
|
9919
|
+
return obj[key];
|
|
9920
|
+
}
|
|
9921
|
+
}
|
|
9922
|
+
return null;
|
|
9923
|
+
}
|
|
9924
|
+
function createHasnaStorageClient(name, transport) {
|
|
9925
|
+
return {
|
|
9926
|
+
name,
|
|
9927
|
+
baseUrl: transport.baseUrl,
|
|
9928
|
+
transport,
|
|
9929
|
+
async list(resource, options = {}) {
|
|
9930
|
+
const raw = await transport.get(resourcePath(resource), options);
|
|
9931
|
+
return {
|
|
9932
|
+
items: extractItems(raw),
|
|
9933
|
+
total: extractTotal(raw),
|
|
9934
|
+
cursor: extractCursor(raw),
|
|
9935
|
+
raw
|
|
9936
|
+
};
|
|
9937
|
+
},
|
|
9938
|
+
async get(resource, id, options = {}) {
|
|
9939
|
+
try {
|
|
9940
|
+
return await transport.get(entityPath(resource, id), options);
|
|
9941
|
+
} catch (error) {
|
|
9942
|
+
if (error instanceof HasnaHttpError && error.status === 404)
|
|
9943
|
+
return null;
|
|
9944
|
+
throw error;
|
|
9945
|
+
}
|
|
9946
|
+
},
|
|
9947
|
+
async create(resource, body, options = {}) {
|
|
9948
|
+
const { idempotencyKey, ...rest } = options;
|
|
9949
|
+
return transport.post(resourcePath(resource), body, {
|
|
9950
|
+
...rest,
|
|
9951
|
+
idempotencyKey: idempotencyKey ?? newIdempotencyKey()
|
|
9952
|
+
});
|
|
9953
|
+
},
|
|
9954
|
+
async update(resource, id, patch, options = {}) {
|
|
9955
|
+
const { method = "PATCH", idempotencyKey, ...rest } = options;
|
|
9956
|
+
const call = method === "PUT" ? transport.put : transport.patch;
|
|
9957
|
+
return call(entityPath(resource, id), patch, { ...rest, ...idempotencyKey ? { idempotencyKey } : {} });
|
|
9958
|
+
},
|
|
9959
|
+
async delete(resource, id, options = {}) {
|
|
9960
|
+
try {
|
|
9961
|
+
await transport.del(entityPath(resource, id), undefined, options);
|
|
9962
|
+
} catch (error) {
|
|
9963
|
+
if (error instanceof HasnaHttpError && error.status === 404)
|
|
9964
|
+
return;
|
|
9965
|
+
throw error;
|
|
9966
|
+
}
|
|
9967
|
+
}
|
|
9968
|
+
};
|
|
9969
|
+
}
|
|
9970
|
+
|
|
9971
|
+
// src/lib/cloud/resolve.ts
|
|
9972
|
+
function firstValue(env, keys) {
|
|
9973
|
+
for (const key of keys) {
|
|
9974
|
+
const value = env[key]?.trim();
|
|
9975
|
+
if (value)
|
|
9976
|
+
return value;
|
|
9977
|
+
}
|
|
9978
|
+
return;
|
|
9979
|
+
}
|
|
9980
|
+
function resolveCloudStorage(name, env = process.env) {
|
|
9981
|
+
const token = envToken(name);
|
|
9982
|
+
const modeKeys = [`HASNA_${token}_STORAGE_MODE`, `HASNA_${token}_MODE`, `${token}_STORAGE_MODE`, `${token}_MODE`];
|
|
9983
|
+
const apiUrlKeys = [`HASNA_${token}_API_URL`, `${token}_API_URL`];
|
|
9984
|
+
const apiKeyKeys = [
|
|
9985
|
+
`HASNA_${token}_API_KEY`,
|
|
9986
|
+
`${token}_API_KEY`,
|
|
9987
|
+
`HASNA_${token}_API_TOKEN`,
|
|
9988
|
+
`${token}_API_TOKEN`
|
|
9989
|
+
];
|
|
9990
|
+
const explicitMode = firstValue(env, modeKeys);
|
|
9991
|
+
if (explicitMode && normalizeStorageMode(explicitMode).mode === "local") {
|
|
9992
|
+
return { transport: "local", client: null };
|
|
9993
|
+
}
|
|
9994
|
+
const apiUrl = firstValue(env, apiUrlKeys);
|
|
9995
|
+
const apiKey = firstValue(env, apiKeyKeys);
|
|
9996
|
+
if (!apiUrl || !apiKey) {
|
|
9997
|
+
return { transport: "local", client: null };
|
|
9998
|
+
}
|
|
9999
|
+
const cloudEnv = { ...env, [`HASNA_${token}_STORAGE_MODE`]: "self_hosted" };
|
|
10000
|
+
const wired = createClientTransport(name, cloudEnv);
|
|
10001
|
+
if (wired.transport !== "cloud-http") {
|
|
10002
|
+
return { transport: "local", client: null };
|
|
10003
|
+
}
|
|
10004
|
+
return {
|
|
10005
|
+
transport: "cloud-http",
|
|
10006
|
+
client: createHasnaStorageClient(name, wired.client),
|
|
10007
|
+
baseUrl: wired.client.baseUrl
|
|
10008
|
+
};
|
|
10009
|
+
}
|
|
10010
|
+
|
|
10011
|
+
// src/lib/store/index.ts
|
|
10012
|
+
class CloudUnsupportedError extends Error {
|
|
10013
|
+
constructor(operation) {
|
|
10014
|
+
super(`operation not supported over the hosted OpenLoops API: ${operation}. ` + `Run it on a machine using local storage, or unset HASNA_LOOPS_API_URL/HASNA_LOOPS_API_KEY.`);
|
|
10015
|
+
this.name = "CloudUnsupportedError";
|
|
10016
|
+
}
|
|
10017
|
+
}
|
|
10018
|
+
|
|
10019
|
+
class LocalStore {
|
|
10020
|
+
transport = "local";
|
|
10021
|
+
store;
|
|
10022
|
+
constructor(store = new Store) {
|
|
10023
|
+
this.store = store;
|
|
10024
|
+
}
|
|
10025
|
+
get raw() {
|
|
10026
|
+
return this.store;
|
|
10027
|
+
}
|
|
10028
|
+
async close() {
|
|
10029
|
+
this.store.close();
|
|
10030
|
+
}
|
|
10031
|
+
async createLoop(input, from) {
|
|
10032
|
+
return from ? this.store.createLoop(input, from) : this.store.createLoop(input);
|
|
10033
|
+
}
|
|
10034
|
+
async getLoop(id) {
|
|
10035
|
+
return this.store.getLoop(id);
|
|
10036
|
+
}
|
|
10037
|
+
async findLoopByName(name) {
|
|
10038
|
+
return this.store.findLoopByName(name);
|
|
10039
|
+
}
|
|
10040
|
+
async requireLoop(idOrName) {
|
|
10041
|
+
return this.store.requireLoop(idOrName);
|
|
10042
|
+
}
|
|
10043
|
+
async requireUniqueLoop(idOrName) {
|
|
10044
|
+
return this.store.requireUniqueLoop(idOrName);
|
|
10045
|
+
}
|
|
10046
|
+
async listLoops(opts = {}) {
|
|
10047
|
+
return this.store.listLoops(opts);
|
|
10048
|
+
}
|
|
10049
|
+
async countLoops(status, opts = {}) {
|
|
10050
|
+
return this.store.countLoops(status, opts);
|
|
10051
|
+
}
|
|
10052
|
+
async updateLoop(id, patch) {
|
|
10053
|
+
return this.store.updateLoop(id, patch);
|
|
10054
|
+
}
|
|
10055
|
+
async renameLoop(id, name) {
|
|
10056
|
+
return this.store.renameLoop(id, name);
|
|
10057
|
+
}
|
|
10058
|
+
async archiveLoop(idOrName) {
|
|
10059
|
+
return this.store.archiveLoop(idOrName);
|
|
10060
|
+
}
|
|
10061
|
+
async unarchiveLoop(idOrName) {
|
|
10062
|
+
return this.store.unarchiveLoop(idOrName);
|
|
10063
|
+
}
|
|
10064
|
+
async deleteLoop(idOrName) {
|
|
10065
|
+
return this.store.deleteLoop(idOrName);
|
|
10066
|
+
}
|
|
10067
|
+
async createWorkflow(input) {
|
|
10068
|
+
return this.store.createWorkflow(input);
|
|
10069
|
+
}
|
|
10070
|
+
async getWorkflow(id) {
|
|
10071
|
+
return this.store.getWorkflow(id);
|
|
10072
|
+
}
|
|
10073
|
+
async findWorkflowByName(name) {
|
|
10074
|
+
return this.store.findWorkflowByName(name);
|
|
10075
|
+
}
|
|
10076
|
+
async requireWorkflow(idOrName) {
|
|
10077
|
+
return this.store.requireWorkflow(idOrName);
|
|
10078
|
+
}
|
|
10079
|
+
async listWorkflows(opts = {}) {
|
|
10080
|
+
return this.store.listWorkflows(opts);
|
|
10081
|
+
}
|
|
10082
|
+
async countWorkflows(opts = {}) {
|
|
10083
|
+
return this.store.countWorkflows(opts);
|
|
10084
|
+
}
|
|
10085
|
+
async archiveWorkflow(idOrName) {
|
|
10086
|
+
return this.store.archiveWorkflow(idOrName);
|
|
10087
|
+
}
|
|
10088
|
+
async getWorkflowRun(id) {
|
|
10089
|
+
return this.store.getWorkflowRun(id);
|
|
10090
|
+
}
|
|
10091
|
+
async requireWorkflowRun(id) {
|
|
10092
|
+
return this.store.requireWorkflowRun(id);
|
|
10093
|
+
}
|
|
10094
|
+
async listWorkflowRuns(opts = {}) {
|
|
10095
|
+
return this.store.listWorkflowRuns(opts);
|
|
10096
|
+
}
|
|
10097
|
+
async listWorkflowStepRuns(workflowRunId) {
|
|
10098
|
+
return this.store.listWorkflowStepRuns(workflowRunId);
|
|
10099
|
+
}
|
|
10100
|
+
async listWorkflowEvents(workflowRunId, limit) {
|
|
10101
|
+
return limit === undefined ? this.store.listWorkflowEvents(workflowRunId) : this.store.listWorkflowEvents(workflowRunId, limit);
|
|
10102
|
+
}
|
|
10103
|
+
async recoverWorkflowRun(workflowRunId, reason) {
|
|
10104
|
+
return reason === undefined ? this.store.recoverWorkflowRun(workflowRunId) : this.store.recoverWorkflowRun(workflowRunId, reason);
|
|
10105
|
+
}
|
|
10106
|
+
async cancelWorkflowRun(workflowRunId, reason) {
|
|
10107
|
+
return reason === undefined ? this.store.cancelWorkflowRun(workflowRunId) : this.store.cancelWorkflowRun(workflowRunId, reason);
|
|
10108
|
+
}
|
|
10109
|
+
async listWorkflowWorkItems(opts = {}) {
|
|
10110
|
+
return this.store.listWorkflowWorkItems(opts);
|
|
10111
|
+
}
|
|
10112
|
+
async getWorkflowWorkItem(id) {
|
|
10113
|
+
return this.store.getWorkflowWorkItem(id);
|
|
10114
|
+
}
|
|
10115
|
+
async requeueWorkflowWorkItem(id, patch = {}) {
|
|
10116
|
+
return this.store.requeueWorkflowWorkItem(id, patch);
|
|
10117
|
+
}
|
|
10118
|
+
async listWorkflowInvocations(opts = {}) {
|
|
10119
|
+
return this.store.listWorkflowInvocations(opts);
|
|
10120
|
+
}
|
|
10121
|
+
async getWorkflowInvocation(id) {
|
|
10122
|
+
return this.store.getWorkflowInvocation(id);
|
|
10123
|
+
}
|
|
10124
|
+
async getGoal(id) {
|
|
10125
|
+
return this.store.getGoal(id);
|
|
10126
|
+
}
|
|
10127
|
+
async findGoalByLoop(idOrName) {
|
|
10128
|
+
return this.store.findGoalByLoop(idOrName);
|
|
10129
|
+
}
|
|
10130
|
+
async findGoalByRunId(id) {
|
|
10131
|
+
return this.store.findGoalByRunId(id);
|
|
10132
|
+
}
|
|
10133
|
+
async listGoals(opts = {}) {
|
|
10134
|
+
return this.store.listGoals(opts);
|
|
10135
|
+
}
|
|
10136
|
+
async listGoalPlanNodes(goalIdOrPlanId) {
|
|
10137
|
+
return this.store.listGoalPlanNodes(goalIdOrPlanId);
|
|
10138
|
+
}
|
|
10139
|
+
async listGoalRuns(opts = {}) {
|
|
10140
|
+
return this.store.listGoalRuns(opts);
|
|
10141
|
+
}
|
|
10142
|
+
async listRuns(opts = {}) {
|
|
10143
|
+
return this.store.listRuns(opts);
|
|
10144
|
+
}
|
|
10145
|
+
async getRun(id) {
|
|
10146
|
+
return this.store.getRun(id);
|
|
10147
|
+
}
|
|
10148
|
+
async writeRunReceipt(input) {
|
|
10149
|
+
return this.store.writeRunReceipt(input);
|
|
10150
|
+
}
|
|
10151
|
+
async getRunReceipt(runId) {
|
|
10152
|
+
return this.store.getRunReceipt(runId);
|
|
10153
|
+
}
|
|
10154
|
+
async listRunReceipts(opts = {}) {
|
|
10155
|
+
return this.store.listRunReceipts(opts);
|
|
10156
|
+
}
|
|
10157
|
+
async pruneHistory(opts) {
|
|
10158
|
+
return this.store.pruneHistory(opts);
|
|
10159
|
+
}
|
|
10160
|
+
}
|
|
10161
|
+
function clean(query) {
|
|
10162
|
+
const out = {};
|
|
10163
|
+
for (const [key, value] of Object.entries(query)) {
|
|
10164
|
+
if (value !== undefined && value !== null && value !== "")
|
|
10165
|
+
out[key] = value;
|
|
10166
|
+
}
|
|
10167
|
+
return out;
|
|
10168
|
+
}
|
|
10169
|
+
function pickArray(raw, key, fallback = []) {
|
|
10170
|
+
if (raw && typeof raw === "object" && Array.isArray(raw[key])) {
|
|
10171
|
+
return raw[key];
|
|
10172
|
+
}
|
|
10173
|
+
return fallback;
|
|
10174
|
+
}
|
|
10175
|
+
function pickObject(raw, key) {
|
|
10176
|
+
if (raw && typeof raw === "object") {
|
|
10177
|
+
const value = raw[key];
|
|
10178
|
+
return value == null ? undefined : value;
|
|
10179
|
+
}
|
|
10180
|
+
return raw == null ? undefined : raw;
|
|
10181
|
+
}
|
|
10182
|
+
|
|
10183
|
+
class ApiStore {
|
|
10184
|
+
client;
|
|
10185
|
+
baseUrl;
|
|
10186
|
+
transport = "cloud-http";
|
|
10187
|
+
constructor(client, baseUrl) {
|
|
10188
|
+
this.client = client;
|
|
10189
|
+
this.baseUrl = baseUrl;
|
|
10190
|
+
}
|
|
10191
|
+
get t() {
|
|
10192
|
+
return this.client.transport;
|
|
10193
|
+
}
|
|
10194
|
+
async close() {}
|
|
10195
|
+
async createLoop(input) {
|
|
10196
|
+
return pickObject(await this.t.post("/loops", input), "loop");
|
|
10197
|
+
}
|
|
10198
|
+
async getLoop(id) {
|
|
10199
|
+
try {
|
|
10200
|
+
return pickObject(await this.t.get(`/loops/${encodeURIComponent(id)}`), "loop");
|
|
10201
|
+
} catch {
|
|
10202
|
+
return;
|
|
10203
|
+
}
|
|
10204
|
+
}
|
|
10205
|
+
async findLoopByName(name) {
|
|
10206
|
+
const matches = (await this.listLoops({ name, limit: 1000 })).filter((loop) => loop.name === name);
|
|
10207
|
+
return matches[0];
|
|
10208
|
+
}
|
|
10209
|
+
async requireLoop(idOrName) {
|
|
10210
|
+
const loop = await this.resolveLoop(idOrName);
|
|
10211
|
+
if (!loop)
|
|
10212
|
+
throw new LoopNotFoundError(idOrName);
|
|
10213
|
+
return loop;
|
|
10214
|
+
}
|
|
10215
|
+
async requireUniqueLoop(idOrName) {
|
|
10216
|
+
const byId = await this.getLoop(idOrName);
|
|
10217
|
+
if (byId)
|
|
10218
|
+
return byId;
|
|
10219
|
+
const matches = (await this.listLoops({ name: idOrName, limit: 1000 })).filter((loop) => loop.name === idOrName);
|
|
10220
|
+
if (matches.length === 0)
|
|
10221
|
+
throw new LoopNotFoundError(idOrName);
|
|
10222
|
+
if (matches.length === 1)
|
|
10223
|
+
return matches[0];
|
|
10224
|
+
const active = matches.filter((loop) => !loop.archivedAt);
|
|
10225
|
+
if (active.length !== 1)
|
|
10226
|
+
throw new AmbiguousNameError(idOrName);
|
|
10227
|
+
return active[0];
|
|
10228
|
+
}
|
|
10229
|
+
async resolveLoop(idOrName) {
|
|
10230
|
+
const byId = await this.getLoop(idOrName);
|
|
10231
|
+
if (byId)
|
|
10232
|
+
return byId;
|
|
10233
|
+
const matches = (await this.listLoops({ name: idOrName, limit: 1000 })).filter((loop) => loop.name === idOrName);
|
|
10234
|
+
if (matches.length === 0)
|
|
10235
|
+
return;
|
|
10236
|
+
if (matches.length === 1)
|
|
10237
|
+
return matches[0];
|
|
10238
|
+
const active = matches.filter((loop) => !loop.archivedAt);
|
|
10239
|
+
if (active.length === 1)
|
|
10240
|
+
return active[0];
|
|
10241
|
+
throw new AmbiguousNameError(idOrName);
|
|
10242
|
+
}
|
|
10243
|
+
async listLoops(opts = {}) {
|
|
10244
|
+
const raw = await this.t.get("/loops", { query: clean({ ...opts }) });
|
|
10245
|
+
return pickArray(raw, "loops");
|
|
10246
|
+
}
|
|
10247
|
+
async countLoops(status, opts = {}) {
|
|
10248
|
+
const raw = await this.t.get("/loops/count", { query: clean({ status, ...opts }) });
|
|
10249
|
+
return Number(pickObject(raw, "count") ?? 0);
|
|
10250
|
+
}
|
|
10251
|
+
async updateLoop(id, patch) {
|
|
10252
|
+
return pickObject(await this.t.patch(`/loops/${encodeURIComponent(id)}`, patch), "loop");
|
|
10253
|
+
}
|
|
10254
|
+
async renameLoop(id, name) {
|
|
10255
|
+
return pickObject(await this.t.post(`/loops/${encodeURIComponent(id)}/rename`, { name }), "loop");
|
|
10256
|
+
}
|
|
10257
|
+
async archiveLoop(idOrName) {
|
|
10258
|
+
const loop = await this.requireLoop(idOrName);
|
|
10259
|
+
return pickObject(await this.t.post(`/loops/${encodeURIComponent(loop.id)}/archive`), "loop");
|
|
10260
|
+
}
|
|
10261
|
+
async unarchiveLoop(idOrName) {
|
|
10262
|
+
const loop = await this.requireLoop(idOrName);
|
|
10263
|
+
return pickObject(await this.t.post(`/loops/${encodeURIComponent(loop.id)}/unarchive`), "loop");
|
|
10264
|
+
}
|
|
10265
|
+
async deleteLoop(idOrName) {
|
|
10266
|
+
const loop = await this.resolveLoop(idOrName);
|
|
10267
|
+
if (!loop)
|
|
10268
|
+
return false;
|
|
10269
|
+
const raw = await this.t.request("DELETE", `/loops/${encodeURIComponent(loop.id)}`);
|
|
10270
|
+
return Boolean(pickObject(raw, "deleted") ?? true);
|
|
10271
|
+
}
|
|
10272
|
+
async createWorkflow(input) {
|
|
10273
|
+
return pickObject(await this.t.post("/workflows", input), "workflow");
|
|
10274
|
+
}
|
|
10275
|
+
async getWorkflow(id) {
|
|
10276
|
+
try {
|
|
10277
|
+
return pickObject(await this.t.get(`/workflows/${encodeURIComponent(id)}`), "workflow");
|
|
10278
|
+
} catch {
|
|
10279
|
+
return;
|
|
10280
|
+
}
|
|
10281
|
+
}
|
|
10282
|
+
async findWorkflowByName(name) {
|
|
10283
|
+
const matches = (await this.listWorkflows({ limit: 1000 })).filter((wf) => wf.name === name);
|
|
10284
|
+
return matches[0];
|
|
10285
|
+
}
|
|
10286
|
+
async requireWorkflow(idOrName) {
|
|
10287
|
+
const byId = await this.getWorkflow(idOrName);
|
|
10288
|
+
if (byId)
|
|
10289
|
+
return byId;
|
|
10290
|
+
const byName = await this.findWorkflowByName(idOrName);
|
|
10291
|
+
if (byName)
|
|
10292
|
+
return byName;
|
|
10293
|
+
throw new Error(`workflow not found: ${idOrName}`);
|
|
10294
|
+
}
|
|
10295
|
+
async listWorkflows(opts = {}) {
|
|
10296
|
+
const raw = await this.t.get("/workflows", { query: clean({ ...opts }) });
|
|
10297
|
+
return pickArray(raw, "workflows");
|
|
10298
|
+
}
|
|
10299
|
+
async countWorkflows(opts = {}) {
|
|
10300
|
+
const raw = await this.t.get("/workflows/count", { query: clean({ ...opts }) });
|
|
10301
|
+
return Number(pickObject(raw, "count") ?? 0);
|
|
10302
|
+
}
|
|
10303
|
+
async archiveWorkflow(idOrName) {
|
|
10304
|
+
const wf = await this.requireWorkflow(idOrName);
|
|
10305
|
+
return pickObject(await this.t.post(`/workflows/${encodeURIComponent(wf.id)}/archive`), "workflow");
|
|
10306
|
+
}
|
|
10307
|
+
async getWorkflowRun(id) {
|
|
10308
|
+
try {
|
|
10309
|
+
return pickObject(await this.t.get(`/workflow-runs/${encodeURIComponent(id)}`), "workflowRun");
|
|
10310
|
+
} catch {
|
|
10311
|
+
return;
|
|
10312
|
+
}
|
|
10313
|
+
}
|
|
10314
|
+
async requireWorkflowRun(id) {
|
|
10315
|
+
const run = await this.getWorkflowRun(id);
|
|
10316
|
+
if (!run)
|
|
10317
|
+
throw new Error(`workflow run not found: ${id}`);
|
|
10318
|
+
return run;
|
|
10319
|
+
}
|
|
10320
|
+
async listWorkflowRuns(opts = {}) {
|
|
10321
|
+
const raw = await this.t.get("/workflow-runs", { query: clean({ ...opts }) });
|
|
10322
|
+
return pickArray(raw, "workflowRuns");
|
|
10323
|
+
}
|
|
10324
|
+
async listWorkflowStepRuns(workflowRunId) {
|
|
10325
|
+
const raw = await this.t.get(`/workflow-runs/${encodeURIComponent(workflowRunId)}/steps`);
|
|
10326
|
+
return pickArray(raw, "steps");
|
|
10327
|
+
}
|
|
10328
|
+
async listWorkflowEvents(workflowRunId, limit) {
|
|
10329
|
+
const raw = await this.t.get(`/workflow-runs/${encodeURIComponent(workflowRunId)}/events`, { query: clean({ limit }) });
|
|
10330
|
+
return pickArray(raw, "events");
|
|
10331
|
+
}
|
|
10332
|
+
async recoverWorkflowRun(workflowRunId, reason) {
|
|
10333
|
+
const raw = await this.t.post(`/workflow-runs/${encodeURIComponent(workflowRunId)}/recover`, { reason });
|
|
10334
|
+
return {
|
|
10335
|
+
run: pickObject(raw, "workflowRun"),
|
|
10336
|
+
recoveredSteps: pickArray(raw, "recoveredSteps")
|
|
10337
|
+
};
|
|
10338
|
+
}
|
|
10339
|
+
async cancelWorkflowRun(_workflowRunId, _reason) {
|
|
10340
|
+
throw new CloudUnsupportedError("workflows cancel");
|
|
10341
|
+
}
|
|
10342
|
+
async listWorkflowWorkItems(opts = {}) {
|
|
10343
|
+
const raw = await this.t.get("/work-items", { query: clean({ ...opts }) });
|
|
10344
|
+
return pickArray(raw, "workItems");
|
|
10345
|
+
}
|
|
10346
|
+
async getWorkflowWorkItem(id) {
|
|
10347
|
+
try {
|
|
10348
|
+
return pickObject(await this.t.get(`/work-items/${encodeURIComponent(id)}`), "workItem");
|
|
10349
|
+
} catch {
|
|
10350
|
+
return;
|
|
10351
|
+
}
|
|
10352
|
+
}
|
|
10353
|
+
async requeueWorkflowWorkItem(_id, _patch = {}) {
|
|
10354
|
+
throw new CloudUnsupportedError("routes requeue");
|
|
10355
|
+
}
|
|
10356
|
+
async listWorkflowInvocations(opts = {}) {
|
|
10357
|
+
const raw = await this.t.get("/invocations", { query: clean({ ...opts }) });
|
|
10358
|
+
return pickArray(raw, "invocations");
|
|
10359
|
+
}
|
|
10360
|
+
async getWorkflowInvocation(id) {
|
|
10361
|
+
try {
|
|
10362
|
+
return pickObject(await this.t.get(`/invocations/${encodeURIComponent(id)}`), "invocation");
|
|
10363
|
+
} catch {
|
|
10364
|
+
return;
|
|
10365
|
+
}
|
|
10366
|
+
}
|
|
10367
|
+
async getGoal(id) {
|
|
10368
|
+
try {
|
|
10369
|
+
return pickObject(await this.t.get(`/goals/${encodeURIComponent(id)}`), "goal");
|
|
10370
|
+
} catch {
|
|
10371
|
+
return;
|
|
10372
|
+
}
|
|
10373
|
+
}
|
|
10374
|
+
async findGoalByLoop(idOrName) {
|
|
10375
|
+
try {
|
|
10376
|
+
return pickObject(await this.t.get("/goals", { query: clean({ loop: idOrName }) }), "goal");
|
|
10377
|
+
} catch {
|
|
10378
|
+
return;
|
|
10379
|
+
}
|
|
10380
|
+
}
|
|
10381
|
+
async findGoalByRunId(id) {
|
|
10382
|
+
try {
|
|
10383
|
+
return pickObject(await this.t.get("/goals", { query: clean({ runId: id }) }), "goal");
|
|
10384
|
+
} catch {
|
|
10385
|
+
return;
|
|
10386
|
+
}
|
|
10387
|
+
}
|
|
10388
|
+
async listGoals(opts = {}) {
|
|
10389
|
+
const raw = await this.t.get("/goals", { query: clean({ ...opts }) });
|
|
10390
|
+
return pickArray(raw, "goals");
|
|
10391
|
+
}
|
|
10392
|
+
async listGoalPlanNodes(goalIdOrPlanId) {
|
|
10393
|
+
const raw = await this.t.get(`/goals/${encodeURIComponent(goalIdOrPlanId)}/plan-nodes`);
|
|
10394
|
+
return pickArray(raw, "nodes");
|
|
10395
|
+
}
|
|
10396
|
+
async listGoalRuns(opts = {}) {
|
|
10397
|
+
const raw = await this.t.get("/goal-runs", { query: clean({ ...opts }) });
|
|
10398
|
+
return pickArray(raw, "goalRuns");
|
|
10399
|
+
}
|
|
10400
|
+
async listRuns(opts = {}) {
|
|
10401
|
+
const raw = await this.t.get("/runs", { query: clean({ ...opts, showOutput: true }) });
|
|
10402
|
+
return pickArray(raw, "runs");
|
|
10403
|
+
}
|
|
10404
|
+
async getRun(id) {
|
|
10405
|
+
try {
|
|
10406
|
+
return pickObject(await this.t.get(`/runs/${encodeURIComponent(id)}`, { query: { showOutput: true } }), "run");
|
|
10407
|
+
} catch {
|
|
10408
|
+
return;
|
|
10409
|
+
}
|
|
10410
|
+
}
|
|
10411
|
+
async writeRunReceipt(input) {
|
|
10412
|
+
return pickObject(await this.t.post("/receipts", input), "receipt");
|
|
10413
|
+
}
|
|
10414
|
+
async getRunReceipt(runId) {
|
|
10415
|
+
try {
|
|
10416
|
+
return pickObject(await this.t.get(`/receipts/${encodeURIComponent(runId)}`), "receipt");
|
|
10417
|
+
} catch {
|
|
10418
|
+
return;
|
|
10419
|
+
}
|
|
10420
|
+
}
|
|
10421
|
+
async listRunReceipts(opts = {}) {
|
|
10422
|
+
const raw = await this.t.get("/receipts", { query: clean({ ...opts }) });
|
|
10423
|
+
return pickArray(raw, "receipts");
|
|
10424
|
+
}
|
|
10425
|
+
async pruneHistory(opts) {
|
|
10426
|
+
const raw = await this.t.post("/history/prune", {
|
|
10427
|
+
maxAgeDays: opts.maxAgeDays,
|
|
10428
|
+
keepPerLoop: opts.keepPerLoop,
|
|
10429
|
+
dryRun: opts.dryRun
|
|
10430
|
+
});
|
|
10431
|
+
return pickObject(raw, "history");
|
|
10432
|
+
}
|
|
10433
|
+
}
|
|
10434
|
+
function getStore(env = process.env) {
|
|
10435
|
+
const resolution = resolveCloudStorage("loops", env);
|
|
10436
|
+
if (resolution.transport === "cloud-http")
|
|
10437
|
+
return new ApiStore(resolution.client, resolution.baseUrl);
|
|
10438
|
+
return new LocalStore;
|
|
10439
|
+
}
|
|
10440
|
+
function isCloudStore(env = process.env) {
|
|
10441
|
+
return resolveCloudStorage("loops", env).transport === "cloud-http";
|
|
10442
|
+
}
|
|
10443
|
+
|
|
10444
|
+
// src/sdk/index.ts
|
|
10445
|
+
class LoopsClient {
|
|
10446
|
+
store;
|
|
10447
|
+
ownStore;
|
|
10448
|
+
runnerId;
|
|
10449
|
+
constructor(opts = {}) {
|
|
10450
|
+
this.store = opts.store ? new LocalStore(opts.store) : getStore();
|
|
10451
|
+
this.ownStore = !opts.store;
|
|
10452
|
+
this.runnerId = opts.runnerId ?? `sdk:${process.pid}`;
|
|
10453
|
+
}
|
|
10454
|
+
localRuntime(operation) {
|
|
10455
|
+
if (this.store.transport !== "local") {
|
|
10456
|
+
throw new Error(`loops SDK ${operation} operates on this machine's local runtime and is not available while flipped to the hosted OpenLoops API. ` + `Unset HASNA_LOOPS_API_URL/HASNA_LOOPS_API_KEY (or set HASNA_LOOPS_STORAGE_MODE=local) to run it here.`);
|
|
10457
|
+
}
|
|
10458
|
+
return this.store.raw;
|
|
10459
|
+
}
|
|
10460
|
+
create(input) {
|
|
10461
|
+
return this.store.createLoop(input);
|
|
10462
|
+
}
|
|
10463
|
+
list(filters = {}) {
|
|
10464
|
+
return this.store.listLoops({
|
|
10465
|
+
status: filters.status,
|
|
10466
|
+
limit: filters.limit,
|
|
10467
|
+
includeArchived: filters.includeArchived,
|
|
10468
|
+
archived: filters.archivedOnly
|
|
10469
|
+
});
|
|
9107
10470
|
}
|
|
9108
10471
|
get(idOrName) {
|
|
9109
10472
|
return this.store.requireLoop(idOrName);
|
|
9110
10473
|
}
|
|
9111
|
-
pause(idOrName) {
|
|
9112
|
-
|
|
10474
|
+
async pause(idOrName) {
|
|
10475
|
+
const loop = await this.store.requireUniqueLoop(idOrName);
|
|
10476
|
+
return this.store.updateLoop(loop.id, { status: "paused" });
|
|
9113
10477
|
}
|
|
9114
|
-
resume(idOrName) {
|
|
9115
|
-
const loop = this.store.requireUniqueLoop(idOrName);
|
|
10478
|
+
async resume(idOrName) {
|
|
10479
|
+
const loop = await this.store.requireUniqueLoop(idOrName);
|
|
9116
10480
|
let nextRunAt = loop.nextRunAt;
|
|
9117
10481
|
if (!nextRunAt) {
|
|
9118
10482
|
const now = new Date;
|
|
@@ -9120,8 +10484,9 @@ class LoopsClient {
|
|
|
9120
10484
|
}
|
|
9121
10485
|
return this.store.updateLoop(loop.id, { status: "active", nextRunAt });
|
|
9122
10486
|
}
|
|
9123
|
-
stop(idOrName) {
|
|
9124
|
-
|
|
10487
|
+
async stop(idOrName) {
|
|
10488
|
+
const loop = await this.store.requireUniqueLoop(idOrName);
|
|
10489
|
+
return this.store.updateLoop(loop.id, { status: "stopped", nextRunAt: undefined });
|
|
9125
10490
|
}
|
|
9126
10491
|
archive(idOrName) {
|
|
9127
10492
|
return this.store.archiveLoop(idOrName);
|
|
@@ -9129,14 +10494,15 @@ class LoopsClient {
|
|
|
9129
10494
|
unarchive(idOrName) {
|
|
9130
10495
|
return this.store.unarchiveLoop(idOrName);
|
|
9131
10496
|
}
|
|
9132
|
-
delete(idOrName) {
|
|
9133
|
-
|
|
10497
|
+
async delete(idOrName) {
|
|
10498
|
+
const loop = await this.store.requireUniqueLoop(idOrName);
|
|
10499
|
+
return this.store.deleteLoop(loop.id);
|
|
9134
10500
|
}
|
|
9135
|
-
runs(idOrName, filters = {}) {
|
|
10501
|
+
async runs(idOrName, filters = {}) {
|
|
9136
10502
|
let loopId;
|
|
9137
10503
|
if (idOrName) {
|
|
9138
10504
|
try {
|
|
9139
|
-
loopId = this.get(idOrName).id;
|
|
10505
|
+
loopId = (await this.get(idOrName)).id;
|
|
9140
10506
|
} catch (error) {
|
|
9141
10507
|
if (error instanceof LoopNotFoundError)
|
|
9142
10508
|
return [];
|
|
@@ -9145,48 +10511,59 @@ class LoopsClient {
|
|
|
9145
10511
|
}
|
|
9146
10512
|
return this.store.listRuns({ loopId, status: filters.status, limit: filters.limit });
|
|
9147
10513
|
}
|
|
10514
|
+
writeReceipt(input) {
|
|
10515
|
+
return this.store.writeRunReceipt(input);
|
|
10516
|
+
}
|
|
10517
|
+
receipt(runId) {
|
|
10518
|
+
return this.store.getRunReceipt(runId);
|
|
10519
|
+
}
|
|
10520
|
+
receipts(filters = {}) {
|
|
10521
|
+
return this.store.listRunReceipts(filters);
|
|
10522
|
+
}
|
|
10523
|
+
async goal(idOrName) {
|
|
10524
|
+
const goal = await this.store.getGoal(idOrName) ?? await this.store.findGoalByLoop(idOrName) ?? await this.store.findGoalByRunId(idOrName);
|
|
10525
|
+
return {
|
|
10526
|
+
goal,
|
|
10527
|
+
runs: goal ? await this.store.listGoalRuns({ goalId: goal.goalId }) : []
|
|
10528
|
+
};
|
|
10529
|
+
}
|
|
9148
10530
|
doctor() {
|
|
9149
|
-
return runDoctor(this.
|
|
10531
|
+
return runDoctor(this.localRuntime("doctor()"));
|
|
9150
10532
|
}
|
|
9151
10533
|
health(opts = {}) {
|
|
9152
|
-
return buildHealthReport(this.
|
|
10534
|
+
return buildHealthReport(this.localRuntime("health()"), opts);
|
|
9153
10535
|
}
|
|
9154
10536
|
healthScan(opts = {}) {
|
|
9155
|
-
|
|
10537
|
+
const store = this.localRuntime("healthScan()");
|
|
10538
|
+
return buildHealthScan(store, {
|
|
9156
10539
|
...opts,
|
|
9157
|
-
doctor: opts.doctor ? runDoctor(
|
|
9158
|
-
daemon: opts.daemon ? daemonStatus(
|
|
10540
|
+
doctor: opts.doctor ? runDoctor(store) : undefined,
|
|
10541
|
+
daemon: opts.daemon ? daemonStatus(store) : undefined
|
|
9159
10542
|
});
|
|
9160
10543
|
}
|
|
9161
|
-
goal(idOrName) {
|
|
9162
|
-
const goal = this.store.getGoal(idOrName) ?? this.store.findGoalByLoop(idOrName) ?? this.store.findGoalByRunId(idOrName);
|
|
9163
|
-
return {
|
|
9164
|
-
goal,
|
|
9165
|
-
runs: goal ? this.store.listGoalRuns({ goalId: goal.goalId }) : []
|
|
9166
|
-
};
|
|
9167
|
-
}
|
|
9168
10544
|
async tick() {
|
|
9169
|
-
return tick({ store: this.
|
|
10545
|
+
return tick({ store: this.localRuntime("tick()"), runnerId: this.runnerId });
|
|
9170
10546
|
}
|
|
9171
10547
|
async runNow(idOrName) {
|
|
9172
|
-
const
|
|
10548
|
+
const store = this.localRuntime("runNow()");
|
|
10549
|
+
const result = await runLoopNow({ store, idOrName: store.requireUniqueLoop(idOrName).id, runnerId: this.runnerId });
|
|
9173
10550
|
return result.run;
|
|
9174
10551
|
}
|
|
9175
10552
|
exportBundle(opts = {}) {
|
|
9176
|
-
return exportLoopsMigrationBundle(this.
|
|
10553
|
+
return exportLoopsMigrationBundle(this.localRuntime("exportBundle()"), opts);
|
|
9177
10554
|
}
|
|
9178
10555
|
planImport(bundle, opts = {}) {
|
|
9179
|
-
return buildImportMigrationPlan(this.
|
|
10556
|
+
return buildImportMigrationPlan(this.localRuntime("planImport()"), bundle, opts);
|
|
9180
10557
|
}
|
|
9181
10558
|
importBundle(bundle, opts = {}) {
|
|
9182
|
-
return applyImportMigrationBundle(this.
|
|
10559
|
+
return applyImportMigrationBundle(this.localRuntime("importBundle()"), bundle, opts);
|
|
9183
10560
|
}
|
|
9184
10561
|
planSelfHostedMigration(opts = {}) {
|
|
9185
|
-
return buildSelfHostedMigrationPlan(this.
|
|
10562
|
+
return buildSelfHostedMigrationPlan(this.localRuntime("planSelfHostedMigration()"), { ...opts, operation: opts.operation ?? "self-hosted-migrate" });
|
|
9186
10563
|
}
|
|
9187
|
-
close() {
|
|
10564
|
+
async close() {
|
|
9188
10565
|
if (this.ownStore)
|
|
9189
|
-
this.store.close();
|
|
10566
|
+
await this.store.close();
|
|
9190
10567
|
}
|
|
9191
10568
|
}
|
|
9192
10569
|
function loops(opts = {}) {
|